Skip to content

feat(html): allow setting a title to display #35659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/test-reporters-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
8 changes: 8 additions & 0 deletions packages/html-reporter/src/headerView.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/html-reporter/src/headerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export const HeaderView: React.FC<{
</>);
};

export const HeaderTitleView: React.FC<{ title: string }> = ({ title }) => <div className='header-title'>{title}</div>;

const StatsNavView: React.FC<{
stats: Stats
}> = ({ stats }) => {
Expand Down
13 changes: 11 additions & 2 deletions packages/html-reporter/src/reportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 <div className='htmlreport vbox px-4 pb-4'>
<main>
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
Expand Down Expand Up @@ -127,7 +136,7 @@ const TestCaseViewLoader: React.FC<{

if (test === 'not-found') {
return <div className='test-case-column vbox'>
<div className='test-case-title'>Test not found</div>
<HeaderTitleView title='Test not found' />
<div className='test-case-location'>Test ID: {testId}</div>
</div>;
}
Expand Down
8 changes: 0 additions & 8 deletions packages/html-reporter/src/testCaseView.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion packages/html-reporter/src/testCaseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -50,7 +51,7 @@ export const TestCaseView: React.FC<{
<div style={{ width: 10 }}></div>
<div className={clsx(!next && 'hidden')}><Link href={testResultHref({ test: next }) + filterParam}>next »</Link></div>
</div>
<div className='test-case-title'>{test.title}</div>
<HeaderTitleView title={test.title} />
<div className='hbox'>
<div className='test-case-location'>
<CopyToClipboardContainer value={`${test.location.file}:${test.location.line}`}>
Expand Down
2 changes: 2 additions & 0 deletions packages/html-reporter/src/testFilesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down Expand Up @@ -83,6 +84,7 @@ export const TestFilesHeader: React.FC<{
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
</div>
{metadataVisible && <MetadataView metadata={report.metadata}/>}
{report.title && <HeaderTitleView title={report.title} />}
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
</AutoChip>}
Expand Down
1 change: 1 addition & 0 deletions packages/html-reporter/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type Location = {

export type HTMLReport = {
metadata: Metadata;
title: string | undefined;
files: TestFileSummary[];
stats: Stats;
projectNames: string[];
Expand Down
15 changes: 11 additions & 4 deletions packages/playwright/src/reporters/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type HtmlReporterOptions = {
host?: string,
port?: number,
attachmentsBaseURL?: string,
title?: string,
_mode?: 'test' | 'list';
_isTestServer?: boolean;
};
Expand All @@ -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[] = [];

Expand All @@ -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<string>();
for (const project of this.config.projects) {
if (this._isSubdirectory(outputFolder, project.outputDir) || this._isSubdirectory(project.outputDir, outputFolder)) {
Expand All @@ -112,14 +115,15 @@ 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,
open: getHtmlReportOptionProcessEnv() || this._options.open || 'on-failure',
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,
};
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -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 }> {
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
>;
Expand Down
27 changes: 24 additions & 3 deletions tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': `
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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']);
});
});
Expand Down
2 changes: 1 addition & 1 deletion utils/generate_types/overrides-test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
>;
Expand Down
Loading