feat(testinfo): add name to attachment output name (#18440)

Per discussion in #12950, adding sanitized name to the output filename
prefix. This can make debugging easier, and the filename structure more
human friendly.
This commit is contained in:
Xiaoxing Ye 2022-11-04 04:54:51 +08:00 committed by GitHub
parent 8538f61a72
commit 9338355e47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 8 deletions

View File

@ -104,7 +104,8 @@ after awaiting the attach call.
* since: v1.10 * since: v1.10
- `name` <[string]> - `name` <[string]>
Attachment name. Attachment name. The name will also be sanitized and used as the prefix of file name
when saving to disk.
### option: TestInfo.attach.body ### option: TestInfo.attach.body
* since: v1.10 * since: v1.10

View File

@ -21,7 +21,7 @@ import path from 'path';
import url from 'url'; import url from 'url';
import { colors, debug, minimatch } from 'playwright-core/lib/utilsBundle'; import { colors, debug, minimatch } from 'playwright-core/lib/utilsBundle';
import type { TestError, Location } from './types'; import type { TestError, Location } from './types';
import { calculateSha1, isRegExp } from 'playwright-core/lib/utils'; import { calculateSha1, isRegExp, isString } from 'playwright-core/lib/utils';
import { isInternalFileName } from 'playwright-core/lib/utils/stackTrace'; import { isInternalFileName } from 'playwright-core/lib/utils/stackTrace';
import { currentTestInfo } from './globals'; import { currentTestInfo } from './globals';
import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace'; import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
@ -287,7 +287,12 @@ export async function normalizeAndSaveAttachment(outputPath: string, name: strin
throw new Error(`Exactly one of "path" and "body" must be specified`); throw new Error(`Exactly one of "path" and "body" must be specified`);
if (options.path !== undefined) { if (options.path !== undefined) {
const hash = calculateSha1(options.path); const hash = calculateSha1(options.path);
const dest = path.join(outputPath, 'attachments', hash + path.extname(options.path));
if (!isString(name))
throw new Error('"name" should be string.');
const sanitizedNamePrefix = sanitizeForFilePath(name) + '-';
const dest = path.join(outputPath, 'attachments', sanitizedNamePrefix + hash + path.extname(options.path));
await fs.promises.mkdir(path.dirname(dest), { recursive: true }); await fs.promises.mkdir(path.dirname(dest), { recursive: true });
await fs.promises.copyFile(options.path, dest); await fs.promises.copyFile(options.path, dest);
const contentType = options.contentType ?? (mime.getType(path.basename(options.path)) || 'application/octet-stream'); const contentType = options.contentType ?? (mime.getType(path.basename(options.path)) || 'application/octet-stream');

View File

@ -1458,7 +1458,7 @@ export interface TestInfo {
* > NOTE: [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach) * > 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 * 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. * the attachment after awaiting the attach call.
* @param name Attachment name. * @param name Attachment name. The name will also be sanitized and used as the prefix of file name when saving to disk.
* @param options * @param options
*/ */
attach(name: string, options?: { attach(name: string, options?: {

View File

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import path from 'path';
import { test, expect, stripAnsi } from './playwright-test-fixtures'; import { test, expect, stripAnsi } from './playwright-test-fixtures';
test('render text attachment', async ({ runInlineTest }) => { test('render text attachment', async ({ runInlineTest }) => {
@ -186,3 +187,94 @@ test(`testInfo.attach allow empty buffer body`, async ({ runInlineTest }) => {
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(stripAnsi(result.output)).toMatch(/^.*attachment #1: name \(text\/plain\).*\n.*\n.*------/gm); expect(stripAnsi(result.output)).toMatch(/^.*attachment #1: name \(text\/plain\).*\n.*\n.*------/gm);
}); });
test(`testInfo.attach use name as prefix`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.js': `
const test = pwt.test.extend({
fixture: async ({}, use, testInfo) => {
const filePath = testInfo.outputPath('foo.txt');
require('fs').writeFileSync(filePath, 'hello');
await use();
await testInfo.attach('some random string', { path: filePath });
},
});
test('success', async ({ fixture }) => {
expect(true).toBe(false);
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
expect(stripAnsi(result.output)).toContain('attachment #1: some random string (text/plain)');
expect(stripAnsi(result.output)).toContain('some-random-string-');
});
test(`testInfo.attach name should be sanitized`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.js': `
const test = pwt.test.extend({
fixture: async ({}, use, testInfo) => {
const filePath = testInfo.outputPath('foo.txt');
require('fs').writeFileSync(filePath, 'hello');
await use();
await testInfo.attach('../../../test', { path: filePath });
},
});
test('success', async ({ fixture }) => {
expect(true).toBe(false);
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
expect(stripAnsi(result.output)).toContain('attachment #1: ../../../test (text/plain)');
expect(stripAnsi(result.output)).toContain(`attachments${path.sep}-test`);
});
test(`testInfo.attach name can be empty string`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.js': `
const test = pwt.test.extend({
fixture: async ({}, use, testInfo) => {
const filePath = testInfo.outputPath('foo.txt');
require('fs').writeFileSync(filePath, 'hello');
await use();
await testInfo.attach('', { path: filePath });
},
});
test('success', async ({ fixture }) => {
expect(true).toBe(false);
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
expect(stripAnsi(result.output)).toContain('attachment #1: (text/plain)');
expect(stripAnsi(result.output)).toContain(`attachments${path.sep}-`);
});
test(`testInfo.attach throw if name is not string`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.js': `
const test = pwt.test.extend({
fixture: async ({}, use, testInfo) => {
const filePath = testInfo.outputPath('foo.txt');
require('fs').writeFileSync(filePath, 'hello');
await use();
await testInfo.attach(false, { path: filePath });
},
});
test('success', async ({ fixture }) => {
expect(true).toBe(true);
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
expect(stripAnsi(result.output)).toContain('"name" should be string.');
});

View File

@ -155,7 +155,7 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
expect(result.attachments[0].name).toBe('foo'); expect(result.attachments[0].name).toBe('foo');
expect(result.attachments[0].contentType).toBe('application/json'); expect(result.attachments[0].contentType).toBe('application/json');
const p = result.attachments[0].path; const p = result.attachments[0].path;
expect(p).toMatch(/[/\\]attachments[/\\][0-9a-f]+\.json$/); expect(p).toMatch(/[/\\]attachments[/\\]foo-[0-9a-f]+\.json$/);
const contents = fs.readFileSync(p); const contents = fs.readFileSync(p);
expect(contents.toString()).toBe('We <3 Playwright!'); expect(contents.toString()).toBe('We <3 Playwright!');
} }
@ -164,7 +164,7 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
expect(result.attachments[0].name).toBe('foo'); expect(result.attachments[0].name).toBe('foo');
expect(result.attachments[0].contentType).toBe('image/png'); expect(result.attachments[0].contentType).toBe('image/png');
const p = result.attachments[0].path; const p = result.attachments[0].path;
expect(p).toMatch(/[/\\]attachments[/\\][0-9a-f]+\.json$/); expect(p).toMatch(/[/\\]attachments[/\\]foo-[0-9a-f]+\.json$/);
const contents = fs.readFileSync(p); const contents = fs.readFileSync(p);
expect(contents.toString()).toBe('We <3 Playwright!'); expect(contents.toString()).toBe('We <3 Playwright!');
} }
@ -173,7 +173,7 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
expect(result.attachments[0].name).toBe('example.png'); expect(result.attachments[0].name).toBe('example.png');
expect(result.attachments[0].contentType).toBe('x-playwright/custom'); expect(result.attachments[0].contentType).toBe('x-playwright/custom');
const p = result.attachments[0].path; const p = result.attachments[0].path;
expect(p).toMatch(/[/\\]attachments[/\\][0-9a-f]+\.json$/); expect(p).toMatch(/[/\\]attachments[/\\]example-png-[0-9a-f]+\.json$/);
const contents = fs.readFileSync(p); const contents = fs.readFileSync(p);
expect(contents.toString()).toBe('We <3 Playwright!'); expect(contents.toString()).toBe('We <3 Playwright!');
} }
@ -182,7 +182,7 @@ test(`testInfo.attach should save attachments via path`, async ({ runInlineTest
expect(result.attachments[0].name).toBe('foo'); expect(result.attachments[0].name).toBe('foo');
expect(result.attachments[0].contentType).toBe('application/octet-stream'); expect(result.attachments[0].contentType).toBe('application/octet-stream');
const p = result.attachments[0].path; const p = result.attachments[0].path;
expect(p).toMatch(/[/\\]attachments[/\\][0-9a-f]+\.this-extension-better-not-map-to-an-actual-mimetype$/); expect(p).toMatch(/[/\\]attachments[/\\]foo-[0-9a-f]+\.this-extension-better-not-map-to-an-actual-mimetype$/);
const contents = fs.readFileSync(p); const contents = fs.readFileSync(p);
expect(contents.toString()).toBe('We <3 Playwright!'); expect(contents.toString()).toBe('We <3 Playwright!');
} }