chore: select first failure (#21593)
This commit is contained in:
parent
b85d670491
commit
08a5922c4d
|
|
@ -114,7 +114,6 @@ export const Timeline: React.FunctionComponent<{
|
||||||
const xd = Math.abs(time - xMiddle);
|
const xd = Math.abs(time - xMiddle);
|
||||||
if (left > right)
|
if (left > right)
|
||||||
continue;
|
continue;
|
||||||
// Prefer closest yDistance (the same bar), among those prefer the closest xDistance.
|
|
||||||
if (index === undefined || xd < xDistance!) {
|
if (index === undefined || xd < xDistance!) {
|
||||||
index = i;
|
index = i;
|
||||||
xDistance = xd;
|
xDistance = xd;
|
||||||
|
|
|
||||||
|
|
@ -52,17 +52,18 @@ const xtermDataSource: XtermDataSource = {
|
||||||
|
|
||||||
export const WatchModeView: React.FC<{}> = ({
|
export const WatchModeView: React.FC<{}> = ({
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isWatchingFiles, setIsWatchingFiles] = useSetting<boolean>('test-ui-watch-files', false);
|
||||||
|
const [filterText, setFilterText] = useSetting<string>('test-ui-filter-text', '');
|
||||||
|
const [filterExpanded, setFilterExpanded] = useSetting<boolean>('test-ui-filter-expanded', false);
|
||||||
|
const [isShowingOutput, setIsShowingOutput] = useSetting<boolean>('test-ui-show-output', false);
|
||||||
|
|
||||||
const [projects, setProjects] = React.useState<Map<string, boolean>>(new Map());
|
const [projects, setProjects] = React.useState<Map<string, boolean>>(new Map());
|
||||||
const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined });
|
const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined });
|
||||||
const [isRunningTest, setIsRunningTest] = React.useState<boolean>(false);
|
|
||||||
const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0, skipped: 0 });
|
const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0, skipped: 0 });
|
||||||
const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined);
|
const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined);
|
||||||
const [settingsVisible, setSettingsVisible] = React.useState<boolean>(false);
|
const [settingsVisible, setSettingsVisible] = React.useState<boolean>(false);
|
||||||
const [isWatchingFiles, setIsWatchingFiles] = React.useState<boolean>(true);
|
|
||||||
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
|
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
|
||||||
const [filterText, setFilterText] = React.useState<string>('');
|
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean }>();
|
||||||
const [filterExpanded, setFilterExpanded] = React.useState<boolean>(false);
|
|
||||||
const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(false);
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -101,9 +102,9 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
const time = ' [' + new Date().toLocaleTimeString() + ']';
|
const time = ' [' + new Date().toLocaleTimeString() + ']';
|
||||||
xtermDataSource.write('\x1B[2m—'.repeat(Math.max(0, xtermSize.cols - time.length)) + time + '\x1B[22m');
|
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 });
|
setProgress({ total: testIds.length, passed: 0, failed: 0, skipped: 0 });
|
||||||
setIsRunningTest(true);
|
setRunningState({ testIds: new Set(testIds) });
|
||||||
sendMessage('run', { testIds }).then(() => {
|
sendMessage('run', { testIds }).then(() => {
|
||||||
setIsRunningTest(false);
|
setRunningState(undefined);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,6 +126,7 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
setFilterText(result.join(' '));
|
setFilterText(result.join(' '));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRunningTest = !!runningState;
|
||||||
const result = selectedTest?.results[0];
|
const result = selectedTest?.results[0];
|
||||||
const isFinished = result && result.duration >= 0;
|
const isFinished = result && result.duration >= 0;
|
||||||
return <div className='vbox watch-mode'>
|
return <div className='vbox watch-mode'>
|
||||||
|
|
@ -182,7 +184,7 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
projects={projects}
|
projects={projects}
|
||||||
filterText={filterText}
|
filterText={filterText}
|
||||||
rootSuite={rootSuite}
|
rootSuite={rootSuite}
|
||||||
isRunningTest={isRunningTest}
|
runningState={runningState}
|
||||||
isWatchingFiles={isWatchingFiles}
|
isWatchingFiles={isWatchingFiles}
|
||||||
runTests={runTests}
|
runTests={runTests}
|
||||||
onTestSelected={setSelectedTest}
|
onTestSelected={setSelectedTest}
|
||||||
|
|
@ -209,12 +211,12 @@ export const TestList: React.FC<{
|
||||||
filterText: string,
|
filterText: string,
|
||||||
rootSuite: { value: Suite | undefined },
|
rootSuite: { value: Suite | undefined },
|
||||||
runTests: (testIds: string[]) => void,
|
runTests: (testIds: string[]) => void,
|
||||||
isRunningTest: boolean,
|
runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean },
|
||||||
isWatchingFiles: boolean,
|
isWatchingFiles: boolean,
|
||||||
isVisible: boolean,
|
isVisible: boolean,
|
||||||
setVisibleTestIds: (testIds: string[]) => void,
|
setVisibleTestIds: (testIds: string[]) => void,
|
||||||
onTestSelected: (test: TestCase | undefined) => 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<TreeState>({ expandedItems: new Map() });
|
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||||
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
||||||
|
|
||||||
|
|
@ -239,6 +241,28 @@ export const TestList: React.FC<{
|
||||||
return { rootItem, treeItemMap };
|
return { rootItem, treeItemMap };
|
||||||
}, [filterText, rootSuite, projects, setVisibleTestIds]);
|
}, [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 } = React.useMemo(() => {
|
||||||
const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined;
|
const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined;
|
||||||
let selectedTest: TestCase | undefined;
|
let selectedTest: TestCase | undefined;
|
||||||
|
|
@ -273,7 +297,7 @@ export const TestList: React.FC<{
|
||||||
render={treeItem => {
|
render={treeItem => {
|
||||||
return <div className='hbox watch-mode-list-item'>
|
return <div className='hbox watch-mode-list-item'>
|
||||||
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
|
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
|
||||||
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={isRunningTest}></ToolbarButton>
|
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState}></ToolbarButton>
|
||||||
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => sendMessageNoReply('open', { location: locationToOpen(treeItem) })}></ToolbarButton>
|
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => sendMessageNoReply('open', { location: locationToOpen(treeItem) })}></ToolbarButton>
|
||||||
</div>;
|
</div>;
|
||||||
}}
|
}}
|
||||||
|
|
@ -291,6 +315,8 @@ export const TestList: React.FC<{
|
||||||
selectedItem={selectedTreeItem}
|
selectedItem={selectedTreeItem}
|
||||||
onAccepted={runTreeItem}
|
onAccepted={runTreeItem}
|
||||||
onSelected={treeItem => {
|
onSelected={treeItem => {
|
||||||
|
if (runningState)
|
||||||
|
runningState.itemSelectedByUser = true;
|
||||||
setSelectedTreeItemId(treeItem.id);
|
setSelectedTreeItemId(treeItem.id);
|
||||||
}}
|
}}
|
||||||
noItemsMessage='No tests' />;
|
noItemsMessage='No tests' />;
|
||||||
|
|
@ -509,6 +535,7 @@ type TreeItemBase = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
location: Location,
|
location: Location,
|
||||||
|
parent: TreeItem | undefined;
|
||||||
children: TreeItem[];
|
children: TreeItem[];
|
||||||
status: 'none' | 'running' | 'passed' | 'failed' | 'skipped';
|
status: 'none' | 'running' | 'passed' | 'failed' | 'skipped';
|
||||||
};
|
};
|
||||||
|
|
@ -538,6 +565,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>
|
||||||
id: 'root',
|
id: 'root',
|
||||||
title: '',
|
title: '',
|
||||||
location: { file: '', line: 0, column: 0 },
|
location: { file: '', line: 0, column: 0 },
|
||||||
|
parent: undefined,
|
||||||
children: [],
|
children: [],
|
||||||
status: 'none',
|
status: 'none',
|
||||||
};
|
};
|
||||||
|
|
@ -552,6 +580,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>
|
||||||
id: parentGroup.id + '\x1e' + title,
|
id: parentGroup.id + '\x1e' + title,
|
||||||
title,
|
title,
|
||||||
location: suite.location!,
|
location: suite.location!,
|
||||||
|
parent: parentGroup,
|
||||||
children: [],
|
children: [],
|
||||||
status: 'none',
|
status: 'none',
|
||||||
};
|
};
|
||||||
|
|
@ -568,6 +597,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>
|
||||||
kind: 'case',
|
kind: 'case',
|
||||||
id: parentGroup.id + '\x1e' + title,
|
id: parentGroup.id + '\x1e' + title,
|
||||||
title,
|
title,
|
||||||
|
parent: parentGroup,
|
||||||
children: [],
|
children: [],
|
||||||
tests: [],
|
tests: [],
|
||||||
location: test.location,
|
location: test.location,
|
||||||
|
|
@ -593,6 +623,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>
|
||||||
title: projectName,
|
title: projectName,
|
||||||
location: test.location!,
|
location: test.location!,
|
||||||
test,
|
test,
|
||||||
|
parent: testCaseItem,
|
||||||
children: [],
|
children: [],
|
||||||
status,
|
status,
|
||||||
project: projectName
|
project: projectName
|
||||||
|
|
@ -751,3 +782,17 @@ function stepsToModel(result: TestResult): MultiTraceModel {
|
||||||
|
|
||||||
return new MultiTraceModel([contextEntry]);
|
return new MultiTraceModel([contextEntry]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useSetting<S>(name: string, defaultValue: S): [S, React.Dispatch<React.SetStateAction<S>>] {
|
||||||
|
const string = localStorage.getItem(name);
|
||||||
|
let value = defaultValue;
|
||||||
|
if (string !== null)
|
||||||
|
value = JSON.parse(string);
|
||||||
|
|
||||||
|
const [state, setState] = React.useState<S>(value);
|
||||||
|
const setStateWrapper = (value: React.SetStateAction<S>) => {
|
||||||
|
localStorage.setItem(name, JSON.stringify(value));
|
||||||
|
setState(value);
|
||||||
|
};
|
||||||
|
return [state, setStateWrapper];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { ListView } from './listView';
|
||||||
|
|
||||||
export type TreeItem = {
|
export type TreeItem = {
|
||||||
id: string,
|
id: string,
|
||||||
|
parent: TreeItem | undefined,
|
||||||
children: TreeItem[],
|
children: TreeItem[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -57,8 +58,10 @@ export function TreeView<T extends TreeItem>({
|
||||||
noItemsMessage,
|
noItemsMessage,
|
||||||
}: TreeViewProps<T>) {
|
}: TreeViewProps<T>) {
|
||||||
const treeItems = React.useMemo(() => {
|
const treeItems = React.useMemo(() => {
|
||||||
|
for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent)
|
||||||
|
treeState.expandedItems.set(item.id, true);
|
||||||
return flattenTree<T>(rootItem, treeState.expandedItems);
|
return flattenTree<T>(rootItem, treeState.expandedItems);
|
||||||
}, [rootItem, treeState]);
|
}, [rootItem, selectedItem, treeState]);
|
||||||
|
|
||||||
return <TreeListView
|
return <TreeListView
|
||||||
items={[...treeItems.keys()]}
|
items={[...treeItems.keys()]}
|
||||||
|
|
@ -98,10 +101,18 @@ export function TreeView<T extends TreeItem>({
|
||||||
}}
|
}}
|
||||||
onIconClicked={item => {
|
onIconClicked={item => {
|
||||||
const { expanded } = treeItems.get(item as T)!;
|
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);
|
treeState.expandedItems.set(item.id, false);
|
||||||
else
|
} else {
|
||||||
treeState.expandedItems.set(item.id, true);
|
treeState.expandedItems.set(item.id, true);
|
||||||
|
}
|
||||||
setTreeState({ ...treeState });
|
setTreeState({ ...treeState });
|
||||||
}}
|
}}
|
||||||
noItemsMessage={noItemsMessage} />;
|
noItemsMessage={noItemsMessage} />;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue