feat(test runner): export testInfo.data (#7525)
This is a key-value storage for any information that goes into the report. Also export JSONReport types.
This commit is contained in:
parent
b6b96daa88
commit
77deca1d6b
|
|
@ -275,6 +275,7 @@ export class Dispatcher {
|
||||||
const { test, result } = this._testById.get(params.testId)!;
|
const { test, result } = this._testById.get(params.testId)!;
|
||||||
result.duration = params.duration;
|
result.duration = params.duration;
|
||||||
result.error = params.error;
|
result.error = params.error;
|
||||||
|
result.data = params.data;
|
||||||
test.expectedStatus = params.expectedStatus;
|
test.expectedStatus = params.expectedStatus;
|
||||||
test.annotations = params.annotations;
|
test.annotations = params.annotations;
|
||||||
test.timeout = params.timeout;
|
test.timeout = params.timeout;
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||||
const preserveTrace = captureTrace && (trace === 'on' || (testFailed && trace === 'retain-on-failure') || (trace === 'on-first-retry' && testInfo.retry === 1));
|
const preserveTrace = captureTrace && (trace === 'on' || (testFailed && trace === 'retain-on-failure') || (trace === 'on-first-retry' && testInfo.retry === 1));
|
||||||
if (preserveTrace) {
|
if (preserveTrace) {
|
||||||
const tracePath = testInfo.outputPath(`trace.zip`);
|
const tracePath = testInfo.outputPath(`trace.zip`);
|
||||||
|
testInfo.data.playwrightTrace = tracePath;
|
||||||
await context.tracing.stop({ path: tracePath });
|
await context.tracing.stop({ path: tracePath });
|
||||||
} else if (captureTrace) {
|
} else if (captureTrace) {
|
||||||
await context.tracing.stop();
|
await context.tracing.stop();
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export type TestEndPayload = {
|
||||||
expectedStatus: TestStatus;
|
expectedStatus: TestStatus;
|
||||||
annotations: { type: string, description?: string }[];
|
annotations: { type: string, description?: string }[];
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
data: { [key: string]: any },
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestEntry = {
|
export type TestEntry = {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ export interface TestResult {
|
||||||
duration: number;
|
duration: number;
|
||||||
status?: TestStatus;
|
status?: TestStatus;
|
||||||
error?: TestError;
|
error?: TestError;
|
||||||
|
data: { [key: string]: any };
|
||||||
stdout: (string | Buffer)[];
|
stdout: (string | Buffer)[];
|
||||||
stderr: (string | Buffer)[];
|
stderr: (string | Buffer)[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,60 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import EmptyReporter from './empty';
|
import EmptyReporter from './empty';
|
||||||
import { FullConfig, Test, Suite, Spec, TestResult, TestError, FullResult } from '../reporter';
|
import { FullConfig, Test, Suite, Spec, TestResult, TestError, FullResult, TestStatus } from '../reporter';
|
||||||
|
|
||||||
interface SerializedSuite {
|
export interface JSONReport {
|
||||||
|
config: Omit<FullConfig, 'projects'> & {
|
||||||
|
projects: {
|
||||||
|
outputDir: string,
|
||||||
|
repeatEach: number,
|
||||||
|
retries: number,
|
||||||
|
metadata: any,
|
||||||
|
name: string,
|
||||||
|
testDir: string,
|
||||||
|
testIgnore: string[],
|
||||||
|
testMatch: string[],
|
||||||
|
timeout: number,
|
||||||
|
}[],
|
||||||
|
};
|
||||||
|
suites: JSONReportSuite[];
|
||||||
|
errors: TestError[];
|
||||||
|
}
|
||||||
|
export interface JSONReportSuite {
|
||||||
title: string;
|
title: string;
|
||||||
file: string;
|
file: string;
|
||||||
column: number;
|
column: number;
|
||||||
line: number;
|
line: number;
|
||||||
specs: ReturnType<JSONReporter['_serializeTestSpec']>[];
|
specs: JSONReportSpec[];
|
||||||
suites?: SerializedSuite[];
|
suites?: JSONReportSuite[];
|
||||||
}
|
}
|
||||||
|
export interface JSONReportSpec {
|
||||||
export type ReportFormat = ReturnType<JSONReporter['_serializeReport']>;
|
title: string;
|
||||||
|
ok: boolean;
|
||||||
|
tests: JSONReportTest[];
|
||||||
|
file: string;
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
export interface JSONReportTest {
|
||||||
|
timeout: number;
|
||||||
|
annotations: { type: string, description?: string }[],
|
||||||
|
expectedStatus: TestStatus;
|
||||||
|
projectName: string;
|
||||||
|
results: JSONReportTestResult[];
|
||||||
|
status: 'skipped' | 'expected' | 'unexpected' | 'flaky';
|
||||||
|
}
|
||||||
|
export interface JSONReportTestResult {
|
||||||
|
workerIndex: number;
|
||||||
|
status: TestStatus | undefined;
|
||||||
|
duration: number;
|
||||||
|
error: TestError | undefined;
|
||||||
|
stdout: JSONReportSTDIOEntry[],
|
||||||
|
stderr: JSONReportSTDIOEntry[],
|
||||||
|
retry: number;
|
||||||
|
data: { [key: string]: any },
|
||||||
|
}
|
||||||
|
export type JSONReportSTDIOEntry = { text: string } | { buffer: string };
|
||||||
|
|
||||||
function toPosixPath(aPath: string): string {
|
function toPosixPath(aPath: string): string {
|
||||||
return aPath.split(path.sep).join(path.posix.sep);
|
return aPath.split(path.sep).join(path.posix.sep);
|
||||||
|
|
@ -58,7 +100,7 @@ class JSONReporter extends EmptyReporter {
|
||||||
outputReport(this._serializeReport(), this._outputFile);
|
outputReport(this._serializeReport(), this._outputFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serializeReport() {
|
private _serializeReport(): JSONReport {
|
||||||
return {
|
return {
|
||||||
config: {
|
config: {
|
||||||
...this.config,
|
...this.config,
|
||||||
|
|
@ -77,15 +119,15 @@ class JSONReporter extends EmptyReporter {
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
suites: this.suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s),
|
suites: this.suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s) as JSONReportSuite[],
|
||||||
errors: this._errors
|
errors: this._errors
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serializeSuite(suite: Suite): null | SerializedSuite {
|
private _serializeSuite(suite: Suite): null | JSONReportSuite {
|
||||||
if (!suite.findSpec(test => true))
|
if (!suite.findSpec(test => true))
|
||||||
return null;
|
return null;
|
||||||
const suites = suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s) as SerializedSuite[];
|
const suites = suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s) as JSONReportSuite[];
|
||||||
return {
|
return {
|
||||||
title: suite.title,
|
title: suite.title,
|
||||||
file: toPosixPath(path.relative(this.config.rootDir, suite.file)),
|
file: toPosixPath(path.relative(this.config.rootDir, suite.file)),
|
||||||
|
|
@ -96,7 +138,7 @@ class JSONReporter extends EmptyReporter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serializeTestSpec(spec: Spec) {
|
private _serializeTestSpec(spec: Spec): JSONReportSpec {
|
||||||
return {
|
return {
|
||||||
title: spec.title,
|
title: spec.title,
|
||||||
ok: spec.ok(),
|
ok: spec.ok(),
|
||||||
|
|
@ -107,17 +149,18 @@ class JSONReporter extends EmptyReporter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serializeTest(test: Test) {
|
private _serializeTest(test: Test): JSONReportTest {
|
||||||
return {
|
return {
|
||||||
timeout: test.timeout,
|
timeout: test.timeout,
|
||||||
annotations: test.annotations,
|
annotations: test.annotations,
|
||||||
expectedStatus: test.expectedStatus,
|
expectedStatus: test.expectedStatus,
|
||||||
projectName: test.projectName,
|
projectName: test.projectName,
|
||||||
results: test.results.map(r => this._serializeTestResult(r)),
|
results: test.results.map(r => this._serializeTestResult(r)),
|
||||||
|
status: test.status(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serializeTestResult(result: TestResult) {
|
private _serializeTestResult(result: TestResult): JSONReportTestResult {
|
||||||
return {
|
return {
|
||||||
workerIndex: result.workerIndex,
|
workerIndex: result.workerIndex,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
|
|
@ -126,11 +169,12 @@ class JSONReporter extends EmptyReporter {
|
||||||
stdout: result.stdout.map(s => stdioEntry(s)),
|
stdout: result.stdout.map(s => stdioEntry(s)),
|
||||||
stderr: result.stderr.map(s => stdioEntry(s)),
|
stderr: result.stderr.map(s => stdioEntry(s)),
|
||||||
retry: result.retry,
|
retry: result.retry,
|
||||||
|
data: result.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function outputReport(report: ReportFormat, outputFile: string | undefined) {
|
function outputReport(report: JSONReport, outputFile: string | undefined) {
|
||||||
const reportString = JSON.stringify(report, undefined, 2);
|
const reportString = JSON.stringify(report, undefined, 2);
|
||||||
outputFile = outputFile || process.env[`PLAYWRIGHT_JSON_OUTPUT_NAME`];
|
outputFile = outputFile || process.env[`PLAYWRIGHT_JSON_OUTPUT_NAME`];
|
||||||
if (outputFile) {
|
if (outputFile) {
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,7 @@ export class Test implements reporterTypes.Test {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
stdout: [],
|
stdout: [],
|
||||||
stderr: [],
|
stderr: [],
|
||||||
|
data: {},
|
||||||
};
|
};
|
||||||
this.results.push(result);
|
this.results.push(result);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
retry: entry.retry,
|
retry: entry.retry,
|
||||||
expectedStatus: 'passed',
|
expectedStatus: 'passed',
|
||||||
annotations: [],
|
annotations: [],
|
||||||
|
data: {},
|
||||||
duration: 0,
|
duration: 0,
|
||||||
status: 'passed',
|
status: 'passed',
|
||||||
stdout: [],
|
stdout: [],
|
||||||
|
|
@ -484,6 +485,7 @@ function buildTestEndPayload(testId: string, testInfo: TestInfo): TestEndPayload
|
||||||
expectedStatus: testInfo.expectedStatus,
|
expectedStatus: testInfo.expectedStatus,
|
||||||
annotations: testInfo.annotations,
|
annotations: testInfo.annotations,
|
||||||
timeout: testInfo.timeout,
|
timeout: testInfo.timeout,
|
||||||
|
data: testInfo.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,3 +82,21 @@ test('should report projectName in result', async ({ runInlineTest }) => {
|
||||||
expect(report.suites[0].specs[0].tests[1].projectName).toBe('');
|
expect(report.suites[0].specs[0].tests[1].projectName).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should access testInfo.data in fixture', async ({ runInlineTest }) => {
|
||||||
|
const { exitCode, report } = await runInlineTest({
|
||||||
|
'test-data-visible-in-env.spec.ts': `
|
||||||
|
const test = pwt.test.extend({
|
||||||
|
foo: async ({}, run, testInfo) => {
|
||||||
|
await run();
|
||||||
|
testInfo.data.foo = 'bar';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
test('ensure fixture can set data', async ({ foo }) => {
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
const test = report.suites[0].specs[0].tests[0];
|
||||||
|
expect(test.results[0].data).toEqual({ foo: 'bar' });
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { spawn } from 'child_process';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import type { ReportFormat } from '../../src/test/reporters/json';
|
import type { JSONReport, JSONReportSuite } from '../../src/test/reporters/json';
|
||||||
import rimraf from 'rimraf';
|
import rimraf from 'rimraf';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
|
|
@ -33,7 +33,7 @@ type RunResult = {
|
||||||
failed: number,
|
failed: number,
|
||||||
flaky: number,
|
flaky: number,
|
||||||
skipped: number,
|
skipped: number,
|
||||||
report: ReportFormat,
|
report: JSONReport,
|
||||||
results: any[],
|
results: any[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -191,7 +191,7 @@ async function runPlaywrightTest(baseDir: string, params: any, env: Env, options
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
function visitSuites(suites?: ReportFormat['suites']) {
|
function visitSuites(suites?: JSONReportSuite[]) {
|
||||||
if (!suites)
|
if (!suites)
|
||||||
return;
|
return;
|
||||||
for (const suite of suites) {
|
for (const suite of suites) {
|
||||||
|
|
|
||||||
7
types/test.d.ts
vendored
7
types/test.d.ts
vendored
|
|
@ -124,7 +124,7 @@ export type WebServerConfig = {
|
||||||
*/
|
*/
|
||||||
command: string,
|
command: string,
|
||||||
/**
|
/**
|
||||||
* The port that your server is expected to appear on. If not specified, it does get automatically collected via the
|
* The port that your server is expected to appear on. If not specified, it does get automatically collected via the
|
||||||
* command output when a localhost URL gets printed.
|
* command output when a localhost URL gets printed.
|
||||||
*/
|
*/
|
||||||
port?: number,
|
port?: number,
|
||||||
|
|
@ -394,6 +394,11 @@ export interface TestInfo extends WorkerInfo {
|
||||||
*/
|
*/
|
||||||
annotations: { type: string, description?: string }[];
|
annotations: { type: string, description?: string }[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arbitrary data that test fixtures can provide for the test report.
|
||||||
|
*/
|
||||||
|
data: { [key: string]: any };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When tests are run multiple times, each run gets a unique `repeatEachIndex`.
|
* When tests are run multiple times, each run gets a unique `repeatEachIndex`.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue