chore: align test tree with vscode (#29864)

This commit is contained in:
Pavel Feldman 2024-03-14 15:44:35 -07:00 committed by GitHub
parent 23bfeec5c7
commit 94348bb3c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 94 additions and 51 deletions

View file

@ -39,28 +39,31 @@ export type TestCaseItem = TreeItemBase & {
kind: 'case', kind: 'case',
tests: reporterTypes.TestCase[]; tests: reporterTypes.TestCase[];
children: TestItem[]; children: TestItem[];
test: reporterTypes.TestCase | undefined;
project: reporterTypes.FullProject | undefined;
}; };
export type TestItem = TreeItemBase & { export type TestItem = TreeItemBase & {
kind: 'test', kind: 'test',
test: reporterTypes.TestCase; test: reporterTypes.TestCase;
project: string; project: reporterTypes.FullProject;
}; };
export type TreeItem = GroupItem | TestCaseItem | TestItem; export type TreeItem = GroupItem | TestCaseItem | TestItem;
export class TestTree { export class TestTree {
rootItem: GroupItem; rootItem: GroupItem;
readonly treeItemMap = new Map<string, TreeItem>(); private _treeItemById = new Map<string, TreeItem>();
readonly visibleTestIds = new Set<string>(); private _treeItemByTestId = new Map<string, TestItem | TestCaseItem>();
readonly fileNames = new Set<string>(); readonly pathSeparator: string;
constructor(rootSuite: reporterTypes.Suite | undefined, loadErrors: reporterTypes.TestError[], projectFilters: Map<string, boolean>) { constructor(rootFolder: string, rootSuite: reporterTypes.Suite | undefined, loadErrors: reporterTypes.TestError[], projectFilters: Map<string, boolean> | undefined, pathSeparator: string) {
const filterProjects = [...projectFilters.values()].some(Boolean); const filterProjects = projectFilters && [...projectFilters.values()].some(Boolean);
this.pathSeparator = pathSeparator;
this.rootItem = { this.rootItem = {
kind: 'group', kind: 'group',
subKind: 'folder', subKind: 'folder',
id: 'root', id: rootFolder,
title: '', title: '',
location: { file: '', line: 0, column: 0 }, location: { file: '', line: 0, column: 0 },
duration: 0, duration: 0,
@ -69,8 +72,9 @@ export class TestTree {
status: 'none', status: 'none',
hasLoadErrors: false, hasLoadErrors: false,
}; };
this._treeItemById.set(rootFolder, this.rootItem);
const visitSuite = (projectName: string, parentSuite: reporterTypes.Suite, parentGroup: GroupItem) => { const visitSuite = (project: reporterTypes.FullProject, parentSuite: reporterTypes.Suite, parentGroup: GroupItem) => {
for (const suite of parentSuite.suites) { for (const suite of parentSuite.suites) {
const title = suite.title || '<anonymous>'; const title = suite.title || '<anonymous>';
let group = parentGroup.children.find(item => item.kind === 'group' && item.title === title) as GroupItem | undefined; let group = parentGroup.children.find(item => item.kind === 'group' && item.title === title) as GroupItem | undefined;
@ -87,9 +91,9 @@ export class TestTree {
status: 'none', status: 'none',
hasLoadErrors: false, hasLoadErrors: false,
}; };
parentGroup.children.push(group); this._addChild(parentGroup, group);
} }
visitSuite(projectName, suite, group); visitSuite(project, suite, group);
} }
for (const test of parentSuite.tests) { for (const test of parentSuite.tests) {
@ -106,8 +110,10 @@ export class TestTree {
location: test.location, location: test.location,
duration: 0, duration: 0,
status: 'none', status: 'none',
project: undefined,
test: undefined,
}; };
parentGroup.children.push(testCaseItem); this._addChild(parentGroup, testCaseItem);
} }
const result = test.results[0]; const result = test.results[0];
@ -126,40 +132,47 @@ export class TestTree {
status = 'passed'; status = 'passed';
testCaseItem.tests.push(test); testCaseItem.tests.push(test);
testCaseItem.children.push({ const testItem: TestItem = {
kind: 'test', kind: 'test',
id: test.id, id: test.id,
title: projectName, title: project.name,
location: test.location!, location: test.location!,
test, test,
parent: testCaseItem, parent: testCaseItem,
children: [], children: [],
status, status,
duration: test.results.length ? Math.max(0, test.results[0].duration) : 0, duration: test.results.length ? Math.max(0, test.results[0].duration) : 0,
project: projectName, project,
}); };
this._addChild(testCaseItem, testItem);
this._treeItemByTestId.set(test.id, testItem);
testCaseItem.duration = (testCaseItem.children as TestItem[]).reduce((a, b) => a + b.duration, 0); testCaseItem.duration = (testCaseItem.children as TestItem[]).reduce((a, b) => a + b.duration, 0);
} }
}; };
const fileMap = new Map<string, GroupItem>();
for (const projectSuite of rootSuite?.suites || []) { for (const projectSuite of rootSuite?.suites || []) {
if (filterProjects && !projectFilters.get(projectSuite.title)) if (filterProjects && !projectFilters.get(projectSuite.title))
continue; continue;
for (const fileSuite of projectSuite.suites) { for (const fileSuite of projectSuite.suites) {
const fileItem = this._fileItem(fileSuite.location!.file.split(pathSeparator), true, fileMap); const fileItem = this._fileItem(fileSuite.location!.file.split(pathSeparator), true);
visitSuite(projectSuite.title, fileSuite, fileItem); visitSuite(projectSuite.project()!, fileSuite, fileItem);
} }
} }
for (const loadError of loadErrors) { for (const loadError of loadErrors) {
if (!loadError.location) if (!loadError.location)
continue; continue;
const fileItem = this._fileItem(loadError.location.file.split(pathSeparator), true, fileMap); const fileItem = this._fileItem(loadError.location.file.split(pathSeparator), true);
fileItem.hasLoadErrors = true; fileItem.hasLoadErrors = true;
} }
} }
private _addChild(parent: TreeItem, child: TreeItem) {
parent.children.push(child);
child.parent = parent;
this._treeItemById.set(child.id, child);
}
filterTree(filterText: string, statusFilters: Map<string, boolean>, runningTestIds: Set<string> | undefined) { filterTree(filterText: string, statusFilters: Map<string, boolean>, runningTestIds: Set<string> | undefined) {
const tokens = filterText.trim().toLowerCase().split(' '); const tokens = filterText.trim().toLowerCase().split(' ');
const filtersStatuses = [...statusFilters.values()].some(Boolean); const filtersStatuses = [...statusFilters.values()].some(Boolean);
@ -192,14 +205,14 @@ export class TestTree {
visit(this.rootItem); visit(this.rootItem);
} }
private _fileItem(filePath: string[], isFile: boolean, fileItems: Map<string, GroupItem>): GroupItem { private _fileItem(filePath: string[], isFile: boolean): GroupItem {
if (filePath.length === 0) if (filePath.length === 0)
return this.rootItem; return this.rootItem;
const fileName = filePath.join(pathSeparator); const fileName = filePath.join(this.pathSeparator);
const existingFileItem = fileItems.get(fileName); const existingFileItem = this._treeItemById.get(fileName);
if (existingFileItem) if (existingFileItem)
return existingFileItem; return existingFileItem as GroupItem;
const parentFileItem = this._fileItem(filePath.slice(0, filePath.length - 1), false, fileItems); const parentFileItem = this._fileItem(filePath.slice(0, filePath.length - 1), false);
const fileItem: GroupItem = { const fileItem: GroupItem = {
kind: 'group', kind: 'group',
subKind: isFile ? 'file' : 'folder', subKind: isFile ? 'file' : 'folder',
@ -212,8 +225,7 @@ export class TestTree {
status: 'none', status: 'none',
hasLoadErrors: false, hasLoadErrors: false,
}; };
parentFileItem.children.push(fileItem); this._addChild(parentFileItem, fileItem);
fileItems.set(fileName, fileItem);
return fileItem; return fileItem;
} }
@ -221,12 +233,16 @@ export class TestTree {
sortAndPropagateStatus(this.rootItem); sortAndPropagateStatus(this.rootItem);
} }
hideOnlyTests() { flattenForSingleProject() {
const visit = (treeItem: TreeItem) => { const visit = (treeItem: TreeItem) => {
if (treeItem.kind === 'case' && treeItem.children.length === 1) if (treeItem.kind === 'case' && treeItem.children.length === 1) {
treeItem.project = treeItem.children[0].project;
treeItem.test = treeItem.children[0].test;
treeItem.children = []; treeItem.children = [];
else this._treeItemByTestId.set(treeItem.test.id, treeItem);
} else {
treeItem.children.forEach(visit); treeItem.children.forEach(visit);
}
}; };
visit(this.rootItem); visit(this.rootItem);
} }
@ -239,16 +255,41 @@ export class TestTree {
this.rootItem = shortRoot; this.rootItem = shortRoot;
} }
indexTree() { testIds(): Set<string> {
const result = new Set<string>();
const visit = (treeItem: TreeItem) => { const visit = (treeItem: TreeItem) => {
if (treeItem.kind === 'group' && treeItem.location.file)
this.fileNames.add(treeItem.location.file);
if (treeItem.kind === 'case') if (treeItem.kind === 'case')
treeItem.tests.forEach(t => this.visibleTestIds.add(t.id)); treeItem.tests.forEach(t => result.add(t.id));
treeItem.children.forEach(visit); treeItem.children.forEach(visit);
this.treeItemMap.set(treeItem.id, treeItem);
}; };
visit(this.rootItem); visit(this.rootItem);
return result;
}
fileNames(): string[] {
const result = new Set<string>();
const visit = (treeItem: TreeItem) => {
if (treeItem.kind === 'group' && treeItem.subKind === 'file')
result.add(treeItem.id);
else
treeItem.children.forEach(visit);
};
visit(this.rootItem);
return [...result];
}
flatTreeItems(): TreeItem[] {
const result: TreeItem[] = [];
const visit = (treeItem: TreeItem) => {
result.push(treeItem);
treeItem.children.forEach(visit);
};
visit(this.rootItem);
return result;
}
treeItemById(id: string): TreeItem | undefined {
return this._treeItemById.get(id);
} }
collectTestIds(treeItem?: TreeItem): Set<string> { collectTestIds(treeItem?: TreeItem): Set<string> {
@ -312,5 +353,4 @@ export function sortAndPropagateStatus(treeItem: TreeItem) {
treeItem.status = 'passed'; treeItem.status = 'passed';
} }
export const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/';
export const statusEx = Symbol('statusEx'); export const statusEx = Symbol('statusEx');

View file

@ -16,7 +16,7 @@
*/ */
import type { FullConfig, FullProject, TestStatus, Metadata } from './test'; import type { FullConfig, FullProject, TestStatus, Metadata } from './test';
export type { FullConfig, TestStatus } from './test'; export type { FullConfig, TestStatus, FullProject } from './test';
/** /**
* `Suite` is a group of tests. All tests in Playwright Test form the following hierarchy: * `Suite` is a group of tests. All tests in Playwright Test form the following hierarchy:

View file

@ -15,7 +15,7 @@
*/ */
import { TeleReporterReceiver } from '@testIsomorphic/teleReceiver'; import { TeleReporterReceiver } from '@testIsomorphic/teleReceiver';
import { pathSeparator, statusEx } from '@testIsomorphic/testTree'; import { statusEx } from '@testIsomorphic/testTree';
import type { ReporterV2 } from 'playwright/src/reporters/reporterV2'; import type { ReporterV2 } from 'playwright/src/reporters/reporterV2';
import type * as reporterTypes from 'playwright/types/testReporter'; import type * as reporterTypes from 'playwright/types/testReporter';
@ -28,7 +28,8 @@ export type Progress = {
export type TeleSuiteUpdaterOptions = { export type TeleSuiteUpdaterOptions = {
onUpdate: (source: TeleSuiteUpdater, force?: boolean) => void, onUpdate: (source: TeleSuiteUpdater, force?: boolean) => void,
onError?: (error: reporterTypes.TestError) => void onError?: (error: reporterTypes.TestError) => void;
pathSeparator: string;
}; };
export class TeleSuiteUpdater { export class TeleSuiteUpdater {
@ -51,7 +52,7 @@ export class TeleSuiteUpdater {
this._receiver = new TeleReporterReceiver(this._createReporter(), { this._receiver = new TeleReporterReceiver(this._createReporter(), {
mergeProjects: true, mergeProjects: true,
mergeTestCases: true, mergeTestCases: true,
resolvePath: (rootDir, relativePath) => rootDir + pathSeparator + relativePath, resolvePath: (rootDir, relativePath) => rootDir + options.pathSeparator + relativePath,
clearPreviousResultsWhenTestBegins: true, clearPreviousResultsWhenTestBegins: true,
}); });
this._options = options; this._options = options;
@ -75,7 +76,7 @@ export class TeleSuiteUpdater {
}, { }, {
mergeProjects: true, mergeProjects: true,
mergeTestCases: false, mergeTestCases: false,
resolvePath: (rootDir, relativePath) => rootDir + pathSeparator + relativePath, resolvePath: (rootDir, relativePath) => rootDir + this._options.pathSeparator + relativePath,
}); });
}, },

