mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-12 00:52:05 +03:00
chore: extract the tele test tree (#29824)
This commit is contained in:
parent
502d21e96a
commit
8bf8091cb1
@ -126,6 +126,7 @@ type TeleReporterReceiverOptions = {
|
||||
mergeTestCases: boolean;
|
||||
resolvePath: (rootDir: string, relativePath: string) => string;
|
||||
configOverrides?: Pick<reporterTypes.FullConfig, 'configFile' | 'quiet' | 'reportSlowTests' | 'reporter'>;
|
||||
clearPreviousResultsWhenTestBegins?: boolean;
|
||||
};
|
||||
|
||||
export class TeleReporterReceiver {
|
||||
@ -135,7 +136,6 @@ export class TeleReporterReceiver {
|
||||
private _tests = new Map<string, TeleTestCase>();
|
||||
private _rootDir!: string;
|
||||
private _listOnly = false;
|
||||
private _clearPreviousResultsWhenTestBegins: boolean = false;
|
||||
private _config!: reporterTypes.FullConfig;
|
||||
|
||||
constructor(reporter: Partial<ReporterV2>, options: TeleReporterReceiverOptions) {
|
||||
@ -189,10 +189,6 @@ export class TeleReporterReceiver {
|
||||
return this._onExit();
|
||||
}
|
||||
|
||||
_setClearPreviousResultsWhenTestBegins() {
|
||||
this._clearPreviousResultsWhenTestBegins = true;
|
||||
}
|
||||
|
||||
private _onConfigure(config: JsonConfig) {
|
||||
this._rootDir = config.rootDir;
|
||||
this._config = this._parseConfig(config);
|
||||
@ -234,7 +230,7 @@ export class TeleReporterReceiver {
|
||||
|
||||
private _onTestBegin(testId: string, payload: JsonTestResultStart) {
|
||||
const test = this._tests.get(testId)!;
|
||||
if (this._clearPreviousResultsWhenTestBegins)
|
||||
if (this._options.clearPreviousResultsWhenTestBegins)
|
||||
test._clearResults();
|
||||
const testResult = test._createTestResult(payload.id);
|
||||
testResult.retry = payload.retry;
|
||||
|
316
packages/playwright/src/isomorphic/testTree.ts
Normal file
316
packages/playwright/src/isomorphic/testTree.ts
Normal file
@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type TestItemStatus = 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped';
|
||||
import type * as reporterTypes from '../../types/testReporter';
|
||||
|
||||
export type TreeItemBase = {
|
||||
kind: 'root' | 'group' | 'case' | 'test',
|
||||
id: string;
|
||||
title: string;
|
||||
location: reporterTypes.Location,
|
||||
duration: number;
|
||||
parent: TreeItem | undefined;
|
||||
children: TreeItem[];
|
||||
status: TestItemStatus;
|
||||
};
|
||||
|
||||
export type GroupItem = TreeItemBase & {
|
||||
kind: 'group';
|
||||
subKind: 'folder' | 'file' | 'describe';
|
||||
hasLoadErrors: boolean;
|
||||
children: (TestCaseItem | GroupItem)[];
|
||||
};
|
||||
|
||||
export type TestCaseItem = TreeItemBase & {
|
||||
kind: 'case',
|
||||
tests: reporterTypes.TestCase[];
|
||||
children: TestItem[];
|
||||
};
|
||||
|
||||
export type TestItem = TreeItemBase & {
|
||||
kind: 'test',
|
||||
test: reporterTypes.TestCase;
|
||||
project: string;
|
||||
};
|
||||
|
||||
export type TreeItem = GroupItem | TestCaseItem | TestItem;
|
||||
|
||||
export class TestTree {
|
||||
rootItem: GroupItem;
|
||||
readonly treeItemMap = new Map<string, TreeItem>();
|
||||
readonly visibleTestIds = new Set<string>();
|
||||
readonly fileNames = new Set<string>();
|
||||
|
||||
constructor(rootSuite: reporterTypes.Suite | undefined, loadErrors: reporterTypes.TestError[], projectFilters: Map<string, boolean>) {
|
||||
const filterProjects = [...projectFilters.values()].some(Boolean);
|
||||
this.rootItem = {
|
||||
kind: 'group',
|
||||
subKind: 'folder',
|
||||
id: 'root',
|
||||
title: '',
|
||||
location: { file: '', line: 0, column: 0 },
|
||||
duration: 0,
|
||||
parent: undefined,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
|
||||
const visitSuite = (projectName: string, parentSuite: reporterTypes.Suite, parentGroup: GroupItem) => {
|
||||
for (const suite of parentSuite.suites) {
|
||||
const title = suite.title || '<anonymous>';
|
||||
let group = parentGroup.children.find(item => item.kind === 'group' && item.title === title) as GroupItem | undefined;
|
||||
if (!group) {
|
||||
group = {
|
||||
kind: 'group',
|
||||
subKind: 'describe',
|
||||
id: 'suite:' + parentSuite.titlePath().join('\x1e') + '\x1e' + title, // account for anonymous suites
|
||||
title,
|
||||
location: suite.location!,
|
||||
duration: 0,
|
||||
parent: parentGroup,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
parentGroup.children.push(group);
|
||||
}
|
||||
visitSuite(projectName, suite, group);
|
||||
}
|
||||
|
||||
for (const test of parentSuite.tests) {
|
||||
const title = test.title;
|
||||
let testCaseItem = parentGroup.children.find(t => t.kind !== 'group' && t.title === title) as TestCaseItem;
|
||||
if (!testCaseItem) {
|
||||
testCaseItem = {
|
||||
kind: 'case',
|
||||
id: 'test:' + test.titlePath().join('\x1e'),
|
||||
title,
|
||||
parent: parentGroup,
|
||||
children: [],
|
||||
tests: [],
|
||||
location: test.location,
|
||||
duration: 0,
|
||||
status: 'none',
|
||||
};
|
||||
parentGroup.children.push(testCaseItem);
|
||||
}
|
||||
|
||||
const result = test.results[0];
|
||||
let status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none';
|
||||
if ((result as any)?.[statusEx] === 'scheduled')
|
||||
status = 'scheduled';
|
||||
else if ((result as any)?.[statusEx] === 'running')
|
||||
status = 'running';
|
||||
else if (result?.status === 'skipped')
|
||||
status = 'skipped';
|
||||
else if (result?.status === 'interrupted')
|
||||
status = 'none';
|
||||
else if (result && test.outcome() !== 'expected')
|
||||
status = 'failed';
|
||||
else if (result && test.outcome() === 'expected')
|
||||
status = 'passed';
|
||||
|
||||
testCaseItem.tests.push(test);
|
||||
testCaseItem.children.push({
|
||||
kind: 'test',
|
||||
id: test.id,
|
||||
title: projectName,
|
||||
location: test.location!,
|
||||
test,
|
||||
parent: testCaseItem,
|
||||
children: [],
|
||||
status,
|
||||
duration: test.results.length ? Math.max(0, test.results[0].duration) : 0,
|
||||
project: projectName,
|
||||
});
|
||||
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 || []) {
|
||||
if (filterProjects && !projectFilters.get(projectSuite.title))
|
||||
continue;
|
||||
for (const fileSuite of projectSuite.suites) {
|
||||
const fileItem = this._fileItem(fileSuite.location!.file.split(pathSeparator), true, fileMap);
|
||||
visitSuite(projectSuite.title, fileSuite, fileItem);
|
||||
}
|
||||
}
|
||||
|
||||
for (const loadError of loadErrors) {
|
||||
if (!loadError.location)
|
||||
continue;
|
||||
const fileItem = this._fileItem(loadError.location.file.split(pathSeparator), true, fileMap);
|
||||
fileItem.hasLoadErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
filterTree(filterText: string, statusFilters: Map<string, boolean>, runningTestIds: Set<string> | undefined) {
|
||||
const tokens = filterText.trim().toLowerCase().split(' ');
|
||||
const filtersStatuses = [...statusFilters.values()].some(Boolean);
|
||||
|
||||
const filter = (testCase: TestCaseItem) => {
|
||||
const titleWithTags = [...testCase.tests[0].titlePath(), ...testCase.tests[0].tags].join(' ').toLowerCase();
|
||||
if (!tokens.every(token => titleWithTags.includes(token)) && !testCase.tests.some(t => runningTestIds?.has(t.id)))
|
||||
return false;
|
||||
testCase.children = (testCase.children as TestItem[]).filter(test => {
|
||||
return !filtersStatuses || runningTestIds?.has(test.test.id) || statusFilters.get(test.status);
|
||||
});
|
||||
testCase.tests = (testCase.children as TestItem[]).map(c => c.test);
|
||||
return !!testCase.children.length;
|
||||
};
|
||||
|
||||
const visit = (treeItem: GroupItem) => {
|
||||
const newChildren: (GroupItem | TestCaseItem)[] = [];
|
||||
for (const child of treeItem.children) {
|
||||
if (child.kind === 'case') {
|
||||
if (filter(child))
|
||||
newChildren.push(child);
|
||||
} else {
|
||||
visit(child);
|
||||
if (child.children.length || child.hasLoadErrors)
|
||||
newChildren.push(child);
|
||||
}
|
||||
}
|
||||
treeItem.children = newChildren;
|
||||
};
|
||||
visit(this.rootItem);
|
||||
}
|
||||
|
||||
private _fileItem(filePath: string[], isFile: boolean, fileItems: Map<string, GroupItem>): GroupItem {
|
||||
if (filePath.length === 0)
|
||||
return this.rootItem;
|
||||
const fileName = filePath.join(pathSeparator);
|
||||
const existingFileItem = fileItems.get(fileName);
|
||||
if (existingFileItem)
|
||||
return existingFileItem;
|
||||
const parentFileItem = this._fileItem(filePath.slice(0, filePath.length - 1), false, fileItems);
|
||||
const fileItem: GroupItem = {
|
||||
kind: 'group',
|
||||
subKind: isFile ? 'file' : 'folder',
|
||||
id: fileName,
|
||||
title: filePath[filePath.length - 1],
|
||||
location: { file: fileName, line: 0, column: 0 },
|
||||
duration: 0,
|
||||
parent: parentFileItem,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
parentFileItem.children.push(fileItem);
|
||||
fileItems.set(fileName, fileItem);
|
||||
return fileItem;
|
||||
}
|
||||
|
||||
sortAndPropagateStatus() {
|
||||
sortAndPropagateStatus(this.rootItem);
|
||||
}
|
||||
|
||||
hideOnlyTests() {
|
||||
const visit = (treeItem: TreeItem) => {
|
||||
if (treeItem.kind === 'case' && treeItem.children.length === 1)
|
||||
treeItem.children = [];
|
||||
else
|
||||
treeItem.children.forEach(visit);
|
||||
};
|
||||
visit(this.rootItem);
|
||||
}
|
||||
|
||||
shortenRoot() {
|
||||
let shortRoot = this.rootItem;
|
||||
while (shortRoot.children.length === 1 && shortRoot.children[0].kind === 'group' && shortRoot.children[0].subKind === 'folder')
|
||||
shortRoot = shortRoot.children[0];
|
||||
shortRoot.location = this.rootItem.location;
|
||||
this.rootItem = shortRoot;
|
||||
}
|
||||
|
||||
indexTree() {
|
||||
const visit = (treeItem: TreeItem) => {
|
||||
if (treeItem.kind === 'group' && treeItem.location.file)
|
||||
this.fileNames.add(treeItem.location.file);
|
||||
if (treeItem.kind === 'case')
|
||||
treeItem.tests.forEach(t => this.visibleTestIds.add(t.id));
|
||||
treeItem.children.forEach(visit);
|
||||
this.treeItemMap.set(treeItem.id, treeItem);
|
||||
};
|
||||
visit(this.rootItem);
|
||||
}
|
||||
|
||||
collectTestIds(treeItem?: TreeItem): Set<string> {
|
||||
const testIds = new Set<string>();
|
||||
if (!treeItem)
|
||||
return testIds;
|
||||
|
||||
const visit = (treeItem: TreeItem) => {
|
||||
if (treeItem.kind === 'case')
|
||||
treeItem.tests.map(t => t.id).forEach(id => testIds.add(id));
|
||||
else if (treeItem.kind === 'test')
|
||||
testIds.add(treeItem.id);
|
||||
else
|
||||
treeItem.children?.forEach(visit);
|
||||
};
|
||||
visit(treeItem);
|
||||
return testIds;
|
||||
}
|
||||
|
||||
locationToOpen(treeItem?: TreeItem) {
|
||||
if (!treeItem)
|
||||
return;
|
||||
return treeItem.location.file + ':' + treeItem.location.line;
|
||||
}
|
||||
}
|
||||
|
||||
export function sortAndPropagateStatus(treeItem: TreeItem) {
|
||||
for (const child of treeItem.children)
|
||||
sortAndPropagateStatus(child);
|
||||
|
||||
if (treeItem.kind === 'group') {
|
||||
treeItem.children.sort((a, b) => {
|
||||
const fc = a.location.file.localeCompare(b.location.file);
|
||||
return fc || a.location.line - b.location.line;
|
||||
});
|
||||
}
|
||||
|
||||
let allPassed = treeItem.children.length > 0;
|
||||
let allSkipped = treeItem.children.length > 0;
|
||||
let hasFailed = false;
|
||||
let hasRunning = false;
|
||||
let hasScheduled = false;
|
||||
|
||||
for (const child of treeItem.children) {
|
||||
allSkipped = allSkipped && child.status === 'skipped';
|
||||
allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped');
|
||||
hasFailed = hasFailed || child.status === 'failed';
|
||||
hasRunning = hasRunning || child.status === 'running';
|
||||
hasScheduled = hasScheduled || child.status === 'scheduled';
|
||||
}
|
||||
|
||||
if (hasRunning)
|
||||
treeItem.status = 'running';
|
||||
else if (hasScheduled)
|
||||
treeItem.status = 'scheduled';
|
||||
else if (hasFailed)
|
||||
treeItem.status = 'failed';
|
||||
else if (allSkipped)
|
||||
treeItem.status = 'skipped';
|
||||
else if (allPassed)
|
||||
treeItem.status = 'passed';
|
||||
}
|
||||
|
||||
export const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/';
|
||||
export const statusEx = Symbol('statusEx');
|
137
packages/trace-viewer/src/ui/teleSuiteUpdater.ts
Normal file
137
packages/trace-viewer/src/ui/teleSuiteUpdater.ts
Normal file
@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TeleReporterReceiver } from '@testIsomorphic/teleReceiver';
|
||||
import { pathSeparator, statusEx } from '@testIsomorphic/testTree';
|
||||
import type { ReporterV2 } from 'playwright/src/reporters/reporterV2';
|
||||
import type * as reporterTypes from 'playwright/types/testReporter';
|
||||
|
||||
export type Progress = {
|
||||
total: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
};
|
||||
|
||||
export type TeleSuiteUpdaterOptions = {
|
||||
onUpdate: (source: TeleSuiteUpdater, force?: boolean) => void,
|
||||
onError?: (error: reporterTypes.TestError) => void
|
||||
};
|
||||
|
||||
export class TeleSuiteUpdater {
|
||||
rootSuite: reporterTypes.Suite | undefined;
|
||||
config: reporterTypes.FullConfig | undefined;
|
||||
readonly loadErrors: reporterTypes.TestError[] = [];
|
||||
readonly progress: Progress = {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
|
||||
private _receiver: TeleReporterReceiver;
|
||||
private _lastRunReceiver: TeleReporterReceiver | undefined;
|
||||
private _lastRunTestCount = 0;
|
||||
private _options: TeleSuiteUpdaterOptions;
|
||||
|
||||
constructor(options: TeleSuiteUpdaterOptions) {
|
||||
this._receiver = new TeleReporterReceiver(this._createReporter(), {
|
||||
mergeProjects: true,
|
||||
mergeTestCases: true,
|
||||
resolvePath: (rootDir, relativePath) => rootDir + pathSeparator + relativePath,
|
||||
clearPreviousResultsWhenTestBegins: true,
|
||||
});
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
private _createReporter(): ReporterV2 {
|
||||
return {
|
||||
version: () => 'v2',
|
||||
|
||||
onConfigure: (c: reporterTypes.FullConfig) => {
|
||||
this.config = c;
|
||||
// TeleReportReceiver is merging everything into a single suite, so when we
|
||||
// run one test, we still get many tests via rootSuite.allTests().length.
|
||||
// To work around that, have a dedicated per-run receiver that will only have
|
||||
// suite for a single test run, and hence will have correct total.
|
||||
this._lastRunReceiver = new TeleReporterReceiver({
|
||||
onBegin: (suite: reporterTypes.Suite) => {
|
||||
this._lastRunTestCount = suite.allTests().length;
|
||||
this._lastRunReceiver = undefined;
|
||||
}
|
||||
}, {
|
||||
mergeProjects: true,
|
||||
mergeTestCases: false,
|
||||
resolvePath: (rootDir, relativePath) => rootDir + pathSeparator + relativePath,
|
||||
});
|
||||
},
|
||||
|
||||
onBegin: (suite: reporterTypes.Suite) => {
|
||||
if (!this.rootSuite)
|
||||
this.rootSuite = suite;
|
||||
this.progress.total = this._lastRunTestCount;
|
||||
this.progress.passed = 0;
|
||||
this.progress.failed = 0;
|
||||
this.progress.skipped = 0;
|
||||
this._options.onUpdate(this, true);
|
||||
},
|
||||
|
||||
onEnd: () => {
|
||||
this._options.onUpdate(this, true);
|
||||
},
|
||||
|
||||
onTestBegin: (test: reporterTypes.TestCase, testResult: reporterTypes.TestResult) => {
|
||||
(testResult as any)[statusEx] = 'running';
|
||||
this._options.onUpdate(this);
|
||||
},
|
||||
|
||||
onTestEnd: (test: reporterTypes.TestCase, testResult: reporterTypes.TestResult) => {
|
||||
if (test.outcome() === 'skipped')
|
||||
++this.progress.skipped;
|
||||
else if (test.outcome() === 'unexpected')
|
||||
++this.progress.failed;
|
||||
else
|
||||
++this.progress.passed;
|
||||
(testResult as any)[statusEx] = testResult.status;
|
||||
this._options.onUpdate(this);
|
||||
},
|
||||
|
||||
onError: (error: reporterTypes.TestError) => {
|
||||
this.loadErrors.push(error);
|
||||
this._options.onError?.(error);
|
||||
this._options.onUpdate(this);
|
||||
},
|
||||
|
||||
printsToStdio: () => {
|
||||
return false;
|
||||
},
|
||||
|
||||
onStdOut: () => {},
|
||||
onStdErr: () => {},
|
||||
onExit: () => {},
|
||||
onStepBegin: () => {},
|
||||
onStepEnd: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
dispatch(mode: 'test' | 'list', message: any) {
|
||||
// The order of receiver dispatches matters here, we want to assign `lastRunTestCount`
|
||||
// before we use it.
|
||||
if (mode === 'test')
|
||||
this._lastRunReceiver?.dispatch('test', message)?.catch(() => {});
|
||||
this._receiver.dispatch(mode, message)?.catch(() => {});
|
||||
}
|
||||
}
|
@ -20,9 +20,11 @@ import '@web/common.css';
|
||||
import React from 'react';
|
||||
import { TreeView } from '@web/components/treeView';
|
||||
import type { TreeState } from '@web/components/treeView';
|
||||
import { baseFullConfig, TeleReporterReceiver, TeleSuite } from '@testIsomorphic/teleReceiver';
|
||||
import { baseFullConfig, TeleSuite } from '@testIsomorphic/teleReceiver';
|
||||
import { TeleSuiteUpdater } from './teleSuiteUpdater';
|
||||
import type { Progress } from './teleSuiteUpdater';
|
||||
import type { TeleTestCase } from '@testIsomorphic/teleReceiver';
|
||||
import type { FullConfig, Suite, TestCase, Location, TestError, TestResult } from 'playwright/types/testReporter';
|
||||
import type * as reporterTypes from 'playwright/types/testReporter';
|
||||
import { SplitView } from '@web/components/splitView';
|
||||
import { idForAction, MultiTraceModel } from './modelUtil';
|
||||
import type { SourceLocation } from './modelUtil';
|
||||
@ -38,10 +40,11 @@ import { artifactsFolderName } from '@testIsomorphic/folders';
|
||||
import { msToString, settings, useSetting } from '@web/uiUtils';
|
||||
import type { ActionTraceEvent } from '@trace/trace';
|
||||
import { connect } from './wsPort';
|
||||
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
||||
import type { TreeItem } from '@testIsomorphic/testTree';
|
||||
import { testStatusIcon } from './testUtils';
|
||||
import type { UITestStatus } from './testUtils';
|
||||
|
||||
let updateRootSuite: (config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress | undefined) => void = () => {};
|
||||
let updateRootSuite: (config: reporterTypes.FullConfig, rootSuite: reporterTypes.Suite, loadErrors: reporterTypes.TestError[], progress: Progress | undefined) => void = () => {};
|
||||
let runWatchedTests = (fileNames: string[]) => {};
|
||||
let xtermSize = { cols: 80, rows: 24 };
|
||||
|
||||
@ -58,9 +61,9 @@ const xtermDataSource: XtermDataSource = {
|
||||
};
|
||||
|
||||
type TestModel = {
|
||||
config: FullConfig | undefined;
|
||||
rootSuite: Suite | undefined;
|
||||
loadErrors: TestError[];
|
||||
config: reporterTypes.FullConfig | undefined;
|
||||
rootSuite: reporterTypes.Suite | undefined;
|
||||
loadErrors: reporterTypes.TestError[];
|
||||
};
|
||||
|
||||
export const UIModeView: React.FC<{}> = ({
|
||||
@ -76,7 +79,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||
const [projectFilters, setProjectFilters] = React.useState<Map<string, boolean>>(new Map());
|
||||
const [testModel, setTestModel] = React.useState<TestModel>({ config: undefined, rootSuite: undefined, loadErrors: [] });
|
||||
const [progress, setProgress] = React.useState<Progress & { total: number } | undefined>();
|
||||
const [selectedItem, setSelectedItem] = React.useState<{ treeItem?: TreeItem, testFile?: SourceLocation, testCase?: TestCase }>({});
|
||||
const [selectedItem, setSelectedItem] = React.useState<{ treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase }>({});
|
||||
const [visibleTestIds, setVisibleTestIds] = React.useState<Set<string>>(new Set());
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean } | undefined>();
|
||||
@ -94,7 +97,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||
setIsLoading(true);
|
||||
setWatchedTreeIds({ value: new Set() });
|
||||
updateRootSuite(baseFullConfig, new TeleSuite('', 'root'), [], undefined);
|
||||
refreshRootSuite(true).then(async () => {
|
||||
refreshRootSuite().then(async () => {
|
||||
setIsLoading(false);
|
||||
const { hasBrowsers } = await sendMessage('checkBrowsers');
|
||||
setHasBrowsers(hasBrowsers);
|
||||
@ -114,7 +117,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||
});
|
||||
}, [reloadTests]);
|
||||
|
||||
updateRootSuite = React.useCallback((config: FullConfig, rootSuite: Suite, loadErrors: TestError[], newProgress: Progress | undefined) => {
|
||||
updateRootSuite = React.useCallback((config: reporterTypes.FullConfig, rootSuite: reporterTypes.Suite, loadErrors: reporterTypes.TestError[], newProgress: Progress | undefined) => {
|
||||
const selectedProjects = config.configFile ? settings.getObject<string[] | undefined>(config.configFile + ':projects', undefined) : undefined;
|
||||
for (const projectName of projectFilters.keys()) {
|
||||
if (!rootSuite.suites.find(s => s.title === projectName))
|
||||
@ -367,7 +370,7 @@ const TestList: React.FC<{
|
||||
setWatchedTreeIds: (ids: { value: Set<string> }) => void,
|
||||
isLoading?: boolean,
|
||||
setVisibleTestIds: (testIds: Set<string>) => void,
|
||||
onItemSelected: (item: { treeItem?: TreeItem, testCase?: TestCase, testFile?: SourceLocation }) => void,
|
||||
onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void,
|
||||
requestedCollapseAllCount: number,
|
||||
}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, setVisibleTestIds, requestedCollapseAllCount }) => {
|
||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||
@ -375,27 +378,15 @@ const TestList: React.FC<{
|
||||
const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount);
|
||||
|
||||
// Build the test tree.
|
||||
const { rootItem, treeItemMap, fileNames } = React.useMemo(() => {
|
||||
let rootItem = createTree(testModel.rootSuite, testModel.loadErrors, projectFilters);
|
||||
filterTree(rootItem, filterText, statusFilters, runningState?.testIds);
|
||||
sortAndPropagateStatus(rootItem);
|
||||
rootItem = shortenRoot(rootItem);
|
||||
|
||||
hideOnlyTests(rootItem);
|
||||
const treeItemMap = new Map<string, TreeItem>();
|
||||
const visibleTestIds = new Set<string>();
|
||||
const fileNames = new Set<string>();
|
||||
const visit = (treeItem: TreeItem) => {
|
||||
if (treeItem.kind === 'group' && treeItem.location.file)
|
||||
fileNames.add(treeItem.location.file);
|
||||
if (treeItem.kind === 'case')
|
||||
treeItem.tests.forEach(t => visibleTestIds.add(t.id));
|
||||
treeItem.children.forEach(visit);
|
||||
treeItemMap.set(treeItem.id, treeItem);
|
||||
};
|
||||
visit(rootItem);
|
||||
setVisibleTestIds(visibleTestIds);
|
||||
return { rootItem, treeItemMap, fileNames };
|
||||
const { testTree } = React.useMemo(() => {
|
||||
const testTree = new TestTree(testModel.rootSuite, testModel.loadErrors, projectFilters);
|
||||
testTree.filterTree(filterText, statusFilters, runningState?.testIds);
|
||||
testTree.sortAndPropagateStatus();
|
||||
testTree.shortenRoot();
|
||||
testTree.hideOnlyTests();
|
||||
testTree.indexTree();
|
||||
setVisibleTestIds(testTree.visibleTestIds);
|
||||
return { testTree };
|
||||
}, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState]);
|
||||
|
||||
// Look for a first failure within the run batch to select it.
|
||||
@ -403,7 +394,7 @@ const TestList: React.FC<{
|
||||
// If collapse was requested, clear the expanded items and return w/o selected item.
|
||||
if (collapseAllCount !== requestedCollapseAllCount) {
|
||||
treeState.expandedItems.clear();
|
||||
for (const item of treeItemMap.keys())
|
||||
for (const item of testTree.treeItemMap.keys())
|
||||
treeState.expandedItems.set(item, false);
|
||||
setCollapseAllCount(requestedCollapseAllCount);
|
||||
setSelectedTreeItemId(undefined);
|
||||
@ -425,15 +416,15 @@ const TestList: React.FC<{
|
||||
selectedTreeItem = treeItem;
|
||||
}
|
||||
};
|
||||
visit(rootItem);
|
||||
visit(testTree.rootItem);
|
||||
|
||||
if (selectedTreeItem)
|
||||
setSelectedTreeItemId(selectedTreeItem.id);
|
||||
}, [runningState, setSelectedTreeItemId, rootItem, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, treeState, setTreeState, treeItemMap]);
|
||||
}, [runningState, setSelectedTreeItemId, testTree, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, treeState, setTreeState]);
|
||||
|
||||
// Compute selected item.
|
||||
const { selectedTreeItem } = React.useMemo(() => {
|
||||
const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined;
|
||||
const selectedTreeItem = selectedTreeItemId ? testTree.treeItemMap.get(selectedTreeItemId) : undefined;
|
||||
let testFile: SourceLocation | undefined;
|
||||
if (selectedTreeItem) {
|
||||
testFile = {
|
||||
@ -445,36 +436,36 @@ const TestList: React.FC<{
|
||||
}
|
||||
};
|
||||
}
|
||||
let selectedTest: TestCase | undefined;
|
||||
let selectedTest: reporterTypes.TestCase | undefined;
|
||||
if (selectedTreeItem?.kind === 'test')
|
||||
selectedTest = selectedTreeItem.test;
|
||||
else if (selectedTreeItem?.kind === 'case' && selectedTreeItem.tests.length === 1)
|
||||
selectedTest = selectedTreeItem.tests[0];
|
||||
onItemSelected({ treeItem: selectedTreeItem, testCase: selectedTest, testFile });
|
||||
return { selectedTreeItem };
|
||||
}, [onItemSelected, selectedTreeItemId, testModel, treeItemMap]);
|
||||
}, [onItemSelected, selectedTreeItemId, testModel, testTree]);
|
||||
|
||||
// Update watch all.
|
||||
React.useEffect(() => {
|
||||
if (isLoading)
|
||||
return;
|
||||
if (watchAll) {
|
||||
sendMessageNoReply('watch', { fileNames: [...fileNames] });
|
||||
sendMessageNoReply('watch', { fileNames: [...testTree.fileNames] });
|
||||
} else {
|
||||
const fileNames = new Set<string>();
|
||||
for (const itemId of watchedTreeIds.value) {
|
||||
const treeItem = treeItemMap.get(itemId);
|
||||
const treeItem = testTree.treeItemMap.get(itemId);
|
||||
const fileName = treeItem?.location.file;
|
||||
if (fileName)
|
||||
fileNames.add(fileName);
|
||||
}
|
||||
sendMessageNoReply('watch', { fileNames: [...fileNames] });
|
||||
}
|
||||
}, [isLoading, rootItem, fileNames, watchAll, watchedTreeIds, treeItemMap]);
|
||||
}, [isLoading, testTree, watchAll, watchedTreeIds]);
|
||||
|
||||
const runTreeItem = (treeItem: TreeItem) => {
|
||||
setSelectedTreeItemId(treeItem.id);
|
||||
runTests('bounce-if-busy', collectTestIds(treeItem));
|
||||
runTests('bounce-if-busy', testTree.collectTestIds(treeItem));
|
||||
};
|
||||
|
||||
runWatchedTests = (changedTestFiles: string[]) => {
|
||||
@ -484,17 +475,17 @@ const TestList: React.FC<{
|
||||
const visit = (treeItem: TreeItem) => {
|
||||
const fileName = treeItem.location.file;
|
||||
if (fileName && set.has(fileName))
|
||||
testIds.push(...collectTestIds(treeItem));
|
||||
testIds.push(...testTree.collectTestIds(treeItem));
|
||||
if (treeItem.kind === 'group' && treeItem.subKind === 'folder')
|
||||
treeItem.children.forEach(visit);
|
||||
};
|
||||
visit(rootItem);
|
||||
visit(testTree.rootItem);
|
||||
} else {
|
||||
for (const treeId of watchedTreeIds.value) {
|
||||
const treeItem = treeItemMap.get(treeId);
|
||||
const treeItem = testTree.treeItemMap.get(treeId);
|
||||
const fileName = treeItem?.location.file;
|
||||
if (fileName && set.has(fileName))
|
||||
testIds.push(...collectTestIds(treeItem));
|
||||
testIds.push(...testTree.collectTestIds(treeItem));
|
||||
}
|
||||
}
|
||||
runTests('queue-if-busy', new Set(testIds));
|
||||
@ -504,7 +495,7 @@ const TestList: React.FC<{
|
||||
name='tests'
|
||||
treeState={treeState}
|
||||
setTreeState={setTreeState}
|
||||
rootItem={rootItem}
|
||||
rootItem={testTree.rootItem}
|
||||
dataTestId='test-tree'
|
||||
render={treeItem => {
|
||||
return <div className='hbox ui-mode-list-item'>
|
||||
@ -512,7 +503,7 @@ const TestList: React.FC<{
|
||||
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>}
|
||||
<Toolbar noMinHeight={true} noShadow={true}>
|
||||
<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) })} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton>
|
||||
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => sendMessageNoReply('open', { location: testTree.locationToOpen(treeItem) })} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton>
|
||||
{!watchAll && <ToolbarButton icon='eye' title='Watch' onClick={() => {
|
||||
if (watchedTreeIds.value.has(treeItem.id))
|
||||
watchedTreeIds.value.delete(treeItem.id);
|
||||
@ -537,7 +528,7 @@ const TestList: React.FC<{
|
||||
};
|
||||
|
||||
const TraceView: React.FC<{
|
||||
item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: TestCase },
|
||||
item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase },
|
||||
rootDir?: string,
|
||||
}> = ({ item, rootDir }) => {
|
||||
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
||||
@ -608,19 +599,17 @@ const TraceView: React.FC<{
|
||||
status={item.treeItem?.status} />;
|
||||
};
|
||||
|
||||
let receiver: TeleReporterReceiver | undefined;
|
||||
let lastRunReceiver: TeleReporterReceiver | undefined;
|
||||
let lastRunTestCount: number;
|
||||
let teleSuiteUpdater: TeleSuiteUpdater | undefined;
|
||||
|
||||
let throttleTimer: NodeJS.Timeout | undefined;
|
||||
let throttleData: { config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress } | undefined;
|
||||
let throttleData: { config: reporterTypes.FullConfig, rootSuite: reporterTypes.Suite, loadErrors: reporterTypes.TestError[], progress: Progress } | undefined;
|
||||
const throttledAction = () => {
|
||||
clearTimeout(throttleTimer);
|
||||
throttleTimer = undefined;
|
||||
updateRootSuite(throttleData!.config, throttleData!.rootSuite, throttleData!.loadErrors, throttleData!.progress);
|
||||
};
|
||||
|
||||
const throttleUpdateRootSuite = (config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress, immediate = false) => {
|
||||
const throttleUpdateRootSuite = (config: reporterTypes.FullConfig, rootSuite: reporterTypes.Suite, loadErrors: reporterTypes.TestError[], progress: Progress, immediate = false) => {
|
||||
throttleData = { config, rootSuite, loadErrors, progress };
|
||||
if (immediate)
|
||||
throttledAction();
|
||||
@ -628,91 +617,15 @@ const throttleUpdateRootSuite = (config: FullConfig, rootSuite: Suite, loadError
|
||||
throttleTimer = setTimeout(throttledAction, 250);
|
||||
};
|
||||
|
||||
const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
|
||||
if (!eraseResults)
|
||||
return sendMessage('list', {});
|
||||
|
||||
let rootSuite: Suite;
|
||||
const loadErrors: TestError[] = [];
|
||||
const progress: Progress = {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
let config: FullConfig;
|
||||
receiver = new TeleReporterReceiver({
|
||||
version: () => 'v2',
|
||||
|
||||
onConfigure: (c: FullConfig) => {
|
||||
config = c;
|
||||
// TeleReportReceiver is merging everything into a single suite, so when we
|
||||
// run one test, we still get many tests via rootSuite.allTests().length.
|
||||
// To work around that, have a dedicated per-run receiver that will only have
|
||||
// suite for a single test run, and hence will have correct total.
|
||||
lastRunReceiver = new TeleReporterReceiver({
|
||||
onBegin: (suite: Suite) => {
|
||||
lastRunTestCount = suite.allTests().length;
|
||||
lastRunReceiver = undefined;
|
||||
}
|
||||
}, {
|
||||
mergeProjects: true,
|
||||
mergeTestCases: false,
|
||||
resolvePath: (rootDir, relativePath) => rootDir + pathSeparator + relativePath,
|
||||
});
|
||||
const refreshRootSuite = (): Promise<void> => {
|
||||
teleSuiteUpdater = new TeleSuiteUpdater({
|
||||
onUpdate: (source, immediate) => {
|
||||
throttleUpdateRootSuite(source.config!, source.rootSuite || new TeleSuite('', 'root'), source.loadErrors, source.progress, immediate);
|
||||
},
|
||||
|
||||
onBegin: (suite: Suite) => {
|
||||
if (!rootSuite)
|
||||
rootSuite = suite;
|
||||
progress.total = lastRunTestCount;
|
||||
progress.passed = 0;
|
||||
progress.failed = 0;
|
||||
progress.skipped = 0;
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress, true);
|
||||
},
|
||||
|
||||
onEnd: () => {
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress, true);
|
||||
},
|
||||
|
||||
onTestBegin: (test: TestCase, testResult: TestResult) => {
|
||||
(testResult as any)[statusEx] = 'running';
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress);
|
||||
},
|
||||
|
||||
onTestEnd: (test: TestCase, testResult: TestResult) => {
|
||||
if (test.outcome() === 'skipped')
|
||||
++progress.skipped;
|
||||
else if (test.outcome() === 'unexpected')
|
||||
++progress.failed;
|
||||
else
|
||||
++progress.passed;
|
||||
(testResult as any)[statusEx] = testResult.status;
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress);
|
||||
},
|
||||
|
||||
onError: (error: TestError) => {
|
||||
onError: error => {
|
||||
xtermDataSource.write((error.stack || error.value || '') + '\n');
|
||||
loadErrors.push(error);
|
||||
throttleUpdateRootSuite(config, rootSuite ?? new TeleSuite('', 'root'), loadErrors, progress);
|
||||
},
|
||||
|
||||
printsToStdio: () => {
|
||||
return false;
|
||||
},
|
||||
|
||||
onStdOut: () => {},
|
||||
onStdErr: () => {},
|
||||
onExit: () => {},
|
||||
onStepBegin: () => {},
|
||||
onStepEnd: () => {},
|
||||
}, {
|
||||
mergeProjects: true,
|
||||
mergeTestCases: true,
|
||||
resolvePath: (rootDir, relativePath) => rootDir + pathSeparator + relativePath,
|
||||
});
|
||||
receiver._setClearPreviousResultsWhenTestBegins();
|
||||
return sendMessage('list', {});
|
||||
};
|
||||
|
||||
@ -729,7 +642,7 @@ const sendMessageNoReply = (method: string, params?: any) => {
|
||||
|
||||
const dispatchEvent = (method: string, params?: any) => {
|
||||
if (method === 'listChanged') {
|
||||
refreshRootSuite(false).catch(() => {});
|
||||
sendMessage('list', {}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -749,304 +662,19 @@ const dispatchEvent = (method: string, params?: any) => {
|
||||
}
|
||||
|
||||
if (method === 'listReport')
|
||||
receiver?.dispatch('list', params)?.catch(() => {});
|
||||
|
||||
if (method === 'testReport') {
|
||||
// The order of receiver dispatches matters here, we want to assign `lastRunTestCount`
|
||||
// before we use it.
|
||||
lastRunReceiver?.dispatch('test', params)?.catch(() => {});
|
||||
receiver?.dispatch('test', params)?.catch(() => {});
|
||||
}
|
||||
teleSuiteUpdater?.dispatch('list', params);
|
||||
if (method === 'testReport')
|
||||
teleSuiteUpdater?.dispatch('test', params);
|
||||
};
|
||||
|
||||
const outputDirForTestCase = (testCase: TestCase): string | undefined => {
|
||||
for (let suite: Suite | undefined = testCase.parent; suite; suite = suite.parent) {
|
||||
const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefined => {
|
||||
for (let suite: reporterTypes.Suite | undefined = testCase.parent; suite; suite = suite.parent) {
|
||||
if (suite.project())
|
||||
return suite.project()?.outputDir;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const locationToOpen = (treeItem?: TreeItem) => {
|
||||
if (!treeItem)
|
||||
return;
|
||||
return treeItem.location.file + ':' + treeItem.location.line;
|
||||
};
|
||||
|
||||
const collectTestIds = (treeItem?: TreeItem): Set<string> => {
|
||||
const testIds = new Set<string>();
|
||||
if (!treeItem)
|
||||
return testIds;
|
||||
|
||||
const visit = (treeItem: TreeItem) => {
|
||||
if (treeItem.kind === 'case')
|
||||
treeItem.tests.map(t => t.id).forEach(id => testIds.add(id));
|
||||
else if (treeItem.kind === 'test')
|
||||
testIds.add(treeItem.id);
|
||||
else
|
||||
treeItem.children?.forEach(visit);
|
||||
};
|
||||
visit(treeItem);
|
||||
return testIds;
|
||||
};
|
||||
|
||||
type Progress = {
|
||||
total: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
};
|
||||
|
||||
type TreeItemBase = {
|
||||
kind: 'root' | 'group' | 'case' | 'test',
|
||||
id: string;
|
||||
title: string;
|
||||
location: Location,
|
||||
duration: number;
|
||||
parent: TreeItem | undefined;
|
||||
children: TreeItem[];
|
||||
status: UITestStatus;
|
||||
};
|
||||
|
||||
type GroupItem = TreeItemBase & {
|
||||
kind: 'group';
|
||||
subKind: 'folder' | 'file' | 'describe';
|
||||
hasLoadErrors: boolean;
|
||||
children: (TestCaseItem | GroupItem)[];
|
||||
};
|
||||
|
||||
type TestCaseItem = TreeItemBase & {
|
||||
kind: 'case',
|
||||
tests: TestCase[];
|
||||
children: TestItem[];
|
||||
};
|
||||
|
||||
type TestItem = TreeItemBase & {
|
||||
kind: 'test',
|
||||
test: TestCase;
|
||||
project: string;
|
||||
};
|
||||
|
||||
type TreeItem = GroupItem | TestCaseItem | TestItem;
|
||||
|
||||
function getFileItem(rootItem: GroupItem, filePath: string[], isFile: boolean, fileItems: Map<string, GroupItem>): GroupItem {
|
||||
if (filePath.length === 0)
|
||||
return rootItem;
|
||||
const fileName = filePath.join(pathSeparator);
|
||||
const existingFileItem = fileItems.get(fileName);
|
||||
if (existingFileItem)
|
||||
return existingFileItem;
|
||||
const parentFileItem = getFileItem(rootItem, filePath.slice(0, filePath.length - 1), false, fileItems);
|
||||
const fileItem: GroupItem = {
|
||||
kind: 'group',
|
||||
subKind: isFile ? 'file' : 'folder',
|
||||
id: fileName,
|
||||
title: filePath[filePath.length - 1],
|
||||
location: { file: fileName, line: 0, column: 0 },
|
||||
duration: 0,
|
||||
parent: parentFileItem,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
parentFileItem.children.push(fileItem);
|
||||
fileItems.set(fileName, fileItem);
|
||||
return fileItem;
|
||||
}
|
||||
|
||||
function createTree(rootSuite: Suite | undefined, loadErrors: TestError[], projectFilters: Map<string, boolean>): GroupItem {
|
||||
const filterProjects = [...projectFilters.values()].some(Boolean);
|
||||
const rootItem: GroupItem = {
|
||||
kind: 'group',
|
||||
subKind: 'folder',
|
||||
id: 'root',
|
||||
title: '',
|
||||
location: { file: '', line: 0, column: 0 },
|
||||
duration: 0,
|
||||
parent: undefined,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
|
||||
const visitSuite = (projectName: string, parentSuite: Suite, parentGroup: GroupItem) => {
|
||||
for (const suite of parentSuite.suites) {
|
||||
const title = suite.title || '<anonymous>';
|
||||
let group = parentGroup.children.find(item => item.kind === 'group' && item.title === title) as GroupItem | undefined;
|
||||
if (!group) {
|
||||
group = {
|
||||
kind: 'group',
|
||||
subKind: 'describe',
|
||||
id: 'suite:' + parentSuite.titlePath().join('\x1e') + '\x1e' + title, // account for anonymous suites
|
||||
title,
|
||||
location: suite.location!,
|
||||
duration: 0,
|
||||
parent: parentGroup,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
parentGroup.children.push(group);
|
||||
}
|
||||
visitSuite(projectName, suite, group);
|
||||
}
|
||||
|
||||
for (const test of parentSuite.tests) {
|
||||
const title = test.title;
|
||||
let testCaseItem = parentGroup.children.find(t => t.kind !== 'group' && t.title === title) as TestCaseItem;
|
||||
if (!testCaseItem) {
|
||||
testCaseItem = {
|
||||
kind: 'case',
|
||||
id: 'test:' + test.titlePath().join('\x1e'),
|
||||
title,
|
||||
parent: parentGroup,
|
||||
children: [],
|
||||
tests: [],
|
||||
location: test.location,
|
||||
duration: 0,
|
||||
status: 'none',
|
||||
};
|
||||
parentGroup.children.push(testCaseItem);
|
||||
}
|
||||
|
||||
const result = test.results[0];
|
||||
let status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none';
|
||||
if ((result as any)?.[statusEx] === 'scheduled')
|
||||
status = 'scheduled';
|
||||
else if ((result as any)?.[statusEx] === 'running')
|
||||
status = 'running';
|
||||
else if (result?.status === 'skipped')
|
||||
status = 'skipped';
|
||||
else if (result?.status === 'interrupted')
|
||||
status = 'none';
|
||||
else if (result && test.outcome() !== 'expected')
|
||||
status = 'failed';
|
||||
else if (result && test.outcome() === 'expected')
|
||||
status = 'passed';
|
||||
|
||||
testCaseItem.tests.push(test);
|
||||
testCaseItem.children.push({
|
||||
kind: 'test',
|
||||
id: test.id,
|
||||
title: projectName,
|
||||
location: test.location!,
|
||||
test,
|
||||
parent: testCaseItem,
|
||||
children: [],
|
||||
status,
|
||||
duration: test.results.length ? Math.max(0, test.results[0].duration) : 0,
|
||||
project: projectName,
|
||||
});
|
||||
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 || []) {
|
||||
if (filterProjects && !projectFilters.get(projectSuite.title))
|
||||
continue;
|
||||
for (const fileSuite of projectSuite.suites) {
|
||||
const fileItem = getFileItem(rootItem, fileSuite.location!.file.split(pathSeparator), true, fileMap);
|
||||
visitSuite(projectSuite.title, fileSuite, fileItem);
|
||||
}
|
||||
}
|
||||
|
||||
for (const loadError of loadErrors) {
|
||||
if (!loadError.location)
|
||||
continue;
|
||||
const fileItem = getFileItem(rootItem, loadError.location.file.split(pathSeparator), true, fileMap);
|
||||
fileItem.hasLoadErrors = true;
|
||||
}
|
||||
return rootItem;
|
||||
}
|
||||
|
||||
function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map<string, boolean>, runningTestIds: Set<string> | undefined) {
|
||||
const tokens = filterText.trim().toLowerCase().split(' ');
|
||||
const filtersStatuses = [...statusFilters.values()].some(Boolean);
|
||||
|
||||
const filter = (testCase: TestCaseItem) => {
|
||||
const titleWithTags = [...testCase.tests[0].titlePath(), ...testCase.tests[0].tags].join(' ').toLowerCase();
|
||||
if (!tokens.every(token => titleWithTags.includes(token)) && !testCase.tests.some(t => runningTestIds?.has(t.id)))
|
||||
return false;
|
||||
testCase.children = (testCase.children as TestItem[]).filter(test => {
|
||||
return !filtersStatuses || runningTestIds?.has(test.test.id) || statusFilters.get(test.status);
|
||||
});
|
||||
testCase.tests = (testCase.children as TestItem[]).map(c => c.test);
|
||||
return !!testCase.children.length;
|
||||
};
|
||||
|
||||
const visit = (treeItem: GroupItem) => {
|
||||
const newChildren: (GroupItem | TestCaseItem)[] = [];
|
||||
for (const child of treeItem.children) {
|
||||
if (child.kind === 'case') {
|
||||
if (filter(child))
|
||||
newChildren.push(child);
|
||||
} else {
|
||||
visit(child);
|
||||
if (child.children.length || child.hasLoadErrors)
|
||||
newChildren.push(child);
|
||||
}
|
||||
}
|
||||
treeItem.children = newChildren;
|
||||
};
|
||||
visit(rootItem);
|
||||
}
|
||||
|
||||
function sortAndPropagateStatus(treeItem: TreeItem) {
|
||||
for (const child of treeItem.children)
|
||||
sortAndPropagateStatus(child);
|
||||
|
||||
if (treeItem.kind === 'group') {
|
||||
treeItem.children.sort((a, b) => {
|
||||
const fc = a.location.file.localeCompare(b.location.file);
|
||||
return fc || a.location.line - b.location.line;
|
||||
});
|
||||
}
|
||||
|
||||
let allPassed = treeItem.children.length > 0;
|
||||
let allSkipped = treeItem.children.length > 0;
|
||||
let hasFailed = false;
|
||||
let hasRunning = false;
|
||||
let hasScheduled = false;
|
||||
|
||||
for (const child of treeItem.children) {
|
||||
allSkipped = allSkipped && child.status === 'skipped';
|
||||
allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped');
|
||||
hasFailed = hasFailed || child.status === 'failed';
|
||||
hasRunning = hasRunning || child.status === 'running';
|
||||
hasScheduled = hasScheduled || child.status === 'scheduled';
|
||||
}
|
||||
|
||||
if (hasRunning)
|
||||
treeItem.status = 'running';
|
||||
else if (hasScheduled)
|
||||
treeItem.status = 'scheduled';
|
||||
else if (hasFailed)
|
||||
treeItem.status = 'failed';
|
||||
else if (allSkipped)
|
||||
treeItem.status = 'skipped';
|
||||
else if (allPassed)
|
||||
treeItem.status = 'passed';
|
||||
}
|
||||
|
||||
function shortenRoot(rootItem: GroupItem): GroupItem {
|
||||
let shortRoot = rootItem;
|
||||
while (shortRoot.children.length === 1 && shortRoot.children[0].kind === 'group' && shortRoot.children[0].subKind === 'folder')
|
||||
shortRoot = shortRoot.children[0];
|
||||
shortRoot.location = rootItem.location;
|
||||
return shortRoot;
|
||||
}
|
||||
|
||||
function hideOnlyTests(rootItem: GroupItem) {
|
||||
const visit = (treeItem: TreeItem) => {
|
||||
if (treeItem.kind === 'case' && treeItem.children.length === 1)
|
||||
treeItem.children = [];
|
||||
else
|
||||
treeItem.children.forEach(visit);
|
||||
};
|
||||
visit(rootItem);
|
||||
}
|
||||
|
||||
async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', url);
|
||||
@ -1054,6 +682,3 @@ async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
|
||||
const contextEntries = await response.json() as ContextEntry[];
|
||||
return new MultiTraceModel(contextEntries);
|
||||
}
|
||||
|
||||
const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/';
|
||||
const statusEx = Symbol('statusEx');
|
||||
|
Loading…
Reference in New Issue
Block a user