feat(test-runner): introduce attachments (#7685)
This commit is contained in:
parent
31572fc372
commit
bde764085c
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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)[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
4
types/test.d.ts
vendored
|
|
@ -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`.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue