diff --git a/packages/trace-viewer/src/ui/timeline.tsx b/packages/trace-viewer/src/ui/timeline.tsx index 4c6eb2f099..797e2bb3e3 100644 --- a/packages/trace-viewer/src/ui/timeline.tsx +++ b/packages/trace-viewer/src/ui/timeline.tsx @@ -114,7 +114,6 @@ export const Timeline: React.FunctionComponent<{ const xd = Math.abs(time - xMiddle); if (left > right) continue; - // Prefer closest yDistance (the same bar), among those prefer the closest xDistance. if (index === undefined || xd < xDistance!) { index = i; xDistance = xd; diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index 89c1e473f0..d9c45726a7 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -52,17 +52,18 @@ const xtermDataSource: XtermDataSource = { export const WatchModeView: React.FC<{}> = ({ }) => { + const [isWatchingFiles, setIsWatchingFiles] = useSetting('test-ui-watch-files', false); + const [filterText, setFilterText] = useSetting('test-ui-filter-text', ''); + const [filterExpanded, setFilterExpanded] = useSetting('test-ui-filter-expanded', false); + const [isShowingOutput, setIsShowingOutput] = useSetting('test-ui-show-output', false); + const [projects, setProjects] = React.useState>(new Map()); const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined }); - const [isRunningTest, setIsRunningTest] = React.useState(false); const [progress, setProgress] = React.useState({ total: 0, passed: 0, failed: 0, skipped: 0 }); const [selectedTest, setSelectedTest] = React.useState(undefined); const [settingsVisible, setSettingsVisible] = React.useState(false); - const [isWatchingFiles, setIsWatchingFiles] = React.useState(true); const [visibleTestIds, setVisibleTestIds] = React.useState([]); - const [filterText, setFilterText] = React.useState(''); - const [filterExpanded, setFilterExpanded] = React.useState(false); - const [isShowingOutput, setIsShowingOutput] = React.useState(false); + const [runningState, setRunningState] = React.useState<{ testIds: Set, itemSelectedByUser?: boolean }>(); const inputRef = React.useRef(null); React.useEffect(() => { @@ -101,9 +102,9 @@ export const WatchModeView: React.FC<{}> = ({ const time = ' [' + new Date().toLocaleTimeString() + ']'; xtermDataSource.write('\x1B[2m—'.repeat(Math.max(0, xtermSize.cols - time.length)) + time + '\x1B[22m'); setProgress({ total: testIds.length, passed: 0, failed: 0, skipped: 0 }); - setIsRunningTest(true); + setRunningState({ testIds: new Set(testIds) }); sendMessage('run', { testIds }).then(() => { - setIsRunningTest(false); + setRunningState(undefined); }); }; @@ -125,6 +126,7 @@ export const WatchModeView: React.FC<{}> = ({ setFilterText(result.join(' ')); }; + const isRunningTest = !!runningState; const result = selectedTest?.results[0]; const isFinished = result && result.duration >= 0; return
@@ -182,7 +184,7 @@ export const WatchModeView: React.FC<{}> = ({ projects={projects} filterText={filterText} rootSuite={rootSuite} - isRunningTest={isRunningTest} + runningState={runningState} isWatchingFiles={isWatchingFiles} runTests={runTests} onTestSelected={setSelectedTest} @@ -209,12 +211,12 @@ export const TestList: React.FC<{ filterText: string, rootSuite: { value: Suite | undefined }, runTests: (testIds: string[]) => void, - isRunningTest: boolean, + runningState?: { testIds: Set, itemSelectedByUser?: boolean }, isWatchingFiles: boolean, isVisible: boolean, setVisibleTestIds: (testIds: string[]) => void, onTestSelected: (test: TestCase | undefined) => void, -}> = ({ projects, filterText, rootSuite, runTests, isRunningTest, isWatchingFiles, isVisible, onTestSelected, setVisibleTestIds }) => { +}> = ({ projects, filterText, rootSuite, runTests, runningState, isWatchingFiles, isVisible, onTestSelected, setVisibleTestIds }) => { const [treeState, setTreeState] = React.useState({ expandedItems: new Map() }); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState(); @@ -239,6 +241,28 @@ export const TestList: React.FC<{ return { rootItem, treeItemMap }; }, [filterText, rootSuite, projects, setVisibleTestIds]); + React.useEffect(() => { + // Look for a first failure within the run batch to select it. + if (!runningState || runningState.itemSelectedByUser) + return; + let selectedTreeItem: TreeItem | undefined; + const visit = (treeItem: TreeItem) => { + if (selectedTreeItem) + return; + treeItem.children.forEach(visit); + if (treeItem.status === 'failed') { + if (treeItem.kind === 'test' && runningState.testIds.has(treeItem.test.id)) + selectedTreeItem = treeItem; + else if (treeItem.kind === 'case' && runningState.testIds.has(treeItem.tests[0]?.id)) + selectedTreeItem = treeItem; + } + }; + visit(rootItem); + + if (selectedTreeItem) + setSelectedTreeItemId(selectedTreeItem.id); + }, [runningState, setSelectedTreeItemId, rootItem]); + const { selectedTreeItem } = React.useMemo(() => { const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined; let selectedTest: TestCase | undefined; @@ -273,7 +297,7 @@ export const TestList: React.FC<{ render={treeItem => { return
{treeItem.title}
- runTreeItem(treeItem)} disabled={isRunningTest}> + runTreeItem(treeItem)} disabled={!!runningState}> sendMessageNoReply('open', { location: locationToOpen(treeItem) })}>
; }} @@ -291,6 +315,8 @@ export const TestList: React.FC<{ selectedItem={selectedTreeItem} onAccepted={runTreeItem} onSelected={treeItem => { + if (runningState) + runningState.itemSelectedByUser = true; setSelectedTreeItemId(treeItem.id); }} noItemsMessage='No tests' />; @@ -509,6 +535,7 @@ type TreeItemBase = { id: string; title: string; location: Location, + parent: TreeItem | undefined; children: TreeItem[]; status: 'none' | 'running' | 'passed' | 'failed' | 'skipped'; }; @@ -538,6 +565,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map id: 'root', title: '', location: { file: '', line: 0, column: 0 }, + parent: undefined, children: [], status: 'none', }; @@ -552,6 +580,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map id: parentGroup.id + '\x1e' + title, title, location: suite.location!, + parent: parentGroup, children: [], status: 'none', }; @@ -568,6 +597,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map kind: 'case', id: parentGroup.id + '\x1e' + title, title, + parent: parentGroup, children: [], tests: [], location: test.location, @@ -593,6 +623,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map title: projectName, location: test.location!, test, + parent: testCaseItem, children: [], status, project: projectName @@ -751,3 +782,17 @@ function stepsToModel(result: TestResult): MultiTraceModel { return new MultiTraceModel([contextEntry]); } + +function useSetting(name: string, defaultValue: S): [S, React.Dispatch>] { + const string = localStorage.getItem(name); + let value = defaultValue; + if (string !== null) + value = JSON.parse(string); + + const [state, setState] = React.useState(value); + const setStateWrapper = (value: React.SetStateAction) => { + localStorage.setItem(name, JSON.stringify(value)); + setState(value); + }; + return [state, setStateWrapper]; +} diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index 8f96e760d4..b13d8f233a 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -19,6 +19,7 @@ import { ListView } from './listView'; export type TreeItem = { id: string, + parent: TreeItem | undefined, children: TreeItem[], }; @@ -57,8 +58,10 @@ export function TreeView({ noItemsMessage, }: TreeViewProps) { const treeItems = React.useMemo(() => { + for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent) + treeState.expandedItems.set(item.id, true); return flattenTree(rootItem, treeState.expandedItems); - }, [rootItem, treeState]); + }, [rootItem, selectedItem, treeState]); return ({ }} onIconClicked={item => { const { expanded } = treeItems.get(item as T)!; - if (expanded) + if (expanded) { + // Move nested selection up. + for (let i: TreeItem | undefined = selectedItem; i; i = i.parent) { + if (i === item) { + onSelected?.(item as T); + break; + } + } treeState.expandedItems.set(item.id, false); - else + } else { treeState.expandedItems.set(item.id, true); + } setTreeState({ ...treeState }); }} noItemsMessage={noItemsMessage} />;