From 300d80f901940260a8564932ecfdbbc1a8f30d99 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 17 Apr 2025 10:37:52 -0700 Subject: [PATCH 1/5] feat(html): allow setting a title to display --- docs/src/test-reporters-js.md | 1 + packages/html-reporter/src/reportView.css | 18 +++++++++ packages/html-reporter/src/reportView.tsx | 32 ++++++++++----- packages/html-reporter/src/testCaseView.css | 13 ------- packages/html-reporter/src/testCaseView.tsx | 2 +- packages/html-reporter/src/testFilesView.tsx | 6 ++- packages/html-reporter/src/types.d.ts | 5 +++ packages/playwright/src/reporters/html.ts | 41 ++++++++++---------- packages/playwright/types/test.d.ts | 2 +- tests/playwright-test/reporter-html.spec.ts | 27 +++++++++++-- utils/generate_types/overrides-test.d.ts | 2 +- 11 files changed, 99 insertions(+), 50 deletions(-) diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md index ce5698005cba5..294f0466ddb79 100644 --- a/docs/src/test-reporters-js.md +++ b/docs/src/test-reporters-js.md @@ -246,6 +246,7 @@ HTML report supports the following configuration options and environment variabl | Environment Variable Name | Reporter Config Option| Description | Default |---|---|---|---| +| `PLAYWRIGHT_HTML_TITLE` | `title` | A title to display in the generated report. | No title is displayed by default | `PLAYWRIGHT_HTML_OUTPUT_DIR` | `outputFolder` | Directory to save the report to. | `playwright-report` | `PLAYWRIGHT_HTML_OPEN` | `open` | When to open the html report in the browser, one of `'always'`, `'never'` or `'on-failure'` | `'on-failure'` | `PLAYWRIGHT_HTML_HOST` | `host` | When report opens in the browser, it will be served bound to this hostname. | `localhost` diff --git a/packages/html-reporter/src/reportView.css b/packages/html-reporter/src/reportView.css index ae5a633c2466c..05d32f20342c6 100644 --- a/packages/html-reporter/src/reportView.css +++ b/packages/html-reporter/src/reportView.css @@ -29,6 +29,24 @@ body { width: 100%; } +.report-body { + border-radius: 6px; + margin: 12px 0 24px 0; +} + +.report-title { + flex: none; + padding: 8px; + font-weight: 400; + font-size: 32px !important; + line-height: 1.25 !important; +} + +.report-title.metadata-visible { + /* Mirror bottom margin from following chip (.chip-header) so the title appears aligned */ + margin-top: 12px; +} + .test-file-test:not(:first-child) { border-top: 1px solid var(--color-border-default); } diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index e48064201c26f..b233e375c5afe 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -72,25 +72,39 @@ export const ReportView: React.FC<{ return result; }, [report, filter]); + const json = report?.json(); + const reportTitle = json?.options.title; + + React.useEffect(() => { + if (reportTitle) + document.title = reportTitle; + else + document.title = 'Playwright Test Report'; + }, [reportTitle]); + return
- {report?.json() && } + {json && } - setMetadataVisible(visible => !visible)}/> - + + setMetadataVisible(visible => !visible)}/> + + - {!!report && } + {!!report && }
; }; +const Body: React.FC> = ({ children }) =>
{children}
; + const TestCaseViewLoader: React.FC<{ report: LoadedReport, tests: TestCaseSummary[], diff --git a/packages/html-reporter/src/testCaseView.css b/packages/html-reporter/src/testCaseView.css index 9ae47b179d249..143329049a711 100644 --- a/packages/html-reporter/src/testCaseView.css +++ b/packages/html-reporter/src/testCaseView.css @@ -14,11 +14,6 @@ limitations under the License. */ -.test-case-column { - border-radius: 6px; - margin: 12px 0 24px 0; -} - .test-case-column .tab-element.selected { font-weight: 600; border-bottom-color: var(--color-primer-border-active); @@ -34,14 +29,6 @@ color: var(--color-fg-default); } -.test-case-title { - flex: none; - padding: 8px; - font-weight: 400; - font-size: 32px !important; - line-height: 1.25 !important; -} - .test-case-location, .test-case-duration { flex: none; diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 0ccbb8b6f6bd4..486ffc633af38 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -55,7 +55,7 @@ export const TestCaseView: React.FC<{
next »
} - {test &&
{test?.title}
} + {test &&
{test?.title}
} {test &&
diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx index 4b2c48ae1d84e..cd60ded6957af 100644 --- a/packages/html-reporter/src/testFilesView.tsx +++ b/packages/html-reporter/src/testFilesView.tsx @@ -23,6 +23,7 @@ import { AutoChip } from './chip'; import { TestErrorView } from './testErrorView'; import * as icons from './icons'; import { isMetadataEmpty, MetadataView } from './metadataView'; +import { clsx } from '@web/uiUtils'; export const TestFilesView: React.FC<{ tests: TestFileSummary[], @@ -70,7 +71,7 @@ export const TestFilesHeader: React.FC<{ if (!report) return null; return <> -
+
{!isMetadataEmpty(report.metadata) &&
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata @@ -83,6 +84,9 @@ export const TestFilesHeader: React.FC<{
Total time: {msToString(report.duration ?? 0)}
{metadataVisible && } + {report.options.title &&
+ {report.options.title} +
} {!!report.errors.length && {report.errors.map((error, index) => )} } diff --git a/packages/html-reporter/src/types.d.ts b/packages/html-reporter/src/types.d.ts index 17c5a3b3730a4..812a41c880840 100644 --- a/packages/html-reporter/src/types.d.ts +++ b/packages/html-reporter/src/types.d.ts @@ -36,8 +36,13 @@ export type Location = { column: number; }; +export type Options = { + title?: string; +} + export type HTMLReport = { metadata: Metadata; + options: Options; files: TestFileSummary[]; stats: Stats; projectNames: string[]; diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 8e057d81931ff..8f0f9b1e96408 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -31,7 +31,7 @@ import { resolveReporterOutputPath, stripAnsiEscapes } from '../util'; import type { ReporterV2 } from './reporterV2'; import type { Metadata } from '../../types/test'; import type * as api from '../../types/testReporter'; -import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep, TestAnnotation } from '@html-reporter/types'; +import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep, TestAnnotation, Options } from '@html-reporter/types'; import type { ZipFile } from 'playwright-core/lib/zipBundle'; import type { TransformCallback } from 'stream'; @@ -54,6 +54,7 @@ type HtmlReporterOptions = { host?: string, port?: number, attachmentsBaseURL?: string, + title?: string, _mode?: 'test' | 'list'; _isTestServer?: boolean; }; @@ -62,11 +63,7 @@ class HtmlReporter implements ReporterV2 { private config!: api.FullConfig; private suite!: api.Suite; private _options: HtmlReporterOptions; - private _outputFolder!: string; - private _attachmentsBaseURL!: string; - private _open: string | undefined; - private _port: number | undefined; - private _host: string | undefined; + private _resolvedOptions!: { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string, host: string | undefined, port: number | undefined, title: string | undefined }; private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined; private _topLevelErrors: api.TestError[] = []; @@ -87,12 +84,8 @@ class HtmlReporter implements ReporterV2 { } onBegin(suite: api.Suite) { - const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions(); - this._outputFolder = outputFolder; - this._open = open; - this._host = host; - this._port = port; - this._attachmentsBaseURL = attachmentsBaseURL; + this._resolvedOptions = this._resolveOptions(); + const outputFolder = this._resolvedOptions.outputFolder; const reportedWarnings = new Set(); for (const project of this.config.projects) { if (this._isSubdirectory(outputFolder, project.outputDir) || this._isSubdirectory(project.outputDir, outputFolder)) { @@ -112,7 +105,7 @@ class HtmlReporter implements ReporterV2 { this.suite = suite; } - _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string, host: string | undefined, port: number | undefined } { + _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string, host: string | undefined, port: number | undefined, title: string | undefined } { const outputFolder = reportFolderFromEnv() ?? resolveReporterOutputPath('playwright-report', this._options.configDir, this._options.outputFolder); return { outputFolder, @@ -120,6 +113,7 @@ class HtmlReporter implements ReporterV2 { attachmentsBaseURL: process.env.PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL || this._options.attachmentsBaseURL || 'data/', host: process.env.PLAYWRIGHT_HTML_HOST || this._options.host, port: process.env.PLAYWRIGHT_HTML_PORT ? +process.env.PLAYWRIGHT_HTML_PORT : this._options.port, + title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, }; } @@ -134,8 +128,8 @@ class HtmlReporter implements ReporterV2 { async onEnd(result: api.FullResult) { const projectSuites = this.suite.suites; - await removeFolders([this._outputFolder]); - const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL); + await removeFolders([this._resolvedOptions.outputFolder]); + const builder = new HtmlBuilder(this.config, this._resolvedOptions.outputFolder, this._resolvedOptions.attachmentsBaseURL, this._resolvedOptions); this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors); } @@ -143,14 +137,14 @@ class HtmlReporter implements ReporterV2 { if (process.env.CI || !this._buildResult) return; const { ok, singleTestId } = this._buildResult; - const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure')); + const shouldOpen = !this._options._isTestServer && (this._resolvedOptions.open === 'always' || (!ok && this._resolvedOptions.open === 'on-failure')); if (shouldOpen) { - await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId); + await showHTMLReport(this._resolvedOptions.outputFolder, this._resolvedOptions.host, this._resolvedOptions.port, singleTestId); } else if (this._options._mode === 'test' && !this._options._isTestServer) { const packageManagerCommand = getPackageManagerExecCommand(); - const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder); - const hostArg = this._host ? ` --host ${this._host}` : ''; - const portArg = this._port ? ` --port ${this._port}` : ''; + const relativeReportPath = this._resolvedOptions.outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._resolvedOptions.outputFolder); + const hostArg = this._resolvedOptions.host ? ` --host ${this._resolvedOptions.host}` : ''; + const portArg = this._resolvedOptions.port ? ` --port ${this._resolvedOptions.port}` : ''; console.log(''); console.log('To open last HTML report run:'); console.log(colors.cyan(` @@ -227,14 +221,16 @@ export function startHtmlReportServer(folder: string): HttpServer { class HtmlBuilder { private _config: api.FullConfig; + private _options?: Options; private _reportFolder: string; private _stepsInFile = new MultiMap(); private _dataZipFile: ZipFile; private _hasTraces = false; private _attachmentsBaseURL: string; - constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string) { + constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string, options: Options | undefined) { this._config = config; + this._options = options; this._reportFolder = outputDir; fs.mkdirSync(this._reportFolder, { recursive: true }); this._dataZipFile = new yazl.ZipFile(); @@ -295,6 +291,9 @@ class HtmlBuilder { } const htmlReport: HTMLReport = { metadata, + options: { + title: this._options?.title, + }, startTime: result.startTime.getTime(), duration: result.duration, files: [...data.values()].map(e => e.testFileSummary), diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 71a2a162fd015..94eb500f2acdc 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -26,7 +26,7 @@ export type ReporterDescription = Readonly< ['github'] | ['junit'] | ['junit', { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean }] | ['json'] | ['json', { outputFile?: string }] | - ['html'] | ['html', { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', host?: string, port?: number, attachmentsBaseURL?: string }] | + ['html'] | ['html', { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', host?: string, port?: number, attachmentsBaseURL?: string, title?: string }] | ['null'] | [string] | [string, any] >; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index aa827cda08d0e..d4ff4cecfe951 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -435,6 +435,27 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('div').filter({ hasText: /^Tracestrace$/ }).getByRole('link').first()).toHaveAttribute('href', /trace=(https:\/\/some-url\.com\/)[^/\s]+?\.[^/\s]+/); }); + test('should display title if provided', async ({ runInlineTest, page, showReport }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['html', { title: 'Custom report title' }], ['line']] + }; + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('passes', async ({ page }) => { + await page.evaluate('2 + 2'); + }); + ` + }, {}, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await expect(page.locator('.report-title')).toHaveText('Custom report title'); + }); + test('should include stdio', async ({ runInlineTest, page, showReport }) => { const result = await runInlineTest({ 'a.test.js': ` @@ -1819,7 +1840,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { const testTitle = page.locator('.test-file-test .test-file-title', { hasText: `${tag} passes` }); await testTitle.click(); - await expect(page.locator('.test-case-title', { hasText: `${tag} passes` })).toBeVisible(); + await expect(page.locator('.report-title', { hasText: `${tag} passes` })).toBeVisible(); await expect(page.locator('.label', { hasText: tag })).toBeVisible(); await page.goBack(); @@ -2341,7 +2362,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { await notificationsChromiumTestCase.locator('.test-file-title').click(); await expect(page).toHaveURL(/testId/); await expect(page.locator('.test-case-path')).toHaveText('Root describe › @Notifications'); - await expect(page.locator('.test-case-title')).toHaveText('Test failed -- @call @call-details @e2e @regression #VQ458'); + await expect(page.locator('.report-title')).toHaveText('Test failed -- @call @call-details @e2e @regression #VQ458'); await expect(page.locator('.label')).toHaveText(['chromium', 'Notifications', 'call', 'call-details', 'e2e', 'regression']); await page.goBack(); @@ -2353,7 +2374,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { await monitoringFirefoxTestCase.locator('.test-file-title').click(); await expect(page).toHaveURL(/testId/); await expect(page.locator('.test-case-path')).toHaveText('Root describe › @Monitoring'); - await expect(page.locator('.test-case-title')).toHaveText('Test passed -- @call @call-details @e2e @regression #VQ457'); + await expect(page.locator('.report-title')).toHaveText('Test passed -- @call @call-details @e2e @regression #VQ457'); await expect(page.locator('.label')).toHaveText(['firefox', 'Monitoring', 'call', 'call-details', 'e2e', 'regression']); }); }); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 567b33e88fbb1..42a13d2c05231 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -25,7 +25,7 @@ export type ReporterDescription = Readonly< ['github'] | ['junit'] | ['junit', { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean }] | ['json'] | ['json', { outputFile?: string }] | - ['html'] | ['html', { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', host?: string, port?: number, attachmentsBaseURL?: string }] | + ['html'] | ['html', { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', host?: string, port?: number, attachmentsBaseURL?: string, title?: string }] | ['null'] | [string] | [string, any] >; From 5934dc4afd320dae2af892a7927ba27ca3ee3c6e Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 18 Apr 2025 05:59:47 -0700 Subject: [PATCH 2/5] Small type extraction --- packages/playwright/src/reporters/html.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 8f0f9b1e96408..7335b33c1c0d0 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -47,23 +47,26 @@ const isHtmlReportOption = (type: string): type is HtmlReportOpenOption => { return htmlReportOptions.includes(type); }; -type HtmlReporterOptions = { - configDir: string, - outputFolder?: string, - open?: HtmlReportOpenOption, +type InternalResolvedOptions = { + outputFolder: string, + open: HtmlReportOpenOption, + attachmentsBaseURL: string, host?: string, port?: number, - attachmentsBaseURL?: string, - title?: string, + title?: string +}; + +type HtmlReporterOptions = { + configDir: string, _mode?: 'test' | 'list'; _isTestServer?: boolean; -}; +} & Partial; class HtmlReporter implements ReporterV2 { private config!: api.FullConfig; private suite!: api.Suite; private _options: HtmlReporterOptions; - private _resolvedOptions!: { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string, host: string | undefined, port: number | undefined, title: string | undefined }; + private _resolvedOptions!: InternalResolvedOptions; private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined; private _topLevelErrors: api.TestError[] = []; @@ -105,7 +108,7 @@ class HtmlReporter implements ReporterV2 { this.suite = suite; } - _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string, host: string | undefined, port: number | undefined, title: string | undefined } { + _resolveOptions(): InternalResolvedOptions { const outputFolder = reportFolderFromEnv() ?? resolveReporterOutputPath('playwright-report', this._options.configDir, this._options.outputFolder); return { outputFolder, From 5fe43834ed7b5c619023213eee98b639a7de4aa0 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 22 Apr 2025 12:09:39 -0700 Subject: [PATCH 3/5] Strip down set of changes to the minimal set --- packages/html-reporter/src/headerView.css | 8 +++ packages/html-reporter/src/headerView.tsx | 2 + packages/html-reporter/src/reportView.tsx | 25 ++++---- packages/html-reporter/src/testCaseView.css | 5 ++ packages/html-reporter/src/testCaseView.tsx | 3 +- packages/html-reporter/src/testFilesView.tsx | 7 +-- packages/html-reporter/src/types.d.ts | 6 +- packages/playwright/src/reporters/html.ts | 61 +++++++++++--------- tests/playwright-test/reporter-html.spec.ts | 2 +- 9 files changed, 65 insertions(+), 54 deletions(-) diff --git a/packages/html-reporter/src/headerView.css b/packages/html-reporter/src/headerView.css index ef29c442ca2bd..a9d9dfe799c23 100644 --- a/packages/html-reporter/src/headerView.css +++ b/packages/html-reporter/src/headerView.css @@ -18,6 +18,14 @@ float: right; } +.header-title { + flex: none; + padding: 8px; + font-weight: 400; + font-size: 32px !important; + line-height: 1.25 !important; +} + @media only screen and (max-width: 600px) { .header-view-status-container { float: none; diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index c57a6d3374a69..ef6dc2dedcd8a 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -59,6 +59,8 @@ export const HeaderView: React.FC<{ ); }; +export const HeaderTitleView: React.FC<{ title: string }> = ({ title }) =>
{title}
; + const StatsNavView: React.FC<{ stats: Stats }> = ({ stats }) => { diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index b233e375c5afe..691b02f49bcc1 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -72,8 +72,7 @@ export const ReportView: React.FC<{ return result; }, [report, filter]); - const json = report?.json(); - const reportTitle = json?.options.title; + const reportTitle = report?.json()?.title; React.useEffect(() => { if (reportTitle) @@ -84,27 +83,23 @@ export const ReportView: React.FC<{ return
- {json && } + {report?.json() && } - - setMetadataVisible(visible => !visible)}/> - - + setMetadataVisible(visible => !visible)}/> + - {!!report && } + {!!report && }
; }; -const Body: React.FC> = ({ children }) =>
{children}
; - const TestCaseViewLoader: React.FC<{ report: LoadedReport, tests: TestCaseSummary[], diff --git a/packages/html-reporter/src/testCaseView.css b/packages/html-reporter/src/testCaseView.css index 143329049a711..fd8bdf7eca81c 100644 --- a/packages/html-reporter/src/testCaseView.css +++ b/packages/html-reporter/src/testCaseView.css @@ -14,6 +14,11 @@ limitations under the License. */ +.test-case-column { + border-radius: 6px; + margin: 12px 0 24px 0; +} + .test-case-column .tab-element.selected { font-weight: 600; border-bottom-color: var(--color-primer-border-active); diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 486ffc633af38..d599fb1a86458 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -27,6 +27,7 @@ import { linkifyText } from '@web/renderUtils'; import { hashStringToInt, msToString } from './utils'; import { clsx } from '@web/uiUtils'; import { CopyToClipboardContainer } from './copyToClipboard'; +import { HeaderTitleView } from './headerView'; export const TestCaseView: React.FC<{ projectNames: string[], @@ -55,7 +56,7 @@ export const TestCaseView: React.FC<{
next »
} - {test &&
{test?.title}
} + {test && } {test &&
diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx index cd60ded6957af..8c28900bde750 100644 --- a/packages/html-reporter/src/testFilesView.tsx +++ b/packages/html-reporter/src/testFilesView.tsx @@ -24,6 +24,7 @@ import { TestErrorView } from './testErrorView'; import * as icons from './icons'; import { isMetadataEmpty, MetadataView } from './metadataView'; import { clsx } from '@web/uiUtils'; +import { HeaderTitleView } from './headerView'; export const TestFilesView: React.FC<{ tests: TestFileSummary[], @@ -71,7 +72,7 @@ export const TestFilesHeader: React.FC<{ if (!report) return null; return <> -
+
{!isMetadataEmpty(report.metadata) &&
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata @@ -84,9 +85,7 @@ export const TestFilesHeader: React.FC<{
Total time: {msToString(report.duration ?? 0)}
{metadataVisible && } - {report.options.title &&
- {report.options.title} -
} + {report.title && } {!!report.errors.length && {report.errors.map((error, index) => )} } diff --git a/packages/html-reporter/src/types.d.ts b/packages/html-reporter/src/types.d.ts index 812a41c880840..c5897836a39b1 100644 --- a/packages/html-reporter/src/types.d.ts +++ b/packages/html-reporter/src/types.d.ts @@ -36,13 +36,9 @@ export type Location = { column: number; }; -export type Options = { - title?: string; -} - export type HTMLReport = { metadata: Metadata; - options: Options; + title: string | undefined; files: TestFileSummary[]; stats: Stats; projectNames: string[]; diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 7335b33c1c0d0..8c3c2fd6c2522 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -31,7 +31,7 @@ import { resolveReporterOutputPath, stripAnsiEscapes } from '../util'; import type { ReporterV2 } from './reporterV2'; import type { Metadata } from '../../types/test'; import type * as api from '../../types/testReporter'; -import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep, TestAnnotation, Options } from '@html-reporter/types'; +import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep, TestAnnotation } from '@html-reporter/types'; import type { ZipFile } from 'playwright-core/lib/zipBundle'; import type { TransformCallback } from 'stream'; @@ -47,26 +47,28 @@ const isHtmlReportOption = (type: string): type is HtmlReportOpenOption => { return htmlReportOptions.includes(type); }; -type InternalResolvedOptions = { - outputFolder: string, - open: HtmlReportOpenOption, - attachmentsBaseURL: string, - host?: string, - port?: number, - title?: string -}; - type HtmlReporterOptions = { configDir: string, + outputFolder?: string, + open?: HtmlReportOpenOption, + host?: string, + port?: number, + attachmentsBaseURL?: string, + title?: string, _mode?: 'test' | 'list'; _isTestServer?: boolean; -} & Partial; +}; class HtmlReporter implements ReporterV2 { private config!: api.FullConfig; private suite!: api.Suite; private _options: HtmlReporterOptions; - private _resolvedOptions!: InternalResolvedOptions; + private _outputFolder!: string; + private _attachmentsBaseURL!: string; + private _open: string | undefined; + private _port: number | undefined; + private _host: string | undefined; + private _title: string | undefined; private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined; private _topLevelErrors: api.TestError[] = []; @@ -87,8 +89,13 @@ class HtmlReporter implements ReporterV2 { } onBegin(suite: api.Suite) { - this._resolvedOptions = this._resolveOptions(); - const outputFolder = this._resolvedOptions.outputFolder; + const { outputFolder, open, attachmentsBaseURL, host, port, title } = this._resolveOptions(); + this._outputFolder = outputFolder; + this._open = open; + this._host = host; + this._port = port; + this._attachmentsBaseURL = attachmentsBaseURL; + this._title = title; const reportedWarnings = new Set(); for (const project of this.config.projects) { if (this._isSubdirectory(outputFolder, project.outputDir) || this._isSubdirectory(project.outputDir, outputFolder)) { @@ -108,7 +115,7 @@ class HtmlReporter implements ReporterV2 { this.suite = suite; } - _resolveOptions(): InternalResolvedOptions { + _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string, host: string | undefined, port: number | undefined, title: string | undefined } { const outputFolder = reportFolderFromEnv() ?? resolveReporterOutputPath('playwright-report', this._options.configDir, this._options.outputFolder); return { outputFolder, @@ -131,8 +138,8 @@ class HtmlReporter implements ReporterV2 { async onEnd(result: api.FullResult) { const projectSuites = this.suite.suites; - await removeFolders([this._resolvedOptions.outputFolder]); - const builder = new HtmlBuilder(this.config, this._resolvedOptions.outputFolder, this._resolvedOptions.attachmentsBaseURL, this._resolvedOptions); + await removeFolders([this._outputFolder]); + const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, this._title); this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors); } @@ -140,14 +147,14 @@ class HtmlReporter implements ReporterV2 { if (process.env.CI || !this._buildResult) return; const { ok, singleTestId } = this._buildResult; - const shouldOpen = !this._options._isTestServer && (this._resolvedOptions.open === 'always' || (!ok && this._resolvedOptions.open === 'on-failure')); + const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure')); if (shouldOpen) { - await showHTMLReport(this._resolvedOptions.outputFolder, this._resolvedOptions.host, this._resolvedOptions.port, singleTestId); + await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId); } else if (this._options._mode === 'test' && !this._options._isTestServer) { const packageManagerCommand = getPackageManagerExecCommand(); - const relativeReportPath = this._resolvedOptions.outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._resolvedOptions.outputFolder); - const hostArg = this._resolvedOptions.host ? ` --host ${this._resolvedOptions.host}` : ''; - const portArg = this._resolvedOptions.port ? ` --port ${this._resolvedOptions.port}` : ''; + const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder); + const hostArg = this._host ? ` --host ${this._host}` : ''; + const portArg = this._port ? ` --port ${this._port}` : ''; console.log(''); console.log('To open last HTML report run:'); console.log(colors.cyan(` @@ -224,20 +231,20 @@ export function startHtmlReportServer(folder: string): HttpServer { class HtmlBuilder { private _config: api.FullConfig; - private _options?: Options; private _reportFolder: string; private _stepsInFile = new MultiMap(); private _dataZipFile: ZipFile; private _hasTraces = false; private _attachmentsBaseURL: string; + private _title: string | undefined; - constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string, options: Options | undefined) { + constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string, title: string | undefined) { this._config = config; - this._options = options; this._reportFolder = outputDir; fs.mkdirSync(this._reportFolder, { recursive: true }); this._dataZipFile = new yazl.ZipFile(); this._attachmentsBaseURL = attachmentsBaseURL; + this._title = title; } async build(metadata: Metadata, projectSuites: api.Suite[], result: api.FullResult, topLevelErrors: api.TestError[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { @@ -294,9 +301,7 @@ class HtmlBuilder { } const htmlReport: HTMLReport = { metadata, - options: { - title: this._options?.title, - }, + title: this._title, startTime: result.startTime.getTime(), duration: result.duration, files: [...data.values()].map(e => e.testFileSummary), diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index d4ff4cecfe951..e2fdb56d61a3d 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -453,7 +453,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { expect(result.passed).toBe(1); await showReport(); - await expect(page.locator('.report-title')).toHaveText('Custom report title'); + await expect(page.locator('.header-title')).toHaveText('Custom report title'); }); test('should include stdio', async ({ runInlineTest, page, showReport }) => { From 22900af79f16b8d167e7e98f477be39e884727aa Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 22 Apr 2025 12:15:03 -0700 Subject: [PATCH 4/5] Additional minor changes --- packages/html-reporter/src/reportView.css | 18 ------------------ packages/html-reporter/src/testFilesView.tsx | 1 - tests/playwright-test/reporter-html.spec.ts | 6 +++--- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/packages/html-reporter/src/reportView.css b/packages/html-reporter/src/reportView.css index 05d32f20342c6..ae5a633c2466c 100644 --- a/packages/html-reporter/src/reportView.css +++ b/packages/html-reporter/src/reportView.css @@ -29,24 +29,6 @@ body { width: 100%; } -.report-body { - border-radius: 6px; - margin: 12px 0 24px 0; -} - -.report-title { - flex: none; - padding: 8px; - font-weight: 400; - font-size: 32px !important; - line-height: 1.25 !important; -} - -.report-title.metadata-visible { - /* Mirror bottom margin from following chip (.chip-header) so the title appears aligned */ - margin-top: 12px; -} - .test-file-test:not(:first-child) { border-top: 1px solid var(--color-border-default); } diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx index 8c28900bde750..e12f9ca01ae06 100644 --- a/packages/html-reporter/src/testFilesView.tsx +++ b/packages/html-reporter/src/testFilesView.tsx @@ -23,7 +23,6 @@ import { AutoChip } from './chip'; import { TestErrorView } from './testErrorView'; import * as icons from './icons'; import { isMetadataEmpty, MetadataView } from './metadataView'; -import { clsx } from '@web/uiUtils'; import { HeaderTitleView } from './headerView'; export const TestFilesView: React.FC<{ diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index e2fdb56d61a3d..24b1f89a3dbe1 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -1840,7 +1840,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { const testTitle = page.locator('.test-file-test .test-file-title', { hasText: `${tag} passes` }); await testTitle.click(); - await expect(page.locator('.report-title', { hasText: `${tag} passes` })).toBeVisible(); + await expect(page.locator('.header-title', { hasText: `${tag} passes` })).toBeVisible(); await expect(page.locator('.label', { hasText: tag })).toBeVisible(); await page.goBack(); @@ -2362,7 +2362,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { await notificationsChromiumTestCase.locator('.test-file-title').click(); await expect(page).toHaveURL(/testId/); await expect(page.locator('.test-case-path')).toHaveText('Root describe › @Notifications'); - await expect(page.locator('.report-title')).toHaveText('Test failed -- @call @call-details @e2e @regression #VQ458'); + await expect(page.locator('.header-title')).toHaveText('Test failed -- @call @call-details @e2e @regression #VQ458'); await expect(page.locator('.label')).toHaveText(['chromium', 'Notifications', 'call', 'call-details', 'e2e', 'regression']); await page.goBack(); @@ -2374,7 +2374,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { await monitoringFirefoxTestCase.locator('.test-file-title').click(); await expect(page).toHaveURL(/testId/); await expect(page.locator('.test-case-path')).toHaveText('Root describe › @Monitoring'); - await expect(page.locator('.report-title')).toHaveText('Test passed -- @call @call-details @e2e @regression #VQ457'); + await expect(page.locator('.header-title')).toHaveText('Test passed -- @call @call-details @e2e @regression #VQ457'); await expect(page.locator('.label')).toHaveText(['firefox', 'Monitoring', 'call', 'call-details', 'e2e', 'regression']); }); }); From c48b577120ae223b146f5c8e956b61fdbe0893b1 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 22 Apr 2025 12:24:53 -0700 Subject: [PATCH 5/5] Remove new title div after merge --- packages/html-reporter/src/reportView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index 4a49195dc02f6..69fe0e6281a29 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import './colors.css'; import './common.css'; import { Filter } from './filter'; -import { HeaderView } from './headerView'; +import { HeaderTitleView, HeaderView } from './headerView'; import { Route, SearchParamsContext } from './links'; import type { LoadedReport } from './loadedReport'; import './reportView.css'; @@ -136,7 +136,7 @@ const TestCaseViewLoader: React.FC<{ if (test === 'not-found') { return
-
Test not found
+
Test ID: {testId}
; }