diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index 96814dd96c..27381a8665 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -22,7 +22,7 @@ import { Transform, TransformCallback } from 'stream'; import { FullConfig, Suite, Reporter } from '../../types/testReporter'; import { HttpServer } from 'playwright-core/lib/utils/httpServer'; import { calculateSha1, removeFolders } from 'playwright-core/lib/utils/utils'; -import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw'; +import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from './raw'; import assert from 'assert'; import yazl from 'yazl'; import { stripAnsiEscapes } from './base'; @@ -78,7 +78,13 @@ export type TestCase = TestCaseSummary & { results: TestResult[]; }; -export type TestAttachment = JsonAttachment; +export type TestAttachment = { + name: string; + body?: string; + path?: string; + contentType: string; +}; + export type TestResult = { retry: number; @@ -381,6 +387,19 @@ class HtmlBuilder { attachments: result.attachments.map(a => { if (a.name === 'trace') this._hasTraces = true; + + if ((a.name === 'stdout' || a.name === 'stderr') && a.contentType === 'text/plain') { + if (lastAttachment && + lastAttachment.name === a.name && + lastAttachment.contentType === a.contentType) { + lastAttachment.body += stripAnsiEscapes(a.body as string); + return null; + } + a.body = stripAnsiEscapes(a.body as string); + lastAttachment = a as TestAttachment; + return a; + } + if (a.path) { let fileName = a.path; try { @@ -404,17 +423,39 @@ class HtmlBuilder { }; } - if ((a.name === 'stdout' || a.name === 'stderr') && a.contentType === 'text/plain') { - if (lastAttachment && - lastAttachment.name === a.name && - lastAttachment.contentType === a.contentType) { - lastAttachment.body += stripAnsiEscapes(a.body as string); - return null; + if (a.body instanceof Buffer) { + if (isTextContentType(a.contentType)) { + // Content type is like this: "text/html; charset=UTF-8" + const charset = a.contentType.match(/charset=(.*)/)?.[1]; + try { + const body = a.body.toString(charset as any || 'utf-8'); + return { + name: a.name, + contentType: a.contentType, + body, + }; + } catch (e) { + // Invalid encoding, fall through and save to file. + } } - a.body = stripAnsiEscapes(a.body as string); + + fs.mkdirSync(path.join(this._reportFolder, 'data'), { recursive: true }); + const sha1 = calculateSha1(a.body) + '.dat'; + fs.writeFileSync(path.join(this._reportFolder, 'data', sha1), a.body); + return { + name: a.name, + contentType: a.contentType, + path: 'data/' + sha1, + body: a.body, + }; } - lastAttachment = a; - return a; + + // string + return { + name: a.name, + contentType: a.contentType, + body: a.body, + }; }).filter(Boolean) as TestAttachment[] }; } @@ -481,4 +522,8 @@ class Base64Encoder extends Transform { } } +function isTextContentType(contentType: string) { + return contentType.startsWith('text/') || contentType.startsWith('application/json'); +} + export default HtmlReporter; diff --git a/packages/playwright-test/src/reporters/raw.ts b/packages/playwright-test/src/reporters/raw.ts index da303f9cba..2d1a6f1d9e 100644 --- a/packages/playwright-test/src/reporters/raw.ts +++ b/packages/playwright-test/src/reporters/raw.ts @@ -71,7 +71,7 @@ export type JsonTestCase = { export type JsonAttachment = { name: string; - body?: string; + body?: string | Buffer; path?: string; contentType: string; }; @@ -245,7 +245,7 @@ class RawReporter { attachments.push({ name: attachment.name, contentType: attachment.contentType, - body: attachment.body.toString('base64') + body: attachment.body }); } else if (attachment.path) { attachments.push({ @@ -274,7 +274,7 @@ class RawReporter { return { name: type, contentType: 'application/octet-stream', - body: chunk.toString('base64') + body: chunk }; } diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index e84d80e0e8..6d309c722d 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -294,3 +294,44 @@ test('should render annotations', async ({ runInlineTest, page, showReport }) => await page.click('text=skipped test'); await expect(page.locator('.test-case-annotation')).toHaveText('skip: I am not interested in this test'); }); + +test('should render text attachments as text', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = pwt; + test('passing', async ({ page }, testInfo) => { + testInfo.attachments.push({ + name: 'example.txt', + contentType: 'text/plain', + body: Buffer.from('foo'), + }); + + testInfo.attachments.push({ + name: 'example.json', + contentType: 'application/json', + body: Buffer.from(JSON.stringify({ foo: 1 })), + }); + + testInfo.attachments.push({ + name: 'example-utf16.txt', + contentType: 'text/plain, charset=utf16le', + body: Buffer.from('utf16 encoded', 'utf16le'), + }); + + testInfo.attachments.push({ + name: 'example-null.txt', + contentType: 'text/plain, charset=utf16le', + body: null, + }); + }); + `, + }, { reporter: 'dot,html' }); + expect(result.exitCode).toBe(0); + + await showReport(); + await page.click('text=passing'); + await page.click('text=example.txt'); + await page.click('text=example.json'); + await page.click('text=example-utf16.txt'); + await expect(page.locator('.attachment-body')).toHaveText(['foo', '{"foo":1}', 'utf16 encoded']); +}); diff --git a/tests/playwright-test/reporter-raw.spec.ts b/tests/playwright-test/reporter-raw.spec.ts index 546c8dfb91..8290bd34b2 100644 --- a/tests/playwright-test/reporter-raw.spec.ts +++ b/tests/playwright-test/reporter-raw.spec.ts @@ -72,13 +72,13 @@ test('should save stdio', async ({ runInlineTest }, testInfo) => { { name: 'stdout', contentType: 'application/octet-stream', - body: 'AQID' + body: { data: [1, 2, 3], type: 'Buffer' } }, { name: 'stderr', contentType: 'text/plain', body: 'STDERR\n' }, { name: 'stderr', contentType: 'application/octet-stream', - body: 'BAUG' + body: { data: [4, 5, 6], type: 'Buffer' } } ]); });