View file

@ -379,13 +379,12 @@ const TestList: React.FC<{
// Build the test tree. // Build the test tree.
const { testTree } = React.useMemo(() => { const { testTree } = React.useMemo(() => {
const testTree = new TestTree(testModel.rootSuite, testModel.loadErrors, projectFilters); const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, pathSeparator);
testTree.filterTree(filterText, statusFilters, runningState?.testIds); testTree.filterTree(filterText, statusFilters, runningState?.testIds);
testTree.sortAndPropagateStatus(); testTree.sortAndPropagateStatus();
testTree.shortenRoot(); testTree.shortenRoot();
testTree.hideOnlyTests(); testTree.flattenForSingleProject();
testTree.indexTree(); setVisibleTestIds(testTree.testIds());
setVisibleTestIds(testTree.visibleTestIds);
return { testTree }; return { testTree };
}, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState]); }, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState]);
@ -394,8 +393,8 @@ const TestList: React.FC<{
// If collapse was requested, clear the expanded items and return w/o selected item. // If collapse was requested, clear the expanded items and return w/o selected item.
if (collapseAllCount !== requestedCollapseAllCount) { if (collapseAllCount !== requestedCollapseAllCount) {
treeState.expandedItems.clear(); treeState.expandedItems.clear();
for (const item of testTree.treeItemMap.keys()) for (const item of testTree.flatTreeItems())
treeState.expandedItems.set(item, false); treeState.expandedItems.set(item.id, false);
setCollapseAllCount(requestedCollapseAllCount); setCollapseAllCount(requestedCollapseAllCount);
setSelectedTreeItemId(undefined); setSelectedTreeItemId(undefined);
setTreeState({ ...treeState }); setTreeState({ ...treeState });
@ -424,7 +423,7 @@ const TestList: React.FC<{
// Compute selected item. // Compute selected item.
const { selectedTreeItem } = React.useMemo(() => { const { selectedTreeItem } = React.useMemo(() => {
const selectedTreeItem = selectedTreeItemId ? testTree.treeItemMap.get(selectedTreeItemId) : undefined; const selectedTreeItem = selectedTreeItemId ? testTree.treeItemById(selectedTreeItemId) : undefined;
let testFile: SourceLocation | undefined; let testFile: SourceLocation | undefined;
if (selectedTreeItem) { if (selectedTreeItem) {
testFile = { testFile = {
@ -450,11 +449,11 @@ const TestList: React.FC<{
if (isLoading) if (isLoading)
return; return;
if (watchAll) { if (watchAll) {
sendMessageNoReply('watch', { fileNames: [...testTree.fileNames] }); sendMessageNoReply('watch', { fileNames: testTree.fileNames() });
} else { } else {
const fileNames = new Set<string>(); const fileNames = new Set<string>();
for (const itemId of watchedTreeIds.value) { for (const itemId of watchedTreeIds.value) {
const treeItem = testTree.treeItemMap.get(itemId); const treeItem = testTree.treeItemById(itemId);
const fileName = treeItem?.location.file; const fileName = treeItem?.location.file;
if (fileName) if (fileName)
fileNames.add(fileName); fileNames.add(fileName);
@ -482,7 +481,7 @@ const TestList: React.FC<{
visit(testTree.rootItem); visit(testTree.rootItem);
} else { } else {
for (const treeId of watchedTreeIds.value) { for (const treeId of watchedTreeIds.value) {
const treeItem = testTree.treeItemMap.get(treeId); const treeItem = testTree.treeItemById(treeId);
const fileName = treeItem?.location.file; const fileName = treeItem?.location.file;
if (fileName && set.has(fileName)) if (fileName && set.has(fileName))
testIds.push(...testTree.collectTestIds(treeItem)); testIds.push(...testTree.collectTestIds(treeItem));
@ -624,6 +623,7 @@ const refreshRootSuite = (): Promise<void> => {
onError: error => { onError: error => {
xtermDataSource.write((error.stack || error.value || '') + '\n'); xtermDataSource.write((error.stack || error.value || '') + '\n');
}, },
pathSeparator,
}); });
return sendMessage('list', {}); return sendMessage('list', {});
}; };
@ -681,3 +681,5 @@ async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
const contextEntries = await response.json() as ContextEntry[]; const contextEntries = await response.json() as ContextEntry[];
return new MultiTraceModel(contextEntries); return new MultiTraceModel(contextEntries);
} }
export const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/';

View file

@ -15,7 +15,7 @@
*/ */
import type { FullConfig, FullProject, TestStatus, Metadata } from './test'; import type { FullConfig, FullProject, TestStatus, Metadata } from './test';
export type { FullConfig, TestStatus } from './test'; export type { FullConfig, TestStatus, FullProject } from './test';
export interface Suite { export interface Suite {
project(): FullProject | undefined; project(): FullProject | undefined;