feat(test-runner): introduce attachments (#7685)

This commit is contained in:
Pavel Feldman 2021-07-16 13:48:37 -07:00 committed by GitHub
parent 31572fc372
commit bde764085c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 48 additions and 18 deletions

View file

@ -271,7 +271,12 @@ 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; result.attachments = params.attachments.map(a => ({
name: a.name,
path: a.path,
contentType: a.contentType,
body: a.body ? Buffer.from(a.body, 'base64') : undefined
}));
test.expectedStatus = params.expectedStatus; test.expectedStatus = params.expectedStatus;
test.annotations = params.annotations; test.annotations = params.annotations;
test.timeout = params.timeout; test.timeout = params.timeout;

View file

@ -38,7 +38,7 @@ function toMatchSnapshot(this: ReturnType<Expect['getState']>, received: Buffer
options.threshold = projectThreshold; options.threshold = projectThreshold;
const withNegateComparison = this.isNot; const withNegateComparison = this.isNot;
const { pass, message } = compare( const { pass, message, expectedPath, actualPath, diffPath, mimeType } = compare(
received, received,
options.name, options.name,
testInfo.snapshotPath, testInfo.snapshotPath,
@ -47,6 +47,13 @@ function toMatchSnapshot(this: ReturnType<Expect['getState']>, received: Buffer
withNegateComparison, withNegateComparison,
options options
); );
const contentType = mimeType || 'application/octet-stream';
if (expectedPath)
testInfo.attachments.push({ name: 'expected', contentType, path: expectedPath });
if (actualPath)
testInfo.attachments.push({ name: 'actual', contentType, path: actualPath });
if (diffPath)
testInfo.attachments.push({ name: 'diff', contentType, path: diffPath });
return { pass, message: () => message }; return { pass, message: () => message };
} }

View file

@ -88,7 +88,7 @@ export function compare(
updateSnapshots: UpdateSnapshots, updateSnapshots: UpdateSnapshots,
withNegateComparison: boolean, withNegateComparison: boolean,
options?: { threshold?: number } options?: { threshold?: number }
): { pass: boolean; message?: string; } { ): { pass: boolean; message?: string; expectedPath?: string, actualPath?: string, diffPath?: string, mimeType?: string } {
const snapshotFile = snapshotPath(name); const snapshotFile = snapshotPath(name);
if (!fs.existsSync(snapshotFile)) { if (!fs.existsSync(snapshotFile)) {
@ -179,6 +179,10 @@ export function compare(
return { return {
pass: false, pass: false,
message: output.join('\n'), message: output.join('\n'),
expectedPath,
actualPath,
diffPath,
mimeType
}; };
} }

View file

@ -162,17 +162,21 @@ 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 });
testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
} else if (captureTrace) { } else if (captureTrace) {
await context.tracing.stop(); await context.tracing.stop();
} }
const captureScreenshots = (screenshot === 'on' || (screenshot === 'only-on-failure' && testFailed)); const captureScreenshots = (screenshot === 'on' || (screenshot === 'only-on-failure' && testFailed));
if (captureScreenshots) { if (captureScreenshots) {
await Promise.all(allPages.map((page, index) => { await Promise.all(allPages.map(async (page, index) => {
const screenshotPath = testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${++index}.png`); const screenshotPath = testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${++index}.png`);
return page.screenshot({ timeout: 5000, path: screenshotPath }).catch(e => {}); try {
await page.screenshot({ timeout: 5000, path: screenshotPath });
testInfo.attachments.push({ name: 'screenshot', path: screenshotPath, contentType: 'image/png' });
} catch {
}
})); }));
} }

View file

@ -42,7 +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 }, attachments: { name: string, path?: string, body?: string, contentType: string }[];
}; };
export type TestEntry = { export type TestEntry = {

View file

@ -51,7 +51,7 @@ export interface TestResult {
duration: number; duration: number;
status?: TestStatus; status?: TestStatus;
error?: TestError; error?: TestError;
data: { [key: string]: any }; attachments: { name: string, path?: string, body?: Buffer, contentType: string }[];
stdout: (string | Buffer)[]; stdout: (string | Buffer)[];
stderr: (string | Buffer)[]; stderr: (string | Buffer)[];
} }

View file

@ -67,7 +67,7 @@ export interface JSONReportTestResult {
stdout: JSONReportSTDIOEntry[], stdout: JSONReportSTDIOEntry[],
stderr: JSONReportSTDIOEntry[], stderr: JSONReportSTDIOEntry[],
retry: number; retry: number;
data: { [key: string]: any }, attachments: { name: string, path?: string, body?: string, contentType: string }[];
} }
export type JSONReportSTDIOEntry = { text: string } | { buffer: string }; export type JSONReportSTDIOEntry = { text: string } | { buffer: string };
@ -217,7 +217,12 @@ class JSONReporter implements Reporter {
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, attachments: result.attachments.map(a => ({
name: a.name,
contentType: a.contentType,
path: a.path,
body: a.body?.toString('base64')
})),
}; };
} }
} }

View file

@ -187,7 +187,7 @@ export class Test extends Base implements reporterTypes.Test {
duration: 0, duration: 0,
stdout: [], stdout: [],
stderr: [], stderr: [],
data: {}, attachments: [],
}; };
this.results.push(result); this.results.push(result);
return result; return result;

View file

@ -226,7 +226,7 @@ export class WorkerRunner extends EventEmitter {
retry: entry.retry, retry: entry.retry,
expectedStatus: 'passed', expectedStatus: 'passed',
annotations: [], annotations: [],
data: {}, attachments: [],
duration: 0, duration: 0,
status: 'passed', status: 'passed',
stdout: [], stdout: [],
@ -477,7 +477,12 @@ 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, attachments: testInfo.attachments.map(a => ({
name: a.name,
contentType: a.contentType,
path: a.path,
body: a.body?.toString('base64')
}))
}; };
} }

View file

@ -83,13 +83,13 @@ test('should report projectName in result', async ({ runInlineTest }) => {
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
}); });
test('should access testInfo.data in fixture', async ({ runInlineTest }) => { test('should access testInfo.attachments in fixture', async ({ runInlineTest }) => {
const { exitCode, report } = await runInlineTest({ const { exitCode, report } = await runInlineTest({
'test-data-visible-in-env.spec.ts': ` 'test-data-visible-in-env.spec.ts': `
const test = pwt.test.extend({ const test = pwt.test.extend({
foo: async ({}, run, testInfo) => { foo: async ({}, run, testInfo) => {
await run(); await run();
testInfo.data.foo = 'bar'; testInfo.attachments.push({ name: 'foo', body: Buffer.from([1, 2, 3]), contentType: 'application/octet-stream' });
}, },
}); });
test('ensure fixture can set data', async ({ foo }) => { test('ensure fixture can set data', async ({ foo }) => {
@ -98,5 +98,5 @@ test('should access testInfo.data in fixture', async ({ runInlineTest }) => {
}); });
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const test = report.suites[0].specs[0].tests[0]; const test = report.suites[0].specs[0].tests[0];
expect(test.results[0].data).toEqual({ foo: 'bar' }); expect(test.results[0].attachments[0]).toEqual({ name: 'foo', body: 'AQID', contentType: 'application/octet-stream' });
}); });

4
types/test.d.ts vendored
View file

@ -399,9 +399,9 @@ 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. * File attachments for this test.
*/ */
data: { [key: string]: any }; attachments: { name: string, path?: string, body?: Buffer, contentType: string }[];
/** /**
* When tests are run multiple times, each run gets a unique `repeatEachIndex`. * When tests are run multiple times, each run gets a unique `repeatEachIndex`.