fix(test runner): testInfo.attach api review changes (#11211)

Remove overload, require name, merge options.
This commit is contained in:
Dmitry Gozman 2022-01-05 16:39:33 -08:00 committed by GitHub
parent 1857a16381
commit 3ecac56cc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 76 additions and 189 deletions

View File

@ -40,10 +40,11 @@ Learn more about [test annotations](./test-annotations.md).
The list of files or buffers attached to the current test. Some reporters show test attachments.
To safely add a file from disk as an attachment, please use [`method: TestInfo.attach#1`] instead of directly pushing onto this array. For inline attachments, use [`method: TestInfo.attach#1`].
To add an attachment, use [`method: TestInfo.attach`] instead of directly pushing onto this array.
## method: TestInfo.attach#1
Attach a file from disk to the current test. Some reporters show test attachments. The [`option: name`] and [`option: contentType`] will be inferred by default from the [`param: path`], but you can optionally override either of these.
## method: TestInfo.attach
Attach a value or a file from disk to the current test. Some reporters show test attachments. Either [`option: path`] or [`option: body`] must be specified, but not both.
For example, you can attach a screenshot to the test:
@ -52,15 +53,8 @@ const { test, expect } = require('@playwright/test');
test('basic test', async ({ page }, testInfo) => {
await page.goto('https://playwright.dev');
// Capture a screenshot and attach it.
const path = testInfo.outputPath('screenshot.png');
await page.screenshot({ path });
await testInfo.attach(path);
// Optionally override the name.
await testInfo.attach(path, { name: 'example.png' });
// Optionally override the contentType.
await testInfo.attach(path, { name: 'example.custom-file', contentType: 'x-custom-content-type' });
const screenshot = await page.screenshot();
await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});
```
@ -69,15 +63,8 @@ import { test, expect } from '@playwright/test';
test('basic test', async ({ page }, testInfo) => {
await page.goto('https://playwright.dev');
// Capture a screenshot and attach it.
const path = testInfo.outputPath('screenshot.png');
await page.screenshot({ path });
await testInfo.attach(path);
// Optionally override the name.
await testInfo.attach(path, { name: 'example.png' });
// Optionally override the contentType.
await testInfo.attach(path, { name: 'example.custom-file', contentType: 'x-custom-content-type' });
const screenshot = await page.screenshot();
await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});
```
@ -89,7 +76,7 @@ const { test, expect } = require('@playwright/test');
test('basic test', async ({}, testInfo) => {
const { download } = require('./my-custom-helpers');
const tmpPath = await download('a');
await testInfo.attach(tmpPath, { name: 'example.json' });
await testInfo.attach('downloaded', { path: tmpPath });
});
```
@ -99,38 +86,28 @@ import { test, expect } from '@playwright/test';
test('basic test', async ({}, testInfo) => {
const { download } = require('./my-custom-helpers');
const tmpPath = await download('a');
await testInfo.attach(tmpPath, { name: 'example.json' });
await testInfo.attach('downloaded', { path: tmpPath });
});
```
:::note
[`method: TestInfo.attach#1`] automatically takes care of copying attachments to a
location that is accessible to reporters, even if you were to delete the attachment
[`method: TestInfo.attach`] automatically takes care of copying attached files to a
location that is accessible to reporters. You can safely remove the attachment
after awaiting the attach call.
:::
### param: TestInfo.attach#1.path
- `path` <[string]> Path on the filesystem to the attached file.
### option: TestInfo.attach#1.name
- `name` <[void]|[string]> Optional attachment name. If omitted, this will be inferred from [`param: path`].
### option: TestInfo.attach#1.contentType
- `contentType` <[void]|[string]> Optional content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`. If omitted, this falls back to an inferred type based on the [`param: name`] (if set) or [`param: path`]'s extension; it will be set to `application/octet-stream` if the type cannot be inferred from the file extension.
## method: TestInfo.attach#2
Attach data to the current test, either a `string` or a `Buffer`. Some reporters show test attachments.
### param: TestInfo.attach#2.body
- `body` <[string]|[Buffer]> Attachment body.
### param: TestInfo.attach#2.name
### param: TestInfo.attach.name
- `name` <[string]> Attachment name.
### option: TestInfo.attach#2.contentType
- `contentType` <[void]|[string]> Optional content type of this attachment to properly present in the report, for example `'application/json'` or `'application/xml'`. If omitted, this falls back to an inferred type based on the [`param: name`]'s extension; if the type cannot be inferred from the name's extension, it will be set to `text/plain` (if [`param: body`] is a `string`) or `application/octet-stream` (if [`param: body`] is a `Buffer`).
### option: TestInfo.attach.body
- `body` <[string]|[Buffer]> Attachment body. Mutually exclusive with [`option: path`].
### option: TestInfo.attach.contentType
- `contentType` <[void]|[string]> Optional content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`. If omitted, content type is inferred based on the [`option: path`], or defaults to `text/plain` for [string] attachments and `application/octet-stream` for [Buffer] attachments.
### option: TestInfo.attach.path
- `path` <[string]> Path on the filesystem to the attached file. Mutually exclusive with [`option: body`].
## property: TestInfo.column
- type: <[int]>

View File

@ -264,39 +264,20 @@ export class WorkerRunner extends EventEmitter {
expectedStatus: test.expectedStatus,
annotations: [],
attachments: [],
attach: async (...args) => {
const [ pathOrBody, nameOrFileOptions, inlineOptions ] = args as [string | Buffer, string | { contentType?: string, name?: string} | undefined, { contentType?: string } | undefined];
let attachment: { name: string, contentType: string, body?: Buffer, path?: string } | undefined;
if (typeof nameOrFileOptions === 'string') { // inline attachment
const body = pathOrBody;
const name = nameOrFileOptions;
attachment = {
name,
contentType: inlineOptions?.contentType ?? (mime.getType(name) || (typeof body === 'string' ? 'text/plain' : 'application/octet-stream')),
body: typeof body === 'string' ? Buffer.from(body) : body,
};
} else { // path based attachment
const options = nameOrFileOptions;
const thePath = pathOrBody as string;
const name = options?.name ?? path.basename(thePath);
attachment = {
name,
path: thePath,
contentType: options?.contentType ?? (mime.getType(name) || 'application/octet-stream')
};
}
const tmpAttachment = { ...attachment };
if (attachment.path) {
const hash = await calculateFileSha1(attachment.path);
const dest = testInfo.outputPath('attachments', hash + path.extname(attachment.path));
attach: async (name: string, options: { path?: string, body?: string | Buffer, contentType?: string } = {}) => {
if ((options.path !== undefined ? 1 : 0) + (options.body !== undefined ? 1 : 0) !== 1)
throw new Error(`Exactly one of "path" and "body" must be specified`);
if (options.path) {
const hash = await calculateFileSha1(options.path);
const dest = testInfo.outputPath('attachments', hash + path.extname(options.path));
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
await fs.promises.copyFile(attachment.path, dest);
tmpAttachment.path = dest;
await fs.promises.copyFile(options.path, dest);
const contentType = options.contentType ?? (mime.getType(path.basename(options.path)) || 'application/octet-stream');
testInfo.attachments.push({ name, contentType, path: dest });
} else {
const contentType = options.contentType ?? (typeof options.body === 'string' ? 'text/plain' : 'application/octet-stream');
testInfo.attachments.push({ name, contentType, body: typeof options.body === 'string' ? Buffer.from(options.body) : options.body });
}
testInfo.attachments.push(tmpAttachment);
},
duration: 0,
status: 'passed',

View File

@ -1389,15 +1389,14 @@ export interface TestInfo {
/**
* The list of files or buffers attached to the current test. Some reporters show test attachments.
*
* To safely add a file from disk as an attachment, please use
* [testInfo.attach(path[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach-1) instead of
* directly pushing onto this array. For inline attachments, use
* [testInfo.attach(path[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach-1).
* To add an attachment, use
* [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach) instead of directly
* pushing onto this array.
*/
attachments: { name: string, path?: string, body?: Buffer, contentType: string }[];
/**
* Attach a file from disk to the current test. Some reporters show test attachments. The `name` and `contentType` will be
* inferred by default from the `path`, but you can optionally override either of these.
* Attach a value or a file from disk to the current test. Some reporters show test attachments. Either `path` or `body`
* must be specified, but not both.
*
* For example, you can attach a screenshot to the test:
*
@ -1406,15 +1405,8 @@ export interface TestInfo {
*
* test('basic test', async ({ page }, testInfo) => {
* await page.goto('https://playwright.dev');
*
* // Capture a screenshot and attach it.
* const path = testInfo.outputPath('screenshot.png');
* await page.screenshot({ path });
* await testInfo.attach(path);
* // Optionally override the name.
* await testInfo.attach(path, { name: 'example.png' });
* // Optionally override the contentType.
* await testInfo.attach(path, { name: 'example.custom-file', contentType: 'x-custom-content-type' });
* const screenshot = await page.screenshot();
* await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
* });
* ```
*
@ -1426,24 +1418,17 @@ export interface TestInfo {
* test('basic test', async ({}, testInfo) => {
* const { download } = require('./my-custom-helpers');
* const tmpPath = await download('a');
* await testInfo.attach(tmpPath, { name: 'example.json' });
* await testInfo.attach('downloaded', { path: tmpPath });
* });
* ```
*
* > NOTE: [testInfo.attach(path[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach-1)
* automatically takes care of copying attachments to a location that is accessible to reporters, even if you were to
* delete the attachment after awaiting the attach call.
* @param path
* @param options
*/
attach(path: string, options?: { contentType?: string, name?: string}): Promise<void>;
/**
* Attach data to the current test, either a `string` or a `Buffer`. Some reporters show test attachments.
* @param body
* > NOTE: [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach)
* automatically takes care of copying attached files to a location that is accessible to reporters. You can safely remove
* the attachment after awaiting the attach call.
* @param name
* @param options
*/
attach(body: string | Buffer, name: string, options?: { contentType?: string }): Promise<void>;
attach(name: string, options?: { contentType?: string, path?: string, body?: string | Buffer }): Promise<void>;
/**
* Specifies a unique repeat index when running in "repeat each" mode. This mode is enabled by passing `--repeat-each` to
* the [command line](https://playwright.dev/docs/test-cli).

View File

@ -81,33 +81,25 @@ test('render trace attachment', async ({ runInlineTest }) => {
});
test(`testInfo.attach throws an error when attaching a non-existent attachment`, async ({ runInlineTest }) => {
test(`testInfo.attach errors`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.js': `
const { test } = pwt;
test('all options specified', async ({}, testInfo) => {
await testInfo.attach('non-existent-path-all-options', { contentType: 'text/plain', name: 'foo.txt'});
test('fail1', async ({}, testInfo) => {
await testInfo.attach('name', { path: 'foo.txt' });
});
test('no options specified', async ({}, testInfo) => {
await testInfo.attach('non-existent-path-no-options');
test('fail2', async ({}, testInfo) => {
await testInfo.attach('name', { path: 'foo.txt', body: 'bar' });
});
test('partial options - contentType', async ({}, testInfo) => {
await testInfo.attach('non-existent-path-partial-options-content-type', { contentType: 'text/plain'});
});
test('partial options - name', async ({}, testInfo) => {
await testInfo.attach('non-existent-path-partial-options-name', { name: 'foo.txt'});
test('fail3', async ({}, testInfo) => {
await testInfo.attach('name', {});
});
`,
}, { reporter: 'line', workers: 1 });
const text = stripAscii(result.output).replace(/\\/g, '/');
expect(text).toMatch(/Error: ENOENT: no such file or directory, open '.*non-existent-path-all-options.*'/);
expect(text).toMatch(/Error: ENOENT: no such file or directory, open '.*non-existent-path-no-options.*'/);
expect(text).toMatch(/Error: ENOENT: no such file or directory, open '.*non-existent-path-partial-options-content-type.*'/);
expect(text).toMatch(/Error: ENOENT: no such file or directory, open '.*non-existent-path-partial-options-name.*'/);
expect(text).toMatch(/Error: ENOENT: no such file or directory, open '.*foo.txt.*'/);
expect(text).toContain(`Exactly one of "path" and "body" must be specified`);
expect(result.passed).toBe(0);
expect(result.failed).toBe(4);
expect(result.failed).toBe(3);
expect(result.exitCode).toBe(1);
});

View File

@ -119,15 +119,7 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
test('infer contentType from path', async ({}, testInfo) => {
const tmpPath = testInfo.outputPath('example.json');
await fs.promises.writeFile(tmpPath, 'We <3 Playwright!');
await testInfo.attach(tmpPath);
// Forcibly remove the tmp file to ensure attach is actually automagically copying it
await fs.promises.unlink(tmpPath);
});
test('infer contentType from name (over extension)', async ({}, testInfo) => {
const tmpPath = testInfo.outputPath('example.json');
await fs.promises.writeFile(tmpPath, 'We <3 Playwright!');
await testInfo.attach(tmpPath, { name: 'example.png' });
await testInfo.attach('foo', { path: tmpPath });
// Forcibly remove the tmp file to ensure attach is actually automagically copying it
await fs.promises.unlink(tmpPath);
});
@ -135,7 +127,7 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
test('explicit contentType (over extension)', async ({}, testInfo) => {
const tmpPath = testInfo.outputPath('example.json');
await fs.promises.writeFile(tmpPath, 'We <3 Playwright!');
await testInfo.attach(tmpPath, { contentType: 'image/png' });
await testInfo.attach('foo', { path: tmpPath, contentType: 'image/png' });
// Forcibly remove the tmp file to ensure attach is actually automagically copying it
await fs.promises.unlink(tmpPath);
});
@ -143,15 +135,15 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
test('explicit contentType (over extension and name)', async ({}, testInfo) => {
const tmpPath = testInfo.outputPath('example.json');
await fs.promises.writeFile(tmpPath, 'We <3 Playwright!');
await testInfo.attach(tmpPath, { name: 'example.png', contentType: 'x-playwright/custom' });
await testInfo.attach('example.png', { path: tmpPath, contentType: 'x-playwright/custom' });
// Forcibly remove the tmp file to ensure attach is actually automagically copying it
await fs.promises.unlink(tmpPath);
});
test('fallback contentType', async ({}, testInfo) => {
const tmpPath = testInfo.outputPath('example.json');
const tmpPath = testInfo.outputPath('example.this-extension-better-not-map-to-an-actual-mimetype');
await fs.promises.writeFile(tmpPath, 'We <3 Playwright!');
await testInfo.attach(tmpPath, { name: 'example.this-extension-better-not-map-to-an-actual-mimetype' });
await testInfo.attach('foo', { path: tmpPath });
// Forcibly remove the tmp file to ensure attach is actually automagically copying it
await fs.promises.unlink(tmpPath);
});
@ -160,7 +152,7 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
const json = JSON.parse(fs.readFileSync(testInfo.outputPath('test-results', 'report', 'project.report'), 'utf-8'));
{
const result = json.suites[0].tests[0].results[0];
expect(result.attachments[0].name).toBe('example.json');
expect(result.attachments[0].name).toBe('foo');
expect(result.attachments[0].contentType).toBe('application/json');
const p = result.attachments[0].path;
expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.json$/);
@ -169,7 +161,7 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
}
{
const result = json.suites[0].tests[1].results[0];
expect(result.attachments[0].name).toBe('example.png');
expect(result.attachments[0].name).toBe('foo');
expect(result.attachments[0].contentType).toBe('image/png');
const p = result.attachments[0].path;
expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.json$/);
@ -178,15 +170,6 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
}
{
const result = json.suites[0].tests[2].results[0];
expect(result.attachments[0].name).toBe('example.json');
expect(result.attachments[0].contentType).toBe('image/png');
const p = result.attachments[0].path;
expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.json$/);
const contents = fs.readFileSync(p);
expect(contents.toString()).toBe('We <3 Playwright!');
}
{
const result = json.suites[0].tests[3].results[0];
expect(result.attachments[0].name).toBe('example.png');
expect(result.attachments[0].contentType).toBe('x-playwright/custom');
const p = result.attachments[0].path;
@ -195,11 +178,11 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
expect(contents.toString()).toBe('We <3 Playwright!');
}
{
const result = json.suites[0].tests[4].results[0];
expect(result.attachments[0].name).toBe('example.this-extension-better-not-map-to-an-actual-mimetype');
const result = json.suites[0].tests[3].results[0];
expect(result.attachments[0].name).toBe('foo');
expect(result.attachments[0].contentType).toBe('application/octet-stream');
const p = result.attachments[0].path;
expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.json$/);
expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.this-extension-better-not-map-to-an-actual-mimetype$/);
const contents = fs.readFileSync(p);
expect(contents.toString()).toBe('We <3 Playwright!');
}
@ -211,32 +194,20 @@ test(`testInfo.attach should save attachments via inline attachment`, async ({ r
const path = require('path');
const fs = require('fs');
const { test } = pwt;
test('infer contentType - string', async ({}, testInfo) => {
await testInfo.attach('We <3 Playwright!', 'example.json');
test('default contentType - string', async ({}, testInfo) => {
await testInfo.attach('example.json', { body: 'We <3 Playwright!' });
});
test('infer contentType - Buffer', async ({}, testInfo) => {
await testInfo.attach(Buffer.from('We <3 Playwright!'), 'example.json');
});
test('fallback contentType - string', async ({}, testInfo) => {
await testInfo.attach('We <3 Playwright!', 'example.this-extension-better-not-map-to-an-actual-mimetype');
});
test('fallback contentType - Buffer', async ({}, testInfo) => {
await testInfo.attach(Buffer.from('We <3 Playwright!'), 'example.this-extension-better-not-map-to-an-actual-mimetype');
});
test('fallback contentType - no extension', async ({}, testInfo) => {
await testInfo.attach('We <3 Playwright!', 'example');
test('default contentType - Buffer', async ({}, testInfo) => {
await testInfo.attach('example.json', { body: Buffer.from('We <3 Playwright!') });
});
test('explicit contentType - string', async ({}, testInfo) => {
await testInfo.attach('We <3 Playwright!', 'example.json', { contentType: 'x-playwright/custom' });
await testInfo.attach('example.json', { body: 'We <3 Playwright!', contentType: 'x-playwright/custom' });
});
test('explicit contentType - Buffer', async ({}, testInfo) => {
await testInfo.attach(Buffer.from('We <3 Playwright!'), 'example.json', { contentType: 'x-playwright/custom' });
await testInfo.attach('example.json', { body: Buffer.from('We <3 Playwright!'), contentType: 'x-playwright/custom' });
});
`,
}, { reporter: 'dot,' + kRawReporterPath, workers: 1 }, {}, { usesCustomOutputDir: true });
@ -244,41 +215,23 @@ test(`testInfo.attach should save attachments via inline attachment`, async ({ r
{
const result = json.suites[0].tests[0].results[0];
expect(result.attachments[0].name).toBe('example.json');
expect(result.attachments[0].contentType).toBe('application/json');
expect(result.attachments[0].contentType).toBe('text/plain');
expect(Buffer.from(result.attachments[0].body, 'base64')).toEqual(Buffer.from('We <3 Playwright!'));
}
{
const result = json.suites[0].tests[1].results[0];
expect(result.attachments[0].name).toBe('example.json');
expect(result.attachments[0].contentType).toBe('application/json');
expect(Buffer.from(result.attachments[0].body, 'base64')).toEqual(Buffer.from('We <3 Playwright!'));
}
{
const result = json.suites[0].tests[2].results[0];
expect(result.attachments[0].name).toBe('example.this-extension-better-not-map-to-an-actual-mimetype');
expect(result.attachments[0].contentType).toBe('text/plain');
expect(Buffer.from(result.attachments[0].body, 'base64')).toEqual(Buffer.from('We <3 Playwright!'));
}
{
const result = json.suites[0].tests[3].results[0];
expect(result.attachments[0].name).toBe('example.this-extension-better-not-map-to-an-actual-mimetype');
expect(result.attachments[0].contentType).toBe('application/octet-stream');
expect(Buffer.from(result.attachments[0].body, 'base64')).toEqual(Buffer.from('We <3 Playwright!'));
}
{
const result = json.suites[0].tests[4].results[0];
expect(result.attachments[0].name).toBe('example');
expect(result.attachments[0].contentType).toBe('text/plain');
expect(Buffer.from(result.attachments[0].body, 'base64')).toEqual(Buffer.from('We <3 Playwright!'));
}
{
const result = json.suites[0].tests[5].results[0];
const result = json.suites[0].tests[2].results[0];
expect(result.attachments[0].name).toBe('example.json');
expect(result.attachments[0].contentType).toBe('x-playwright/custom');
expect(Buffer.from(result.attachments[0].body, 'base64')).toEqual(Buffer.from('We <3 Playwright!'));
}
{
const result = json.suites[0].tests[6].results[0];
const result = json.suites[0].tests[3].results[0];
expect(result.attachments[0].name).toBe('example.json');
expect(result.attachments[0].contentType).toBe('x-playwright/custom');
expect(Buffer.from(result.attachments[0].body, 'base64')).toEqual(Buffer.from('We <3 Playwright!'));

View File

@ -204,8 +204,7 @@ export interface TestInfo {
timeout: number;
annotations: { type: string, description?: string }[];
attachments: { name: string, path?: string, body?: Buffer, contentType: string }[];
attach(path: string, options?: { contentType?: string, name?: string}): Promise<void>;
attach(body: string | Buffer, name: string, options?: { contentType?: string }): Promise<void>;
attach(name: string, options?: { contentType?: string, path?: string, body?: string | Buffer }): Promise<void>;
repeatEachIndex: number;
retry: number;
duration: number;