chore: merge projects in the tree (#21401)

This commit is contained in:
Pavel Feldman 2023-03-04 16:28:30 -08:00 committed by GitHub
parent f0cd123a45
commit ec056a6312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 113 deletions

View file

@ -393,7 +393,7 @@ export class TeleTestCase implements reporterTypes.TestCase {
retry: this.results.length, retry: this.results.length,
parallelIndex: -1, parallelIndex: -1,
workerIndex: -1, workerIndex: -1,
duration: 0, duration: -1,
startTime: new Date(), startTime: new Date(),
stdout: [], stdout: [],
stderr: [], stderr: [],

View file

@ -113,11 +113,11 @@ class UIMode {
const stop = new ManualPromise(); const stop = new ManualPromise();
const run = taskRunner.run(context, 0, stop).then(async status => { const run = taskRunner.run(context, 0, stop).then(async status => {
await reporter.onExit({ status }); await reporter.onExit({ status });
this._testRun = undefined;
return status; return status;
}); });
this._testRun = { run, stop }; this._testRun = { run, stop };
await run; await run;
this._testRun = undefined;
} }
private async _watchFile(fileName: string) { private async _watchFile(fileName: string) {

View file

@ -32,17 +32,14 @@ let rootSuite: Suite | undefined;
let updateList: () => void = () => {}; let updateList: () => void = () => {};
let updateProgress: () => void = () => {}; let updateProgress: () => void = () => {};
let runWatchedTests = () => {}; let runWatchedTests = () => {};
const expandedItems = new Map<string, boolean | undefined>();
type Entry = { test?: TestCase, fileSuite: Suite };
export const WatchModeView: React.FC<{}> = ({ export const WatchModeView: React.FC<{}> = ({
}) => { }) => {
const [updateCounter, setUpdateCounter] = React.useState(0); const [updateCounter, setUpdateCounter] = React.useState(0);
updateList = () => setUpdateCounter(updateCounter + 1); updateList = () => setUpdateCounter(updateCounter + 1);
const [selectedFileSuite, setSelectedFileSuite] = React.useState<Suite | undefined>(); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>();
const [isRunningTest, setIsRunningTest] = React.useState<boolean>(false); const [isRunningTest, setIsRunningTest] = React.useState<boolean>(false);
const [expandedFiles] = React.useState(new Map<Suite, boolean | undefined>());
const [filterText, setFilterText] = React.useState<string>(''); const [filterText, setFilterText] = React.useState<string>('');
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
@ -52,61 +49,39 @@ export const WatchModeView: React.FC<{}> = ({
sendMessageNoReply('list'); sendMessageNoReply('list');
}, []); }, []);
const { treeItemMap, visibleTestIds, listItems } = React.useMemo(() => {
// updateCounter is used to trigger the compute.
noop(updateCounter);
const treeItems = createTree(rootSuite);
const filteredItems = filterTree(treeItems, filterText);
const treeItemMap = new Map<string, TreeItem>();
const visibleTestIds = new Set<string>();
const visit = (treeItem: TreeItem) => {
if (treeItem.kind === 'test')
visibleTestIds.add(treeItem.id);
treeItem.children?.forEach(visit);
treeItemMap.set(treeItem.id, treeItem);
};
filteredItems.forEach(visit);
const listItems = flattenTree(filteredItems, expandedItems, !!filterText.trim());
return { treeItemMap, visibleTestIds, listItems };
}, [filterText, updateCounter]);
const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined;
React.useEffect(() => { React.useEffect(() => {
sendMessageNoReply('watch', { sendMessageNoReply('watch', { fileName: fileName(selectedTreeItem) });
fileName: selectedFileSuite?.location?.file || selectedTest?.location?.file }, [selectedTreeItem, treeItemMap]);
});
}, [selectedFileSuite, selectedTest]);
const selectedOrDefaultFileSuite = selectedFileSuite || rootSuite?.suites?.[0]?.suites?.[0]; const runTreeItem = (treeItem: TreeItem) => {
const tests: TestCase[] = []; expandedItems.set(treeItem.id, true);
const fileSuites: Suite[] = []; setSelectedTreeItemId(treeItem.id);
runTests(collectTestIds(treeItem));
for (const projectSuite of rootSuite?.suites || []) {
for (const fileSuite of projectSuite.suites) {
if (fileSuite === selectedOrDefaultFileSuite)
tests.push(...fileSuite.allTests());
fileSuites.push(fileSuite);
}
}
const explicitlyOrAutoExpandedFiles = new Set<Suite>();
const entries = new Map<TestCase | Suite, Entry>();
const trimmedFilterText = filterText.trim();
const filterTokens = trimmedFilterText.toLowerCase().split(' ');
for (const fileSuite of fileSuites) {
const hasMatch = !trimmedFilterText || fileSuite.allTests().some(test => {
const fullTitle = test.titlePath().join(' ').toLowerCase();
return !filterTokens.some(token => !fullTitle.includes(token));
});
if (hasMatch)
entries.set(fileSuite, { fileSuite });
const expandState = expandedFiles.get(fileSuite);
const autoExpandMatches = entries.size < 100 && (trimmedFilterText && hasMatch && expandState !== false);
if (expandState === true || autoExpandMatches) {
explicitlyOrAutoExpandedFiles.add(fileSuite);
for (const test of fileSuite.allTests()) {
const fullTitle = test.titlePath().join(' ').toLowerCase();
if (!filterTokens.some(token => !fullTitle.includes(token)))
entries.set(test, { test, fileSuite });
}
}
}
const visibleTestIds = new Set<string>();
for (const { test } of entries.values()) {
if (test)
visibleTestIds.add(test.id);
}
const runEntry = (entry: Entry) => {
expandedFiles.set(entry.fileSuite, true);
setSelectedTest(entry.test);
runTests(collectTestIds(entry));
}; };
runWatchedTests = () => { runWatchedTests = () => {
runTests(collectTestIds({ test: selectedTest, fileSuite: selectedFileSuite || selectedTest!.parent })); runTests(collectTestIds(selectedTreeItem));
}; };
const runTests = (testIds: string[] | undefined) => { const runTests = (testIds: string[] | undefined) => {
@ -116,9 +91,8 @@ export const WatchModeView: React.FC<{}> = ({
}); });
}; };
const selectedEntry = selectedTest ? entries.get(selectedTest) : selectedOrDefaultFileSuite ? entries.get(selectedOrDefaultFileSuite) : undefined;
return <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}> return <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
<TraceView test={selectedTest} isRunningTest={isRunningTest}></TraceView> <TraceView testItem={selectedTreeItem?.kind === 'test' ? selectedTreeItem : undefined} isRunningTest={isRunningTest}></TraceView>
<div className='vbox watch-mode-sidebar'> <div className='vbox watch-mode-sidebar'>
<Toolbar> <Toolbar>
<input ref={inputRef} type='search' placeholder='Filter tests' spellCheck={false} value={filterText} <input ref={inputRef} type='search' placeholder='Filter tests' spellCheck={false} value={filterText}
@ -133,53 +107,56 @@ export const WatchModeView: React.FC<{}> = ({
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton> <ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
</Toolbar> </Toolbar>
<ListView <ListView
items={[...entries.values()]} items={listItems}
itemKey={(entry: Entry) => entry.test ? entry.test!.id : entry.fileSuite.title } itemKey={(treeItem: TreeItem) => treeItem.id }
itemRender={(entry: Entry) => { itemRender={(treeItem: TreeItem) => {
return <div className='hbox watch-mode-list-item'> return <div className='hbox watch-mode-list-item'>
<div className='watch-mode-list-item-title'>{entry.test ? entry.test!.titlePath().slice(3).join(' ') : entry.fileSuite.title}</div> <div className='watch-mode-list-item-title'>{treeItem.title}</div>
<ToolbarButton icon='play' title='Run' onClick={() => runEntry(entry)} disabled={isRunningTest}></ToolbarButton> <ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={isRunningTest}></ToolbarButton>
</div>; </div>;
}} }}
itemIcon={(entry: Entry) => { itemIcon={(treeItem: TreeItem) => {
if (entry.test) { if (treeItem.kind === 'case' && treeItem.children?.length === 1)
if (entry.test.results.length && entry.test.results[0].duration) treeItem = treeItem.children[0];
return entry.test.ok() ? 'codicon-check' : 'codicon-error'; if (treeItem.kind === 'test') {
if (entry.test.results.length) const ok = treeItem.test.outcome() === 'expected';
const failed = treeItem.test.results.length && treeItem.test.outcome() !== 'expected';
const running = treeItem.test.results.some(r => r.duration === -1);
if (running)
return 'codicon-loading'; return 'codicon-loading';
if (ok)
return 'codicon-check';
if (failed)
return 'codicon-error';
} else { } else {
if (explicitlyOrAutoExpandedFiles.has(entry.fileSuite)) return treeItem.expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
return 'codicon-chevron-down';
return 'codicon-chevron-right';
} }
}} }}
itemIndent={(entry: Entry) => entry.test ? 1 : 0} itemIndent={(treeItem: TreeItem) => treeItem.kind === 'file' ? 0 : treeItem.kind === 'case' ? 1 : 2}
selectedItem={selectedEntry} selectedItem={selectedTreeItem}
onAccepted={runEntry} onAccepted={runTreeItem}
onLeftArrow={(entry: Entry) => { onLeftArrow={(treeItem: TreeItem) => {
expandedFiles.set(entry.fileSuite, false); if (treeItem.children && treeItem.expanded)
setSelectedTest(undefined); expandedItems.set(treeItem.id, false);
setSelectedFileSuite(entry.fileSuite);
updateList();
}}
onRightArrow={(entry: Entry) => {
expandedFiles.set(entry.fileSuite, true);
updateList();
}}
onSelected={(entry: Entry) => {
if (entry.test) {
setSelectedFileSuite(undefined);
setSelectedTest(entry.test!);
} else {
setSelectedTest(undefined);
setSelectedFileSuite(entry.fileSuite);
}
}}
onIconClicked={(entry: Entry) => {
if (explicitlyOrAutoExpandedFiles.has(entry.fileSuite))
expandedFiles.set(entry.fileSuite, false);
else else
expandedFiles.set(entry.fileSuite, true); setSelectedTreeItemId(treeItem.parent?.id);
updateList();
}}
onRightArrow={(treeItem: TreeItem) => {
if (treeItem.children)
expandedItems.set(treeItem.id, true);
updateList();
}}
onSelected={(treeItem: TreeItem) => {
setSelectedTreeItemId(treeItem.id);
}}
onIconClicked={(treeItem: TreeItem) => {
if (treeItem.kind === 'test')
return;
if (treeItem.expanded)
expandedItems.set(treeItem.id, false);
else
expandedItems.set(treeItem.id, true);
updateList(); updateList();
}} }}
showNoItemsMessage={true}></ListView> showNoItemsMessage={true}></ListView>
@ -188,17 +165,16 @@ export const WatchModeView: React.FC<{}> = ({
}; };
export const ProgressView: React.FC<{ export const ProgressView: React.FC<{
test: TestCase | undefined, testItem: TestItem | undefined,
}> = ({ }> = ({
test, testItem,
}) => { }) => {
const [updateCounter, setUpdateCounter] = React.useState(0); const [updateCounter, setUpdateCounter] = React.useState(0);
updateProgress = () => setUpdateCounter(updateCounter + 1); updateProgress = () => setUpdateCounter(updateCounter + 1);
const steps: (TestCase | TestStep)[] = []; const steps: (TestCase | TestStep)[] = [];
for (const result of test?.results || []) for (const result of testItem?.test.results || [])
steps.push(...result.steps); steps.push(...result.steps);
return <ListView return <ListView
items={steps} items={steps}
itemRender={(step: TestStep) => step.title} itemRender={(step: TestStep) => step.title}
@ -207,18 +183,18 @@ export const ProgressView: React.FC<{
}; };
export const TraceView: React.FC<{ export const TraceView: React.FC<{
test: TestCase | undefined, testItem: TestItem | undefined,
isRunningTest: boolean, isRunningTest: boolean,
}> = ({ test, isRunningTest }) => { }> = ({ testItem, isRunningTest }) => {
const [model, setModel] = React.useState<MultiTraceModel | undefined>(); const [model, setModel] = React.useState<MultiTraceModel | undefined>();
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
if (!test) { if (!testItem) {
setModel(undefined); setModel(undefined);
return; return;
} }
for (const result of test.results) { for (const result of testItem?.test.results || []) {
const attachment = result.attachments.find(a => a.name === 'trace'); const attachment = result.attachments.find(a => a.name === 'trace');
if (attachment && attachment.path) { if (attachment && attachment.path) {
setModel(await loadSingleTraceFile(attachment.path)); setModel(await loadSingleTraceFile(attachment.path));
@ -227,10 +203,10 @@ export const TraceView: React.FC<{
} }
setModel(undefined); setModel(undefined);
})(); })();
}, [test, isRunningTest]); }, [testItem, isRunningTest]);
if (isRunningTest) if (isRunningTest)
return <ProgressView test={test}></ProgressView>; return <ProgressView testItem={testItem}></ProgressView>;
if (!model) { if (!model) {
return <div className='vbox'> return <div className='vbox'>
@ -296,11 +272,136 @@ const sendMessageNoReply = (method: string, params?: any) => {
}); });
}; };
const collectTestIds = (entry: Entry): string[] => { const fileName = (treeItem?: TreeItem): string | undefined => {
if (!treeItem)
return;
if (treeItem.kind === 'file')
return treeItem.file;
return fileName(treeItem.parent || undefined);
};
const collectTestIds = (treeItem?: TreeItem): string[] => {
if (!treeItem)
return [];
const testIds: string[] = []; const testIds: string[] = [];
if (entry.test) const visit = (treeItem: TreeItem) => {
testIds.push(entry.test.id); if (treeItem.kind === 'test')
else testIds.push(treeItem.id);
entry.fileSuite.allTests().forEach(test => testIds.push(test.id)); treeItem.children?.forEach(visit);
};
visit(treeItem);
return testIds; return testIds;
}; };
type TreeItemBase = {
kind: 'file' | 'case' | 'test',
id: string;
title: string;
parent: TreeItem | null;
children?: TreeItem[];
expanded?: boolean;
};
type FileItem = TreeItemBase & {
kind: 'file',
file: string;
};
type TestCaseItem = TreeItemBase & {
kind: 'case',
};
type TestItem = TreeItemBase & {
kind: 'test',
test: TestCase;
};
type TreeItem = FileItem | TestCaseItem | TestItem;
function createTree(rootSuite?: Suite): FileItem[] {
const fileItems = new Map<string, FileItem>();
for (const projectSuite of rootSuite?.suites || []) {
for (const fileSuite of projectSuite.suites) {
const file = fileSuite.location!.file;
let fileItem = fileItems.get(file);
if (!fileItem) {
fileItem = {
kind: 'file',
id: fileSuite.title,
title: fileSuite.title,
file,
parent: null,
children: [],
expanded: false,
};
fileItems.set(fileSuite.location!.file, fileItem);
}
for (const test of fileSuite.allTests()) {
const title = test.titlePath().slice(3).join(' ');
let testCaseItem = fileItem.children!.find(t => t.title === title);
if (!testCaseItem) {
testCaseItem = {
kind: 'case',
id: fileItem.id + ' / ' + title,
title,
parent: fileItem,
children: [],
expanded: false,
};
fileItem.children!.push(testCaseItem);
}
testCaseItem.children!.push({
kind: 'test',
id: test.id,
title: projectSuite.title,
parent: testCaseItem,
test,
});
}
}
}
return [...fileItems.values()];
}
function filterTree(fileItems: FileItem[], filterText: string): FileItem[] {
const trimmedFilterText = filterText.trim();
const filterTokens = trimmedFilterText.toLowerCase().split(' ');
const result: FileItem[] = [];
for (const fileItem of fileItems) {
if (trimmedFilterText) {
const filteredCases: TreeItem[] = [];
for (const testCaseItem of fileItem.children!) {
const fullTitle = (fileItem.title + ' ' + testCaseItem.title).toLowerCase();
if (filterTokens.every(token => fullTitle.includes(token)))
filteredCases.push(testCaseItem);
}
fileItem.children = filteredCases;
}
if (fileItem.children!.length)
result.push(fileItem);
}
return result;
}
function flattenTree(fileItems: FileItem[], expandedItems: Map<string, boolean | undefined>, hasFilter: boolean): TreeItem[] {
const result: TreeItem[] = [];
for (const fileItem of fileItems) {
result.push(fileItem);
const expandState = expandedItems.get(fileItem.id);
const autoExpandMatches = result.length < 100 && (hasFilter && expandState !== false);
if (expandState || autoExpandMatches) {
fileItem.expanded = true;
for (const testCaseItem of fileItem.children!) {
result.push(testCaseItem);
testCaseItem.expanded = !!expandedItems.get(testCaseItem.id);
if (testCaseItem.expanded && testCaseItem.children!.length > 1)
result.push(...testCaseItem.children!);
}
}
}
return result;
}
function noop(_: any) {}