From 8ecf90b52c041ca1ad97e1fb8dcaf8df238da47f Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Mon, 8 Nov 2021 00:40:01 -0800 Subject: [PATCH 01/10] feat(api): add explicit async testInfo.attach We add an explicit async API for attaching file paths (and Buffers) to tests that can be awaited to help users ensure they are attaching files that actually exist at both the time of the invocation and later when reporters (like the HTML Reporter) run and package up test artifacts. This is intended to help surface attachment issues as soon as possible so you aren't silently left with a missing attachment minutes/days/months later when you go to debug a suddenly breaking test expecting an attachment to be there. NB: The current implemntation incurs an extra file copy compared to manipulating the raw attachments array. If users encounter performance issues because of this, we can consider an option parameter that uses rename under the hood instead of copy. However, that would need to be used with care if the file were to be accessed later in the test. Resolves #10113 --- docs/src/test-api/class-testinfo.md | 72 ++++++++- packages/playwright-test/src/workerRunner.ts | 39 ++++- packages/playwright-test/types/test.d.ts | 48 +++++- .../reporter-attachment.spec.ts | 33 ++++ tests/playwright-test/reporter-raw.spec.ts | 145 ++++++++++++++++++ utils/generate_types/overrides-test.d.ts | 2 + 6 files changed, 331 insertions(+), 8 deletions(-) diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index 4bb3d010ae4e1..00b29e641afc7 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -38,7 +38,14 @@ Learn more about [test annotations](./test-annotations.md). - `path` <[void]|[string]> Optional path on the filesystem to the attached file. - `body` <[void]|[Buffer]> Optional attachment body used instead of a file. -The list of files or buffers attached to the current test. Some reporters show test attachments. For example, you can attach a screenshot to the test. +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`]. + +## 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. + +For example, you can attach a screenshot to the test: ```js js-flavor=js const { test, expect } = require('@playwright/test'); @@ -49,7 +56,11 @@ test('basic test', async ({ page }, testInfo) => { // Capture a screenshot and attach it. const path = testInfo.outputPath('screenshot.png'); await page.screenshot({ path }); - testInfo.attachments.push({ name: 'screenshot', path, contentType: 'image/png' }); + 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' }); }); ``` @@ -62,10 +73,65 @@ test('basic test', async ({ page }, testInfo) => { // Capture a screenshot and attach it. const path = testInfo.outputPath('screenshot.png'); await page.screenshot({ path }); - testInfo.attachments.push({ name: 'screenshot', path, contentType: 'image/png' }); + 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' }); +}); +``` + +Or you can attach files returned by your APIs: + +```js js-flavor=js +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' }); +}); +``` + +```js js-flavor=ts +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' }); }); ``` +:::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 +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 the [`param: name`] (if specified) or [`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 + +Similar to [`method: TestInfo.attach#1`] but attaches inline `strings` or `Buffers` instead of files from disk. + +### param: TestInfo.attach#2.body +- `body` <[string]|[Buffer]> Attachment body. + +### param: TestInfo.attach#2.name +- `name` <[string]> Attachment name. + +### param: 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`). + ## property: TestInfo.column - type: <[int]> diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index 61e5e1238367c..240bc5e8d375e 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -15,8 +15,9 @@ */ import fs from 'fs'; -import path from 'path'; +import path, { basename } from 'path'; import rimraf from 'rimraf'; +import * as mime from 'mime'; import util from 'util'; import colors from 'colors/safe'; import { EventEmitter } from 'events'; @@ -29,6 +30,8 @@ import { Annotations, TestError, TestInfo, TestInfoImpl, TestStepInternal, Worke import { ProjectImpl } from './project'; import { FixturePool, FixtureRunner } from './fixtures'; import { DeadlineRunner, raceAgainstDeadline } from 'playwright-core/lib/utils/async'; +import { calculateFileSha1 } from 'playwright-core/lib/utils/utils'; +// import { calculateFileSha1 } from 'playwright-core/lib/utils/utils'; const removeFolderAsync = util.promisify(rimraf); @@ -264,6 +267,40 @@ export class WorkerRunner extends EventEmitter { expectedStatus: test.expectedStatus, annotations: [], attachments: [], + attach: async (...args) => { + const [ pathOrBody, nameOrOptions, contentType ] = args as [string | Buffer, string | { contentType?: string, name?: string} | undefined, string | undefined ]; + let attachment: { name: string, contentType: string, body?: Buffer, path?: string } | undefined; + if (typeof nameOrOptions === 'string') { // inline attachment + const body = pathOrBody; + const name = nameOrOptions; + + attachment = { + name, + contentType: 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 = nameOrOptions; + const path = pathOrBody as string; + const name = options?.name ?? basename(path); + attachment = { + name, + path, + 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)); + await fs.promises.mkdir(path.dirname(dest), { recursive: true }); + await fs.promises.copyFile(attachment.path, dest); + tmpAttachment.path = dest; + } + + testInfo.attachments.push(tmpAttachment); + }, duration: 0, status: 'passed', stdout: [], diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index c2d9d80497dc0..c19f80e8c9b9f 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -1298,8 +1298,19 @@ export interface TestInfo { */ annotations: { type: string, description?: string }[]; /** - * The list of files or buffers attached to the current test. Some reporters show test attachments. For example, you can - * attach a screenshot to the test. + * 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). + */ + 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. + * + * For example, you can attach a screenshot to the test: * * ```ts * import { test, expect } from '@playwright/test'; @@ -1310,12 +1321,41 @@ export interface TestInfo { * // Capture a screenshot and attach it. * const path = testInfo.outputPath('screenshot.png'); * await page.screenshot({ path }); - * testInfo.attachments.push({ name: 'screenshot', path, contentType: 'image/png' }); + * 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' }); * }); * ``` * + * Or you can attach files returned by your APIs: + * + * ```ts + * 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' }); + * }); + * ``` + * + * > 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 */ - attachments: { name: string, path?: string, body?: Buffer, contentType: string }[]; + attach(path: string, options?: { contentType?: string, name?: string}): Promise; + /** + * Similar to [testInfo.attach(path[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach-1) but + * attaches inline `strings` or `Buffers` instead of files from disk. + * @param body + * @param name + * @param contentType + */ + attach(body: string | Buffer, name: string, contentType?: string): Promise; /** * 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). diff --git a/tests/playwright-test/reporter-attachment.spec.ts b/tests/playwright-test/reporter-attachment.spec.ts index 9e50a3c891c44..6b114d57488aa 100644 --- a/tests/playwright-test/reporter-attachment.spec.ts +++ b/tests/playwright-test/reporter-attachment.spec.ts @@ -79,3 +79,36 @@ test('render trace attachment', async ({ runInlineTest }) => { expect(text).toContain(' ------------------------------------------------------------------------------------------------'); expect(result.exitCode).toBe(1); }); + +for (const { description, apiCall } of [ + { + description: 'all options specified', + apiCall: `attach('non-existent-path', { contentType: 'text/plain', name: 'foo.txt'})`, + }, + { + description: 'no options specified', + apiCall: `attach('non-existent-path')`, + }, + { + description: 'partial options - contentType', + apiCall: `attach('non-existent-path', { contentType: 'text/plain'})`, + }, + { + description: 'partial options - name', + apiCall: `attach('non-existent-path', { name: 'foo.txt'})`, + }, +]) { + test(`testInfo.attach throws an error when attaching a non-existent - ${description}`, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = pwt; + test('one', async ({}, testInfo) => { + await testInfo.${apiCall}; + }); + `, + }, { reporter: 'line' }); + const text = stripAscii(result.output).replace(/\\/g, '/'); + expect(text).toMatch(/Error: ENOENT: no such file or directory, open '.*non-existent-path.*'/); + expect(result.exitCode).toBe(1); + }); +} diff --git a/tests/playwright-test/reporter-raw.spec.ts b/tests/playwright-test/reporter-raw.spec.ts index e6a492a48db47..4ee5bde498b6e 100644 --- a/tests/playwright-test/reporter-raw.spec.ts +++ b/tests/playwright-test/reporter-raw.spec.ts @@ -110,6 +110,151 @@ test('should save attachments', async ({ runInlineTest }, testInfo) => { expect(path2).toBe('dummy-path'); }); +for (const { description, apiCall, expected } of [ + { + description: 'infer contentType from path', + apiCall: `attach(tmpPath)`, + expected: { + contentType: 'application/json', + name: 'example.json', + }, + }, + { + description: 'infer contentType from name (over extension)', + apiCall: `attach(tmpPath, { name: 'example.png' })`, + expected: { + contentType: 'image/png', + name: 'example.png', + }, + }, + { + description: 'explicit contentType (over extension)', + apiCall: `attach(tmpPath, { contentType: 'image/png' })`, + expected: { + contentType: 'image/png', + name: 'example.json', + }, + }, + { + description: 'explicit contentType (over extension and name)', + apiCall: `attach(tmpPath, { name: 'example.png', contentType: 'x-playwright/custom' })`, + expected: { + contentType: 'x-playwright/custom', + name: 'example.png', + }, + }, + { + description: 'fallback contentType', + apiCall: `attach(tmpPath, { name: 'example.this-extension-better-not-map-to-an-actual-mimetype' })`, + expected: { + contentType: 'application/octet-stream', + name: 'example.this-extension-better-not-map-to-an-actual-mimetype', + }, + }, +]) { + test(`testInfo.attach should save attachments via path - ${description}`, async ({ runInlineTest }, testInfo) => { + await runInlineTest({ + 'a.test.js': ` + const path = require('path'); + const fs = require('fs'); + const { test } = pwt; + test('passes', async ({}, testInfo) => { + const tmpPath = testInfo.outputPath('example.json'); + await fs.promises.writeFile(tmpPath, 'We <3 Playwright!'); + await testInfo.${apiCall}; + // Forcibly remove the tmp file to ensure attach is actually automagically copying it + await fs.promises.unlink(tmpPath); + }); + `, + }, { reporter: 'dot,' + kRawReporterPath }, {}, { usesCustomOutputDir: true }); + 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(expected.name); + expect(result.attachments[0].contentType).toBe(expected.contentType); + const p = result.attachments[0].path; + expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.json$/); + const contents = fs.readFileSync(p); + expect(contents.toString()).toBe('We <3 Playwright!'); + }); +} + +for (const { description, apiCall, expected } of [ + { + description: 'infer contentType - string', + apiCall: `attach('We <3 Playwright!', 'example.json')`, + expected: { + contentType: 'application/json', + name: 'example.json', + }, + }, + { + description: 'infer contentType - Buffer', + apiCall: `attach(Buffer.from('We <3 Playwright!'), 'example.json')`, + expected: { + contentType: 'application/json', + name: 'example.json', + }, + }, + { + description: 'fallback contentType - string', + apiCall: `attach('We <3 Playwright!', 'example.this-extension-better-not-map-to-an-actual-mimetype')`, + expected: { + contentType: 'text/plain', + name: 'example.this-extension-better-not-map-to-an-actual-mimetype', + }, + }, + { + description: 'fallback contentType - Buffer', + apiCall: `attach(Buffer.from('We <3 Playwright!'), 'example.this-extension-better-not-map-to-an-actual-mimetype')`, + expected: { + contentType: 'application/octet-stream', + name: 'example.this-extension-better-not-map-to-an-actual-mimetype', + }, + }, + { + description: 'fallback contentType - no extension', + apiCall: `attach('We <3 Playwright!', 'example')`, + expected: { + contentType: 'text/plain', + name: 'example', + }, + }, + { + description: 'explicit contentType - string', + apiCall: `attach('We <3 Playwright!', 'example.json', 'x-playwright/custom')`, + expected: { + contentType: 'x-playwright/custom', + name: 'example.json', + }, + }, + { + description: 'explicit contentType - Buffer', + apiCall: `attach(Buffer.from('We <3 Playwright!'), 'example.json', 'x-playwright/custom')`, + expected: { + contentType: 'x-playwright/custom', + name: 'example.json', + }, + }, +]) { + test(`testInfo.attach should save attachments via inline attachment - ${description}`, async ({ runInlineTest }, testInfo) => { + await runInlineTest({ + 'a.test.js': ` + const path = require('path'); + const fs = require('fs'); + const { test } = pwt; + test('passes', async ({}, testInfo) => { + await testInfo.${apiCall}; + }); + `, + }, { reporter: 'dot,' + kRawReporterPath }, {}, { usesCustomOutputDir: true }); + 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(expected.name); + expect(result.attachments[0].contentType).toBe(expected.contentType); + expect(Buffer.from(result.attachments[0].body, 'base64')).toEqual(Buffer.from('We <3 Playwright!')); + }); +} + test('dupe project names', async ({ runInlineTest }, testInfo) => { await runInlineTest({ 'playwright.config.ts': ` diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 610a1493a1904..32af552dcb27f 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -207,6 +207,8 @@ 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; + attach(body: string | Buffer, name: string, contentType?: string): Promise; repeatEachIndex: number; retry: number; duration: number; From f2edab447d101b1d82c465b094c8bf18f1ca9fc0 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Thu, 18 Nov 2021 11:22:16 -0800 Subject: [PATCH 02/10] explicit tests instead of parameterized --- .../reporter-attachment.spec.ts | 63 ++-- tests/playwright-test/reporter-raw.spec.ts | 270 ++++++++++-------- 2 files changed, 181 insertions(+), 152 deletions(-) diff --git a/tests/playwright-test/reporter-attachment.spec.ts b/tests/playwright-test/reporter-attachment.spec.ts index 6b114d57488aa..6b23eacfea73a 100644 --- a/tests/playwright-test/reporter-attachment.spec.ts +++ b/tests/playwright-test/reporter-attachment.spec.ts @@ -80,35 +80,34 @@ test('render trace attachment', async ({ runInlineTest }) => { expect(result.exitCode).toBe(1); }); -for (const { description, apiCall } of [ - { - description: 'all options specified', - apiCall: `attach('non-existent-path', { contentType: 'text/plain', name: 'foo.txt'})`, - }, - { - description: 'no options specified', - apiCall: `attach('non-existent-path')`, - }, - { - description: 'partial options - contentType', - apiCall: `attach('non-existent-path', { contentType: 'text/plain'})`, - }, - { - description: 'partial options - name', - apiCall: `attach('non-existent-path', { name: 'foo.txt'})`, - }, -]) { - test(`testInfo.attach throws an error when attaching a non-existent - ${description}`, async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'a.test.js': ` - const { test } = pwt; - test('one', async ({}, testInfo) => { - await testInfo.${apiCall}; - }); - `, - }, { reporter: 'line' }); - const text = stripAscii(result.output).replace(/\\/g, '/'); - expect(text).toMatch(/Error: ENOENT: no such file or directory, open '.*non-existent-path.*'/); - expect(result.exitCode).toBe(1); - }); -} + +test(`testInfo.attach throws an error when attaching a non-existent attachment`, 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('no options specified', async ({}, testInfo) => { + await testInfo.attach('non-existent-path-no-options'); + }); + + 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'}); + }); + `, + }, { 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(result.passed).toBe(0); + expect(result.failed).toBe(4); + expect(result.exitCode).toBe(1); +}); diff --git a/tests/playwright-test/reporter-raw.spec.ts b/tests/playwright-test/reporter-raw.spec.ts index 4ee5bde498b6e..edb107591b1b7 100644 --- a/tests/playwright-test/reporter-raw.spec.ts +++ b/tests/playwright-test/reporter-raw.spec.ts @@ -110,150 +110,180 @@ test('should save attachments', async ({ runInlineTest }, testInfo) => { expect(path2).toBe('dummy-path'); }); -for (const { description, apiCall, expected } of [ - { - description: 'infer contentType from path', - apiCall: `attach(tmpPath)`, - expected: { - contentType: 'application/json', - name: 'example.json', - }, - }, - { - description: 'infer contentType from name (over extension)', - apiCall: `attach(tmpPath, { name: 'example.png' })`, - expected: { - contentType: 'image/png', - name: 'example.png', - }, - }, - { - description: 'explicit contentType (over extension)', - apiCall: `attach(tmpPath, { contentType: 'image/png' })`, - expected: { - contentType: 'image/png', - name: 'example.json', - }, - }, - { - description: 'explicit contentType (over extension and name)', - apiCall: `attach(tmpPath, { name: 'example.png', contentType: 'x-playwright/custom' })`, - expected: { - contentType: 'x-playwright/custom', - name: 'example.png', - }, - }, - { - description: 'fallback contentType', - apiCall: `attach(tmpPath, { name: 'example.this-extension-better-not-map-to-an-actual-mimetype' })`, - expected: { - contentType: 'application/octet-stream', - name: 'example.this-extension-better-not-map-to-an-actual-mimetype', - }, - }, -]) { - test(`testInfo.attach should save attachments via path - ${description}`, async ({ runInlineTest }, testInfo) => { - await runInlineTest({ - 'a.test.js': ` +test(`testInfo.attach should save attachments via path`, async ({ runInlineTest }, testInfo) => { + await runInlineTest({ + 'a.test.js': ` const path = require('path'); const fs = require('fs'); const { test } = pwt; - test('passes', async ({}, testInfo) => { + 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' }); + // Forcibly remove the tmp file to ensure attach is actually automagically copying it + await fs.promises.unlink(tmpPath); + }); + + test('explicit contentType (over extension)', async ({}, testInfo) => { const tmpPath = testInfo.outputPath('example.json'); await fs.promises.writeFile(tmpPath, 'We <3 Playwright!'); - await testInfo.${apiCall}; + await testInfo.attach(tmpPath, { contentType: 'image/png' }); + // Forcibly remove the tmp file to ensure attach is actually automagically copying it + await fs.promises.unlink(tmpPath); + }); + + 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' }); + // 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'); + await fs.promises.writeFile(tmpPath, 'We <3 Playwright!'); + await testInfo.attach(tmpPath, { name: 'example.this-extension-better-not-map-to-an-actual-mimetype' }); // Forcibly remove the tmp file to ensure attach is actually automagically copying it await fs.promises.unlink(tmpPath); }); `, - }, { reporter: 'dot,' + kRawReporterPath }, {}, { usesCustomOutputDir: true }); - const json = JSON.parse(fs.readFileSync(testInfo.outputPath('test-results', 'report', 'project.report'), 'utf-8')); + }, { reporter: 'dot,' + kRawReporterPath, workers: 1 }, {}, { usesCustomOutputDir: true }); + 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(expected.name); - expect(result.attachments[0].contentType).toBe(expected.contentType); + expect(result.attachments[0].name).toBe('example.json'); + expect(result.attachments[0].contentType).toBe('application/json'); const p = result.attachments[0].path; expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.json$/); const contents = fs.readFileSync(p); expect(contents.toString()).toBe('We <3 Playwright!'); - }); -} - -for (const { description, apiCall, expected } of [ - { - description: 'infer contentType - string', - apiCall: `attach('We <3 Playwright!', 'example.json')`, - expected: { - contentType: 'application/json', - name: 'example.json', - }, - }, - { - description: 'infer contentType - Buffer', - apiCall: `attach(Buffer.from('We <3 Playwright!'), 'example.json')`, - expected: { - contentType: 'application/json', - name: 'example.json', - }, - }, + } { - description: 'fallback contentType - string', - apiCall: `attach('We <3 Playwright!', 'example.this-extension-better-not-map-to-an-actual-mimetype')`, - expected: { - contentType: 'text/plain', - name: 'example.this-extension-better-not-map-to-an-actual-mimetype', - }, - }, - { - description: 'fallback contentType - Buffer', - apiCall: `attach(Buffer.from('We <3 Playwright!'), 'example.this-extension-better-not-map-to-an-actual-mimetype')`, - expected: { - contentType: 'application/octet-stream', - name: 'example.this-extension-better-not-map-to-an-actual-mimetype', - }, - }, + const result = json.suites[0].tests[1].results[0]; + expect(result.attachments[0].name).toBe('example.png'); + 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!'); + } { - description: 'fallback contentType - no extension', - apiCall: `attach('We <3 Playwright!', 'example')`, - expected: { - contentType: 'text/plain', - name: 'example', - }, - }, + 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!'); + } { - description: 'explicit contentType - string', - apiCall: `attach('We <3 Playwright!', 'example.json', 'x-playwright/custom')`, - expected: { - contentType: 'x-playwright/custom', - name: 'example.json', - }, - }, + 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; + expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.json$/); + const contents = fs.readFileSync(p); + expect(contents.toString()).toBe('We <3 Playwright!'); + } { - description: 'explicit contentType - Buffer', - apiCall: `attach(Buffer.from('We <3 Playwright!'), 'example.json', 'x-playwright/custom')`, - expected: { - contentType: 'x-playwright/custom', - name: 'example.json', - }, - }, -]) { - test(`testInfo.attach should save attachments via inline attachment - ${description}`, async ({ runInlineTest }, testInfo) => { - await runInlineTest({ - 'a.test.js': ` + 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'); + expect(result.attachments[0].contentType).toBe('application/octet-stream'); + const p = result.attachments[0].path; + expect(p).toMatch(/[/\\]attachments[/\\]01a5667d100fac2200bf40cf43083fae0580c58e\.json$/); + const contents = fs.readFileSync(p); + expect(contents.toString()).toBe('We <3 Playwright!'); + } +}); + +test(`testInfo.attach should save attachments via inline attachment`, async ({ runInlineTest }, testInfo) => { + await runInlineTest({ + 'a.test.js': ` const path = require('path'); const fs = require('fs'); const { test } = pwt; - test('passes', async ({}, testInfo) => { - await testInfo.${apiCall}; + test('infer contentType - string', async ({}, testInfo) => { + await testInfo.attach('We <3 Playwright!', 'example.json'); }); - `, - }, { reporter: 'dot,' + kRawReporterPath }, {}, { usesCustomOutputDir: true }); - const json = JSON.parse(fs.readFileSync(testInfo.outputPath('test-results', 'report', 'project.report'), 'utf-8')); + + 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('explicit contentType - string', async ({}, testInfo) => { + await testInfo.attach('We <3 Playwright!', 'example.json', 'x-playwright/custom'); + }); + + test('explicit contentType - Buffer', async ({}, testInfo) => { + await testInfo.attach(Buffer.from('We <3 Playwright!'), 'example.json', 'x-playwright/custom'); + }); + `, + }, { reporter: 'dot,' + kRawReporterPath, workers: 1 }, {}, { usesCustomOutputDir: true }); + 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(expected.name); - expect(result.attachments[0].contentType).toBe(expected.contentType); + 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[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]; + 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]; + 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!')); + } +}); test('dupe project names', async ({ runInlineTest }, testInfo) => { await runInlineTest({ From 39323be0f2c709ade06b8d7613c0936badeef65a Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Thu, 18 Nov 2021 11:25:32 -0800 Subject: [PATCH 03/10] Update docs/src/test-api/class-testinfo.md Co-authored-by: Dmitry Gozman --- docs/src/test-api/class-testinfo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index 00b29e641afc7..78e7a3ef9c5cd 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -113,7 +113,7 @@ after awaiting the attach call. - `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 the [`param: name`] (if specified) or [`param: path`]. +- `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. From 08b495a741df95e1b4dceca81b84f6b40cce8fb3 Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Thu, 18 Nov 2021 11:25:39 -0800 Subject: [PATCH 04/10] Update docs/src/test-api/class-testinfo.md Co-authored-by: Dmitry Gozman --- docs/src/test-api/class-testinfo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index 78e7a3ef9c5cd..8e008e1c16ff5 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -121,7 +121,7 @@ after awaiting the attach call. ## method: TestInfo.attach#2 -Similar to [`method: TestInfo.attach#1`] but attaches inline `strings` or `Buffers` instead of files from disk. +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. From 27bc5be15e16ecc6ff42fdac33740174ec4779ef Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Thu, 18 Nov 2021 11:28:55 -0800 Subject: [PATCH 05/10] address review feedback --- packages/playwright-test/src/workerRunner.ts | 9 ++++----- packages/playwright-test/types/test.d.ts | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index 240bc5e8d375e..a1ef055536a80 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -15,7 +15,7 @@ */ import fs from 'fs'; -import path, { basename } from 'path'; +import path from 'path'; import rimraf from 'rimraf'; import * as mime from 'mime'; import util from 'util'; @@ -31,7 +31,6 @@ import { ProjectImpl } from './project'; import { FixturePool, FixtureRunner } from './fixtures'; import { DeadlineRunner, raceAgainstDeadline } from 'playwright-core/lib/utils/async'; import { calculateFileSha1 } from 'playwright-core/lib/utils/utils'; -// import { calculateFileSha1 } from 'playwright-core/lib/utils/utils'; const removeFolderAsync = util.promisify(rimraf); @@ -281,11 +280,11 @@ export class WorkerRunner extends EventEmitter { }; } else { // path based attachment const options = nameOrOptions; - const path = pathOrBody as string; - const name = options?.name ?? basename(path); + const thePath = pathOrBody as string; + const name = options?.name ?? path.basename(thePath); attachment = { name, - path, + path: thePath, contentType: options?.contentType ?? (mime.getType(name) || 'application/octet-stream') }; } diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index c19f80e8c9b9f..850308a530a7d 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -1349,8 +1349,7 @@ export interface TestInfo { */ attach(path: string, options?: { contentType?: string, name?: string}): Promise; /** - * Similar to [testInfo.attach(path[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach-1) but - * attaches inline `strings` or `Buffers` instead of files from disk. + * Attach data to the current test, either a `string` or a `Buffer`. Some reporters show test attachments. * @param body * @param name * @param contentType From d6fc5f3f9dd5b7ccfc6621a1b8aad1f0e23f29c1 Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Mon, 22 Nov 2021 13:49:57 -0800 Subject: [PATCH 06/10] Update docs/src/test-api/class-testinfo.md Co-authored-by: Dmitry Gozman --- docs/src/test-api/class-testinfo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index 8e008e1c16ff5..61640a9de9b43 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -57,7 +57,7 @@ test('basic test', async ({ page }, testInfo) => { const path = testInfo.outputPath('screenshot.png'); await page.screenshot({ path }); await testInfo.attach(path); - // optionally override the name + // 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' }); From cf80e71c48df2da6887d83e6c7cceba9b913210f Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Mon, 22 Nov 2021 13:50:07 -0800 Subject: [PATCH 07/10] Update docs/src/test-api/class-testinfo.md Co-authored-by: Dmitry Gozman --- docs/src/test-api/class-testinfo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index 61640a9de9b43..f601f90be7ce3 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -74,7 +74,7 @@ test('basic test', async ({ page }, testInfo) => { const path = testInfo.outputPath('screenshot.png'); await page.screenshot({ path }); await testInfo.attach(path); - // optionally override the name + // 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' }); From 6045289f6fab8761ff9e76ec6786a8ab86dc4fae Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Mon, 22 Nov 2021 13:50:12 -0800 Subject: [PATCH 08/10] Update docs/src/test-api/class-testinfo.md Co-authored-by: Dmitry Gozman --- docs/src/test-api/class-testinfo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index f601f90be7ce3..3c277a89f23f2 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -59,7 +59,7 @@ test('basic test', async ({ page }, testInfo) => { await testInfo.attach(path); // Optionally override the name. await testInfo.attach(path, { name: 'example.png' }); - // optionally override the contentType + // Optionally override the contentType. await testInfo.attach(path, { name: 'example.custom-file', contentType: 'x-custom-content-type' }); }); ``` From 1617310ea4b5b1b9de219a093a094ee70e53a781 Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Mon, 22 Nov 2021 13:50:17 -0800 Subject: [PATCH 09/10] Update docs/src/test-api/class-testinfo.md Co-authored-by: Dmitry Gozman --- docs/src/test-api/class-testinfo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index 3c277a89f23f2..42a1764ede9dd 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -76,7 +76,7 @@ test('basic test', async ({ page }, testInfo) => { await testInfo.attach(path); // Optionally override the name. await testInfo.attach(path, { name: 'example.png' }); - // optionally override the contentType + // Optionally override the contentType. await testInfo.attach(path, { name: 'example.custom-file', contentType: 'x-custom-content-type' }); }); ``` From f6c07247df5ff305299051610148cca257f3509b Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Mon, 22 Nov 2021 14:11:53 -0800 Subject: [PATCH 10/10] move contentType to formal option --- docs/src/test-api/class-testinfo.md | 2 +- packages/playwright-test/src/workerRunner.ts | 10 +++++----- packages/playwright-test/types/test.d.ts | 8 ++++---- tests/playwright-test/reporter-raw.spec.ts | 4 ++-- utils/generate_types/overrides-test.d.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index 42a1764ede9dd..681aab2495c4f 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -129,7 +129,7 @@ Attach data to the current test, either a `string` or a `Buffer`. Some reporters ### param: TestInfo.attach#2.name - `name` <[string]> Attachment name. -### param: TestInfo.attach#2.contentType +### 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`). ## property: TestInfo.column diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index a1ef055536a80..de69eb77fc0bf 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -267,19 +267,19 @@ export class WorkerRunner extends EventEmitter { annotations: [], attachments: [], attach: async (...args) => { - const [ pathOrBody, nameOrOptions, contentType ] = args as [string | Buffer, string | { contentType?: string, name?: string} | undefined, string | undefined ]; + 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 nameOrOptions === 'string') { // inline attachment + if (typeof nameOrFileOptions === 'string') { // inline attachment const body = pathOrBody; - const name = nameOrOptions; + const name = nameOrFileOptions; attachment = { name, - contentType: contentType ?? (mime.getType(name) || (typeof body === 'string' ? 'text/plain' : 'application/octet-stream')), + 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 = nameOrOptions; + const options = nameOrFileOptions; const thePath = pathOrBody as string; const name = options?.name ?? path.basename(thePath); attachment = { diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 850308a530a7d..7dac800274da5 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -1322,9 +1322,9 @@ export interface TestInfo { * const path = testInfo.outputPath('screenshot.png'); * await page.screenshot({ path }); * await testInfo.attach(path); - * // optionally override the name + * // Optionally override the name. * await testInfo.attach(path, { name: 'example.png' }); - * // optionally override the contentType + * // Optionally override the contentType. * await testInfo.attach(path, { name: 'example.custom-file', contentType: 'x-custom-content-type' }); * }); * ``` @@ -1352,9 +1352,9 @@ export interface TestInfo { * Attach data to the current test, either a `string` or a `Buffer`. Some reporters show test attachments. * @param body * @param name - * @param contentType + * @param options */ - attach(body: string | Buffer, name: string, contentType?: string): Promise; + attach(body: string | Buffer, name: string, options?: { contentType?: string }): Promise; /** * 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). diff --git a/tests/playwright-test/reporter-raw.spec.ts b/tests/playwright-test/reporter-raw.spec.ts index edb107591b1b7..546c8dfb919c4 100644 --- a/tests/playwright-test/reporter-raw.spec.ts +++ b/tests/playwright-test/reporter-raw.spec.ts @@ -232,11 +232,11 @@ test(`testInfo.attach should save attachments via inline attachment`, async ({ r }); test('explicit contentType - string', async ({}, testInfo) => { - await testInfo.attach('We <3 Playwright!', 'example.json', 'x-playwright/custom'); + await testInfo.attach('We <3 Playwright!', 'example.json', { contentType: 'x-playwright/custom' }); }); test('explicit contentType - Buffer', async ({}, testInfo) => { - await testInfo.attach(Buffer.from('We <3 Playwright!'), 'example.json', 'x-playwright/custom'); + await testInfo.attach(Buffer.from('We <3 Playwright!'), 'example.json', { contentType: 'x-playwright/custom' }); }); `, }, { reporter: 'dot,' + kRawReporterPath, workers: 1 }, {}, { usesCustomOutputDir: true }); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 32af552dcb27f..f5f7dc58c6c5b 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -208,7 +208,7 @@ export interface TestInfo { annotations: { type: string, description?: string }[]; attachments: { name: string, path?: string, body?: Buffer, contentType: string }[]; attach(path: string, options?: { contentType?: string, name?: string}): Promise; - attach(body: string | Buffer, name: string, contentType?: string): Promise; + attach(body: string | Buffer, name: string, options?: { contentType?: string }): Promise; repeatEachIndex: number; retry: number; duration: number;