feat(test-runner): small changes to Reporter api (#7709)
- `TestResult.startTime` - `Suite.location` is optional now - `Test.status()` renamed to `Test.outcome()` to differentiate against a `Test.expectedStatus` and `TestResult.status` of the different type.
This commit is contained in:
parent
e5c7941b49
commit
66ea613c4d
|
|
@ -265,6 +265,7 @@ export class Dispatcher {
|
||||||
worker.on('testBegin', (params: TestBeginPayload) => {
|
worker.on('testBegin', (params: TestBeginPayload) => {
|
||||||
const { test, result: testRun } = this._testById.get(params.testId)!;
|
const { test, result: testRun } = this._testById.get(params.testId)!;
|
||||||
testRun.workerIndex = params.workerIndex;
|
testRun.workerIndex = params.workerIndex;
|
||||||
|
testRun.startTime = new Date(params.startWallTime);
|
||||||
this._reportTestBegin(test);
|
this._reportTestBegin(test);
|
||||||
});
|
});
|
||||||
worker.on('testEnd', (params: TestEndPayload) => {
|
worker.on('testEnd', (params: TestEndPayload) => {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ export type WorkerInitParams = {
|
||||||
|
|
||||||
export type TestBeginPayload = {
|
export type TestBeginPayload = {
|
||||||
testId: string;
|
testId: string;
|
||||||
workerIndex: number,
|
startWallTime: number; // milliseconds since unix epoch
|
||||||
|
workerIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestEndPayload = {
|
export type TestEndPayload = {
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ export class Loader {
|
||||||
try {
|
try {
|
||||||
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file));
|
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file));
|
||||||
suite._requireFile = file;
|
suite._requireFile = file;
|
||||||
suite.location.file = file;
|
suite.location = { file, line: 0, column: 0 };
|
||||||
setCurrentlyLoadingFileSuite(suite);
|
setCurrentlyLoadingFileSuite(suite);
|
||||||
await this._requireOrImport(file);
|
await this._requireOrImport(file);
|
||||||
this._fileSuites.set(file, suite);
|
this._fileSuites.set(file, suite);
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ export class BaseReporter implements Reporter {
|
||||||
const flaky: Test[] = [];
|
const flaky: Test[] = [];
|
||||||
|
|
||||||
this.suite.allTests().forEach(test => {
|
this.suite.allTests().forEach(test => {
|
||||||
switch (test.status()) {
|
switch (test.outcome()) {
|
||||||
case 'skipped': ++skipped; break;
|
case 'skipped': ++skipped; break;
|
||||||
case 'expected': ++expected; break;
|
case 'expected': ++expected; break;
|
||||||
case 'unexpected': unexpected.push(test); break;
|
case 'unexpected': unexpected.push(test); break;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class DotReporter extends BaseReporter {
|
||||||
process.stdout.write(colors.gray('×'));
|
process.stdout.write(colors.gray('×'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (test.status()) {
|
switch (test.outcome()) {
|
||||||
case 'expected': process.stdout.write(colors.green('·')); break;
|
case 'expected': process.stdout.write(colors.green('·')); break;
|
||||||
case 'unexpected': process.stdout.write(colors.red(test.results[test.results.length - 1].status === 'timedOut' ? 'T' : 'F')); break;
|
case 'unexpected': process.stdout.write(colors.red(test.results[test.results.length - 1].status === 'timedOut' ? 'T' : 'F')); break;
|
||||||
case 'flaky': process.stdout.write(colors.yellow('±')); break;
|
case 'flaky': process.stdout.write(colors.yellow('±')); break;
|
||||||
|
|
|
||||||
|
|
@ -127,21 +127,24 @@ class JSONReporter implements Reporter {
|
||||||
const result: JSONReportSuite[] = [];
|
const result: JSONReportSuite[] = [];
|
||||||
for (const projectSuite of suites) {
|
for (const projectSuite of suites) {
|
||||||
for (const fileSuite of projectSuite.suites) {
|
for (const fileSuite of projectSuite.suites) {
|
||||||
if (!fileSuites.has(fileSuite.location.file)) {
|
const file = fileSuite.location!.file;
|
||||||
|
if (!fileSuites.has(file)) {
|
||||||
const serialized = this._serializeSuite(fileSuite);
|
const serialized = this._serializeSuite(fileSuite);
|
||||||
if (serialized) {
|
if (serialized) {
|
||||||
fileSuites.set(fileSuite.location.file, serialized);
|
fileSuites.set(file, serialized);
|
||||||
result.push(serialized);
|
result.push(serialized);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._mergeTestsFromSuite(fileSuites.get(fileSuite.location.file)!, fileSuite);
|
this._mergeTestsFromSuite(fileSuites.get(file)!, fileSuite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _relativeLocation(location: Location): Location {
|
private _relativeLocation(location: Location | undefined): Location {
|
||||||
|
if (!location)
|
||||||
|
return { file: '', line: 0, column: 0 };
|
||||||
return {
|
return {
|
||||||
file: toPosixPath(path.relative(this.config.rootDir, location.file)),
|
file: toPosixPath(path.relative(this.config.rootDir, location.file)),
|
||||||
line: location.line,
|
line: location.line,
|
||||||
|
|
@ -149,7 +152,7 @@ class JSONReporter implements Reporter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _locationMatches(s: JSONReportSuite | JSONReportSpec, location: Location) {
|
private _locationMatches(s: JSONReportSuite | JSONReportSpec, location: Location | undefined) {
|
||||||
const relative = this._relativeLocation(location);
|
const relative = this._relativeLocation(location);
|
||||||
return s.file === relative.file && s.line === relative.line && s.column === relative.column;
|
return s.file === relative.file && s.line === relative.line && s.column === relative.column;
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +208,7 @@ class JSONReporter implements Reporter {
|
||||||
expectedStatus: test.expectedStatus,
|
expectedStatus: test.expectedStatus,
|
||||||
projectName: test.titlePath()[1],
|
projectName: test.titlePath()[1],
|
||||||
results: test.results.map(r => this._serializeTestResult(r)),
|
results: test.results.map(r => this._serializeTestResult(r)),
|
||||||
status: test.status(),
|
status: test.outcome(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ class JUnitReporter implements Reporter {
|
||||||
|
|
||||||
suite.allTests().forEach(test => {
|
suite.allTests().forEach(test => {
|
||||||
++tests;
|
++tests;
|
||||||
if (test.status() === 'skipped')
|
if (test.outcome() === 'skipped')
|
||||||
++skipped;
|
++skipped;
|
||||||
if (!test.ok())
|
if (!test.ok())
|
||||||
++failures;
|
++failures;
|
||||||
|
|
@ -102,7 +102,7 @@ class JUnitReporter implements Reporter {
|
||||||
const entry: XMLEntry = {
|
const entry: XMLEntry = {
|
||||||
name: 'testsuite',
|
name: 'testsuite',
|
||||||
attributes: {
|
attributes: {
|
||||||
name: path.relative(this.config.rootDir, suite.location.file),
|
name: suite.location ? path.relative(this.config.rootDir, suite.location.file) : '',
|
||||||
timestamp: this.timestamp,
|
timestamp: this.timestamp,
|
||||||
hostname: '',
|
hostname: '',
|
||||||
tests,
|
tests,
|
||||||
|
|
@ -130,7 +130,7 @@ class JUnitReporter implements Reporter {
|
||||||
};
|
};
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
|
|
||||||
if (test.status() === 'skipped') {
|
if (test.outcome() === 'skipped') {
|
||||||
entry.children.push({ name: 'skipped'});
|
entry.children.push({ name: 'skipped'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,7 @@ function filterByFocusedLine(suite: Suite, focusedTestFileLines: FilePatternFilt
|
||||||
re.lastIndex = 0;
|
re.lastIndex = 0;
|
||||||
return re.test(testFileName) && (line === testLine || line === null);
|
return re.test(testFileName) && (line === testLine || line === null);
|
||||||
});
|
});
|
||||||
const suiteFilter = (suite: Suite) => testFileLineMatches(suite.location.file, suite.location.line);
|
const suiteFilter = (suite: Suite) => !!suite.location && testFileLineMatches(suite.location.file, suite.location.line);
|
||||||
const testFilter = (test: Test) => testFileLineMatches(test.location.file, test.location.line);
|
const testFilter = (test: Test) => testFileLineMatches(test.location.file, test.location.line);
|
||||||
return filterSuite(suite, suiteFilter, testFilter);
|
return filterSuite(suite, suiteFilter, testFilter);
|
||||||
}
|
}
|
||||||
|
|
@ -406,6 +406,8 @@ function getClashingTestsPerSuite(rootSuite: Suite): Map<string, Test[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildItemLocation(rootDir: string, testOrSuite: Suite | Test) {
|
function buildItemLocation(rootDir: string, testOrSuite: Suite | Test) {
|
||||||
|
if (!testOrSuite.location)
|
||||||
|
return '';
|
||||||
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
|
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import { Annotations, Location } from './types';
|
||||||
|
|
||||||
class Base {
|
class Base {
|
||||||
title: string;
|
title: string;
|
||||||
location: Location = { file: '', line: 0, column: 0 };
|
|
||||||
parent?: Suite;
|
parent?: Suite;
|
||||||
|
|
||||||
_only = false;
|
_only = false;
|
||||||
|
|
@ -48,6 +47,7 @@ export type Modifier = {
|
||||||
export class Suite extends Base implements reporterTypes.Suite {
|
export class Suite extends Base implements reporterTypes.Suite {
|
||||||
suites: Suite[] = [];
|
suites: Suite[] = [];
|
||||||
tests: Test[] = [];
|
tests: Test[] = [];
|
||||||
|
location?: Location;
|
||||||
_fixtureOverrides: any = {};
|
_fixtureOverrides: any = {};
|
||||||
_entries: (Suite | Test)[] = [];
|
_entries: (Suite | Test)[] = [];
|
||||||
_hooks: {
|
_hooks: {
|
||||||
|
|
@ -116,6 +116,7 @@ export class Suite extends Base implements reporterTypes.Suite {
|
||||||
export class Test extends Base implements reporterTypes.Test {
|
export class Test extends Base implements reporterTypes.Test {
|
||||||
fn: Function;
|
fn: Function;
|
||||||
results: reporterTypes.TestResult[] = [];
|
results: reporterTypes.TestResult[] = [];
|
||||||
|
location: Location;
|
||||||
|
|
||||||
expectedStatus: reporterTypes.TestStatus = 'passed';
|
expectedStatus: reporterTypes.TestStatus = 'passed';
|
||||||
timeout = 0;
|
timeout = 0;
|
||||||
|
|
@ -131,14 +132,15 @@ export class Test extends Base implements reporterTypes.Test {
|
||||||
_repeatEachIndex = 0;
|
_repeatEachIndex = 0;
|
||||||
_projectIndex = 0;
|
_projectIndex = 0;
|
||||||
|
|
||||||
constructor(title: string, fn: Function, ordinalInFile: number, testType: TestTypeImpl) {
|
constructor(title: string, fn: Function, ordinalInFile: number, testType: TestTypeImpl, location: Location) {
|
||||||
super(title);
|
super(title);
|
||||||
this.fn = fn;
|
this.fn = fn;
|
||||||
this._ordinalInFile = ordinalInFile;
|
this._ordinalInFile = ordinalInFile;
|
||||||
this._testType = testType;
|
this._testType = testType;
|
||||||
|
this.location = location;
|
||||||
}
|
}
|
||||||
|
|
||||||
status(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
|
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
|
||||||
if (!this.results.length)
|
if (!this.results.length)
|
||||||
return 'skipped';
|
return 'skipped';
|
||||||
if (this.results.length === 1 && this.expectedStatus === this.results[0].status)
|
if (this.results.length === 1 && this.expectedStatus === this.results[0].status)
|
||||||
|
|
@ -158,14 +160,13 @@ export class Test extends Base implements reporterTypes.Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
ok(): boolean {
|
ok(): boolean {
|
||||||
const status = this.status();
|
const status = this.outcome();
|
||||||
return status === 'expected' || status === 'flaky' || status === 'skipped';
|
return status === 'expected' || status === 'flaky' || status === 'skipped';
|
||||||
}
|
}
|
||||||
|
|
||||||
_clone(): Test {
|
_clone(): Test {
|
||||||
const test = new Test(this.title, this.fn, this._ordinalInFile, this._testType);
|
const test = new Test(this.title, this.fn, this._ordinalInFile, this._testType, this.location);
|
||||||
test._only = this._only;
|
test._only = this._only;
|
||||||
test.location = this.location;
|
|
||||||
test._requireFile = this._requireFile;
|
test._requireFile = this._requireFile;
|
||||||
return test;
|
return test;
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +176,7 @@ export class Test extends Base implements reporterTypes.Test {
|
||||||
retry: this.results.length,
|
retry: this.results.length,
|
||||||
workerIndex: 0,
|
workerIndex: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
startTime: new Date(),
|
||||||
stdout: [],
|
stdout: [],
|
||||||
stderr: [],
|
stderr: [],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,8 @@ export class TestTypeImpl {
|
||||||
const ordinalInFile = countByFile.get(suite._requireFile) || 0;
|
const ordinalInFile = countByFile.get(suite._requireFile) || 0;
|
||||||
countByFile.set(suite._requireFile, ordinalInFile + 1);
|
countByFile.set(suite._requireFile, ordinalInFile + 1);
|
||||||
|
|
||||||
const test = new Test(title, fn, ordinalInFile, this);
|
const test = new Test(title, fn, ordinalInFile, this, location);
|
||||||
test._requireFile = suite._requireFile;
|
test._requireFile = suite._requireFile;
|
||||||
test.location = location;
|
|
||||||
suite._addTest(test);
|
suite._addTest(test);
|
||||||
|
|
||||||
if (type === 'only')
|
if (type === 'only')
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const startTime = monotonicTime();
|
const startTime = monotonicTime();
|
||||||
|
const startWallTime = Date.now();
|
||||||
let deadlineRunner: DeadlineRunner<any> | undefined;
|
let deadlineRunner: DeadlineRunner<any> | undefined;
|
||||||
const testId = test._id;
|
const testId = test._id;
|
||||||
|
|
||||||
|
|
@ -293,7 +294,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
return testInfo.timeout ? startTime + testInfo.timeout : undefined;
|
return testInfo.timeout ? startTime + testInfo.timeout : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emit('testBegin', buildTestBeginPayload(testId, testInfo));
|
this.emit('testBegin', buildTestBeginPayload(testId, testInfo, startWallTime));
|
||||||
|
|
||||||
if (testInfo.expectedStatus === 'skipped') {
|
if (testInfo.expectedStatus === 'skipped') {
|
||||||
testInfo.status = 'skipped';
|
testInfo.status = 'skipped';
|
||||||
|
|
@ -461,10 +462,11 @@ export class WorkerRunner extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTestBeginPayload(testId: string, testInfo: TestInfo): TestBeginPayload {
|
function buildTestBeginPayload(testId: string, testInfo: TestInfo, startWallTime: number): TestBeginPayload {
|
||||||
return {
|
return {
|
||||||
testId,
|
testId,
|
||||||
workerIndex: testInfo.workerIndex
|
workerIndex: testInfo.workerIndex,
|
||||||
|
startWallTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,10 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
|
||||||
onStdErr() {
|
onStdErr() {
|
||||||
console.log('\\n%%reporter-stderr%%');
|
console.log('\\n%%reporter-stderr%%');
|
||||||
}
|
}
|
||||||
onTestEnd(test) {
|
onTestEnd(test, result) {
|
||||||
console.log('\\n%%reporter-testend-' + test.title + '-' + test.titlePath()[1] + '%%');
|
console.log('\\n%%reporter-testend-' + test.title + '-' + test.titlePath()[1] + '%%');
|
||||||
|
if (!result.startTime)
|
||||||
|
console.log('\\n%%error-no-start-time');
|
||||||
}
|
}
|
||||||
onTimeout() {
|
onTimeout() {
|
||||||
console.log('\\n%%reporter-timeout%%');
|
console.log('\\n%%reporter-timeout%%');
|
||||||
|
|
|
||||||
15
types/testReporter.d.ts
vendored
15
types/testReporter.d.ts
vendored
|
|
@ -64,7 +64,7 @@ export interface Suite {
|
||||||
/**
|
/**
|
||||||
* Location where the suite is defined.
|
* Location where the suite is defined.
|
||||||
*/
|
*/
|
||||||
location: Location;
|
location?: Location;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Child suites.
|
* Child suites.
|
||||||
|
|
@ -142,9 +142,11 @@ export interface Test {
|
||||||
results: TestResult[];
|
results: TestResult[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overall test status.
|
* Testing outcome for this test. Note that outcome does not directly match to the status:
|
||||||
|
* - Test that is expected to fail and actually fails is 'expected'.
|
||||||
|
* - Test that passes on a second retry is 'flaky'.
|
||||||
*/
|
*/
|
||||||
status(): 'skipped' | 'expected' | 'unexpected' | 'flaky';
|
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the test is considered running fine.
|
* Whether the test is considered running fine.
|
||||||
|
|
@ -165,7 +167,12 @@ export interface TestResult {
|
||||||
/**
|
/**
|
||||||
* Index of the worker where the test was run.
|
* Index of the worker where the test was run.
|
||||||
*/
|
*/
|
||||||
workerIndex: number,
|
workerIndex: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test run start time.
|
||||||
|
*/
|
||||||
|
startTime: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Running time in milliseconds.
|
* Running time in milliseconds.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue