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/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 aa3a219a25877..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'; @@ -72,6 +72,15 @@ export const ReportView: React.FC<{ return result; }, [report, filter]); + const reportTitle = report?.json()?.title; + + React.useEffect(() => { + if (reportTitle) + document.title = reportTitle; + else + document.title = 'Playwright Test Report'; + }, [reportTitle]); + return
{report?.json() && } @@ -127,7 +136,7 @@ const TestCaseViewLoader: React.FC<{ if (test === 'not-found') { return
-
Test not found
+
Test ID: {testId}
; } diff --git a/packages/html-reporter/src/testCaseView.css b/packages/html-reporter/src/testCaseView.css index 9ae47b179d249..fd8bdf7eca81c 100644 --- a/packages/html-reporter/src/testCaseView.css +++ b/packages/html-reporter/src/testCaseView.css @@ -34,14 +34,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 16d4900725a11..4149e02e2abae 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[], @@ -50,7 +51,7 @@ export const TestCaseView: React.FC<{
next »
-
{test.title}
+
diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx index 4b2c48ae1d84e..e12f9ca01ae06 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 { HeaderTitleView } from './headerView'; export const TestFilesView: React.FC<{ tests: TestFileSummary[], @@ -83,6 +84,7 @@ export const TestFilesHeader: React.FC<{
Total time: {msToString(report.duration ?? 0)}
{metadataVisible && } + {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 17c5a3b3730a4..c5897836a39b1 100644 --- a/packages/html-reporter/src/types.d.ts +++ b/packages/html-reporter/src/types.d.ts @@ -38,6 +38,7 @@ export type Location = { export type HTMLReport = { metadata: Metadata; + 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 8e057d81931ff..8c3c2fd6c2522 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -54,6 +54,7 @@ type HtmlReporterOptions = { host?: string, port?: number, attachmentsBaseURL?: string, + title?: string, _mode?: 'test' | 'list'; _isTestServer?: boolean; }; @@ -67,6 +68,7 @@ class HtmlReporter implements ReporterV2 { 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,12 +89,13 @@ class HtmlReporter implements ReporterV2 { } onBegin(suite: api.Suite) { - const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions(); + 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)) { @@ -112,7 +115,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 +123,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, }; } @@ -135,7 +139,7 @@ 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); + const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, this._title); this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors); } @@ -232,13 +236,15 @@ class HtmlBuilder { private _dataZipFile: ZipFile; private _hasTraces = false; private _attachmentsBaseURL: string; + private _title: string | undefined; - constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string) { + constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string, title: string | undefined) { this._config = config; 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 }> { @@ -295,6 +301,7 @@ class HtmlBuilder { } const htmlReport: HTMLReport = { metadata, + title: this._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..24b1f89a3dbe1 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('.header-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('.header-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('.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(); @@ -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('.header-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] >;