diff --git a/packages/playwright-test/src/isomorphic/teleReceiver.ts b/packages/playwright-test/src/isomorphic/teleReceiver.ts index 8a99bf6f58..8b961806de 100644 --- a/packages/playwright-test/src/isomorphic/teleReceiver.ts +++ b/packages/playwright-test/src/isomorphic/teleReceiver.ts @@ -88,12 +88,14 @@ export type JsonTestResultStart = { startTime: string; }; +export type JsonAttachment = Omit & { base64?: string }; + export type JsonTestResultEnd = { id: string; duration: number; status: TestStatus; errors: TestError[]; - attachments: TestResult['attachments']; + attachments: JsonAttachment[]; }; export type JsonTestStepStart = { @@ -228,7 +230,7 @@ export class TeleReporterReceiver { result.status = payload.status; result.statusEx = payload.status; result.errors = payload.errors; - result.attachments = payload.attachments; + result.attachments = this._parseAttachments(payload.attachments); this._reporter.onTestEnd?.(test, result); } @@ -321,6 +323,15 @@ export class TeleReporterReceiver { }; } + private _parseAttachments(attachments: JsonAttachment[]): TestResult['attachments'] { + return attachments.map(a => { + return { + ...a, + body: a.base64 && (globalThis as any).Buffer ? Buffer.from(a.base64, 'base64') : undefined, + }; + }); + } + private _mergeSuitesInto(jsonSuites: JsonSuite[], parent: TeleSuite) { for (const jsonSuite of jsonSuites) { let targetSuite = parent.suites.find(s => s.title === jsonSuite.title); diff --git a/packages/playwright-test/src/reporters/blob.ts b/packages/playwright-test/src/reporters/blob.ts index 36a27270b1..115d8008d8 100644 --- a/packages/playwright-test/src/reporters/blob.ts +++ b/packages/playwright-test/src/reporters/blob.ts @@ -21,7 +21,7 @@ import { mime } from 'playwright-core/lib/utilsBundle'; import { Readable } from 'stream'; import type { FullConfig, FullResult, TestResult } from '../../types/testReporter'; import type { Suite } from '../common/test'; -import type { JsonEvent } from '../isomorphic/teleReceiver'; +import type { JsonAttachment, JsonEvent } from '../isomorphic/teleReceiver'; import { TeleReporterEmitter } from './teleEmitter'; @@ -44,7 +44,7 @@ export class BlobReporter extends TeleReporterEmitter { private _reportFile!: string; constructor(options: BlobReporterOptions) { - super(message => this._messages.push(message)); + super(message => this._messages.push(message), false); this._options = options; this._salt = createGuid(); @@ -78,8 +78,8 @@ export class BlobReporter extends TeleReporterEmitter { ]); } - override _serializeAttachments(attachments: TestResult['attachments']): TestResult['attachments'] { - return attachments.map(attachment => { + override _serializeAttachments(attachments: TestResult['attachments']): JsonAttachment[] { + return super._serializeAttachments(attachments).map(attachment => { if (!attachment.path || !fs.statSync(attachment.path).isFile()) return attachment; // Add run guid to avoid clashes between shards. diff --git a/packages/playwright-test/src/reporters/teleEmitter.ts b/packages/playwright-test/src/reporters/teleEmitter.ts index 3a3eceac2c..fd93498ce6 100644 --- a/packages/playwright-test/src/reporters/teleEmitter.ts +++ b/packages/playwright-test/src/reporters/teleEmitter.ts @@ -20,15 +20,17 @@ import type { SuitePrivate } from '../../types/reporterPrivate'; import type { FullConfig, FullResult, Location, Reporter, TestError, TestResult, TestStep } from '../../types/testReporter'; import { FullConfigInternal, FullProjectInternal } from '../common/config'; import type { Suite, TestCase } from '../common/test'; -import type { JsonConfig, JsonEvent, JsonProject, JsonStdIOType, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver'; +import type { JsonAttachment, JsonConfig, JsonEvent, JsonProject, JsonStdIOType, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver'; import { serializeRegexPatterns } from '../isomorphic/teleReceiver'; export class TeleReporterEmitter implements Reporter { private _messageSink: (message: JsonEvent) => void; private _rootDir!: string; + private _receiverIsInBrowser: boolean; - constructor(messageSink: (message: JsonEvent) => void) { + constructor(messageSink: (message: JsonEvent) => void, receiverIsInBrowser: boolean) { this._messageSink = messageSink; + this._receiverIsInBrowser = receiverIsInBrowser; } onBegin(config: FullConfig, suite: Suite) { @@ -199,8 +201,14 @@ export class TeleReporterEmitter implements Reporter { }; } - _serializeAttachments(attachments: TestResult['attachments']): TestResult['attachments'] { - return attachments; + _serializeAttachments(attachments: TestResult['attachments']): JsonAttachment[] { + return attachments.map(a => { + return { + ...a, + // There is no Buffer in the browser, so there is no point in sending the data there. + base64: (a.body && !this._receiverIsInBrowser) ? a.body.toString('base64') : undefined, + }; + }); } private _serializeStepStart(step: TestStep): JsonTestStepStart { diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index a800a94d8b..a07738edb9 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -158,7 +158,7 @@ class UIMode { } private async _listTests() { - const listReporter = new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params)); + const listReporter = new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true); const reporter = new InternalReporter([listReporter]); this._config.cliListOnly = true; this._config.testIdMatcher = undefined; @@ -183,7 +183,7 @@ class UIMode { this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); const reporters = await createReporters(this._config, 'ui'); - reporters.push(new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params))); + reporters.push(new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true)); const reporter = new InternalReporter(reporters); const taskRunner = createTaskRunnerForWatch(this._config, reporter); const testRun = new TestRun(this._config, reporter); diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 32d3226b23..b328ad2ce2 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -947,6 +947,59 @@ result.stderr: stderr text `); }); +test('encode inline attachments', async ({ runInlineTest, mergeReports }) => { + const reportDir = test.info().outputPath('blob-report'); + const files = { + 'echo-reporter.js': ` + import fs from 'fs'; + + class EchoReporter { + onTestEnd(test, result) { + const attachmentBodies = result.attachments.map(a => a.body?.toString('base64')); + result.attachments.forEach(a => console.log(a.body, 'isBuffer', Buffer.isBuffer(a.body))); + fs.writeFileSync('log.txt', attachmentBodies.join(',')); + } + } + module.exports = EchoReporter; + `, + 'playwright.config.js': ` + module.exports = { + reporter: [['blob']] + }; + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('a test', async ({}) => { + expect(1 + 1).toBe(2); + test.info().attachments.push({ + name: 'example.txt', + contentType: 'text/plain', + body: Buffer.from('foo'), + }); + + test.info().attachments.push({ + name: 'example.json', + contentType: 'application/json', + body: Buffer.from(JSON.stringify({ foo: 1 })), + }); + + test.info().attachments.push({ + name: 'example-utf16.txt', + contentType: 'text/plain, charset=utf16le', + body: Buffer.from('utf16 encoded', 'utf16le'), + }); + }); + `, + }; + + await runInlineTest(files); + + const { exitCode } = await mergeReports(reportDir, {}, { additionalArgs: ['--reporter', test.info().outputPath('echo-reporter.js')] }); + expect(exitCode).toBe(0); + const log = fs.readFileSync(test.info().outputPath('log.txt')).toString(); + expect(log).toBe(`Zm9v,eyJmb28iOjF9,dQB0AGYAMQA2ACAAZQBuAGMAbwBkAGUAZAA=`); +}); + test('preserve steps in html report', async ({ runInlineTest, mergeReports, showReport, page }) => { const reportDir = test.info().outputPath('blob-report'); const files = {