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]
>;