From be79ee0450b3aa77c213e34b65e50f9d675b2d9c Mon Sep 17 00:00:00 2001 From: mindaugasm Date: Tue, 18 Apr 2023 21:25:11 +0300 Subject: [PATCH] feat(html-report): add attachmentsBaseURL option (#22212) Fixes https://github.com/microsoft/playwright/issues/21636 --- docs/src/test-reporters-js.md | 10 ++++++ .../playwright-test/src/reporters/html.ts | 18 ++++++---- packages/playwright-test/types/test.d.ts | 2 +- tests/playwright-test/reporter-html.spec.ts | 36 +++++++++++++++++++ utils/generate_types/overrides-test.d.ts | 2 +- 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md index fe68e2ea7c..79d02556f3 100644 --- a/docs/src/test-reporters-js.md +++ b/docs/src/test-reporters-js.md @@ -183,6 +183,16 @@ export default defineConfig({ }); ``` +If you are uploading attachments from data folder to other location, you can use `attachmentsBaseURL` option to let html report where to look for them. + +```js +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + reporter: [['html', { attachmentsBaseURL: 'https://external-storage.com/' }]], +}); +``` + A quick way of opening the last test run report is: ```bash diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index a4210dc45b..7e3d6578e9 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -46,6 +46,7 @@ type HtmlReporterOptions = { open?: HtmlReportOpenOption, host?: string, port?: number, + attachmentsBaseURL?: string, }; class HtmlReporter implements Reporter { @@ -54,6 +55,7 @@ class HtmlReporter implements Reporter { private _montonicStartTime: number = 0; private _options: HtmlReporterOptions; private _outputFolder!: string; + private _attachmentsBaseURL!: string; private _open: string | undefined; private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined; @@ -68,9 +70,10 @@ class HtmlReporter implements Reporter { onBegin(config: FullConfig, suite: Suite) { this._montonicStartTime = monotonicTime(); this.config = config; - const { outputFolder, open } = this._resolveOptions(); + const { outputFolder, open, attachmentsBaseURL } = this._resolveOptions(); this._outputFolder = outputFolder; this._open = open; + this._attachmentsBaseURL = attachmentsBaseURL; const reportedWarnings = new Set(); for (const project of config.projects) { if (outputFolder.startsWith(project.outputDir) || project.outputDir.startsWith(outputFolder)) { @@ -90,13 +93,14 @@ class HtmlReporter implements Reporter { this.suite = suite; } - _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption } { + _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string } { let { outputFolder } = this._options; if (outputFolder) outputFolder = path.resolve(this._options.configDir, outputFolder); return { outputFolder: reportFolderFromEnv() ?? outputFolder ?? defaultReportFolder(this._options.configDir), open: process.env.PW_TEST_HTML_REPORT_OPEN as any || this._options.open || 'on-failure', + attachmentsBaseURL: this._options.attachmentsBaseURL || 'data/' }; } @@ -109,7 +113,7 @@ class HtmlReporter implements Reporter { return report; }); await removeFolders([this._outputFolder]); - const builder = new HtmlBuilder(this._outputFolder); + const builder = new HtmlBuilder(this._outputFolder, this._attachmentsBaseURL); this._buildResult = await builder.build({ ...this.config.metadata, duration }, reports); } @@ -198,11 +202,13 @@ class HtmlBuilder { private _testPath = new Map(); private _dataZipFile: ZipFile; private _hasTraces = false; + private _attachmentsBaseURL: string; - constructor(outputDir: string) { + constructor(outputDir: string, attachmentsBaseURL: string) { this._reportFolder = outputDir; fs.mkdirSync(this._reportFolder, { recursive: true }); this._dataZipFile = new yazl.ZipFile(); + this._attachmentsBaseURL = attachmentsBaseURL; } async build(metadata: Metadata & { duration: number }, rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { @@ -389,7 +395,7 @@ class HtmlBuilder { try { const buffer = fs.readFileSync(a.path); const sha1 = calculateSha1(buffer) + path.extname(a.path); - fileName = 'data/' + sha1; + fileName = this._attachmentsBaseURL + sha1; fs.mkdirSync(path.join(this._reportFolder, 'data'), { recursive: true }); fs.writeFileSync(path.join(this._reportFolder, 'data', sha1), buffer); } catch (e) { @@ -430,7 +436,7 @@ class HtmlBuilder { return { name: a.name, contentType: a.contentType, - path: 'data/' + sha1, + path: this._attachmentsBaseURL + sha1, }; } diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 3a46308fee..59fbe4604d 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -25,7 +25,7 @@ export type ReporterDescription = ['github'] | ['junit'] | ['junit', { outputFile?: string, stripANSIControlSequences?: boolean }] | ['json'] | ['json', { outputFile?: string }] | - ['html'] | ['html', { outputFolder?: string, open?: 'always' | 'never' | 'on-failure' }] | + ['html'] | ['html', { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', attachmentsBaseURL?: string }] | ['null'] | [string] | [string, any]; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index daceb9bc45..bff5a35ed2 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -329,6 +329,42 @@ test('should include screenshot on failure', async ({ runInlineTest, page, showR expect(src).toBeTruthy(); }); +test('should use different path if attachments base url option is provided', async ({ runInlineTest, page, showReport }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + use: { + viewport: { width: 200, height: 200 }, + screenshot: 'on', + video: 'on', + trace: 'on', + }, + reporter: [['html', { attachmentsBaseURL: 'https://some-url.com/' }]] + }; + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('passes', async ({ page }) => { + await page.evaluate('2 + 2'); + }); + ` + }, {}, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await page.click('text=passes'); + + await expect(page.locator('div').filter({ hasText: /^Screenshotsscreenshot$/ }).getByRole('img')).toHaveAttribute('src', /(https:\/\/some-url\.com\/)[^/\s]+?\.[^/\s]+/); + await expect(page.getByRole('link', { name: 'screenshot' })).toHaveAttribute('href', /(https:\/\/some-url\.com\/)[^/\s]+?\.[^/\s]+/); + + await expect(page.locator('video').locator('source')).toHaveAttribute('src', /(https:\/\/some-url\.com\/)[^/\s]+?\.[^/\s]+/); + await expect(page.getByRole('link', { name: 'video' })).toHaveAttribute('href', /(https:\/\/some-url\.com\/)[^/\s]+?\.[^/\s]+/); + + await expect(page.getByRole('link', { name: 'trace' })).toHaveAttribute('href', /(https:\/\/some-url\.com\/)[^/\s]+?\.[^/\s]+/); + await expect(page.locator('div').filter({ hasText: /^Tracestrace$/ }).getByRole('link').first()).toHaveAttribute('href', /trace=(https:\/\/some-url\.com\/)[^/\s]+?\.[^/\s]+/); +}); + test('should include stdio', async ({ runInlineTest, page, showReport }) => { const result = await runInlineTest({ 'a.test.js': ` diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 519d361962..eca53d1be2 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -24,7 +24,7 @@ export type ReporterDescription = ['github'] | ['junit'] | ['junit', { outputFile?: string, stripANSIControlSequences?: boolean }] | ['json'] | ['json', { outputFile?: string }] | - ['html'] | ['html', { outputFolder?: string, open?: 'always' | 'never' | 'on-failure' }] | + ['html'] | ['html', { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', attachmentsBaseURL?: string }] | ['null'] | [string] | [string, any];