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 1 commit
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
18 changes: 18 additions & 0 deletions packages/html-reporter/src/reportView.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
32 changes: 23 additions & 9 deletions packages/html-reporter/src/reportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div className='htmlreport vbox px-4 pb-4'>
<main>
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
{json && <HeaderView stats={json.stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
<Route predicate={testFilesRoutePredicate}>
<TestFilesHeader report={report?.json()} filteredStats={filteredStats} metadataVisible={metadataVisible} toggleMetadataVisible={() => setMetadataVisible(visible => !visible)}/>
<TestFilesView
tests={filteredTests.files}
expandedFiles={expandedFiles}
setExpandedFiles={setExpandedFiles}
projectNames={report?.json().projectNames || []}
/>
<Body>
<TestFilesHeader report={json} filteredStats={filteredStats} metadataVisible={metadataVisible} toggleMetadataVisible={() => setMetadataVisible(visible => !visible)}/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is everything changes?

<TestFilesView
tests={filteredTests.files}
expandedFiles={expandedFiles}
setExpandedFiles={setExpandedFiles}
projectNames={json?.projectNames || []}
/>
</Body>
</Route>
<Route predicate={testCaseRoutePredicate}>
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
{!!report && <Body><TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} /></Body>}
</Route>
</main>
</div>;
};

const Body: React.FC<React.PropsWithChildren<{}>> = ({ children }) => <div className='report-body'>{children}</div>;

const TestCaseViewLoader: React.FC<{
report: LoadedReport,
tests: TestCaseSummary[],
Expand Down
13 changes: 0 additions & 13 deletions packages/html-reporter/src/testCaseView.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/html-reporter/src/testCaseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,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>}
{test && <div className='test-case-title'>{test?.title}</div>}
{test && <div className='report-title'>{test?.title}</div>}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have it with a neutral name in the common.css

{test && <div className='hbox'>
<div className='test-case-location'>
<CopyToClipboardContainer value={`${test?.location.file}:${test?.location.line}`}>
Expand Down
6 changes: 5 additions & 1 deletion 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 { clsx } from '@web/uiUtils';

export const TestFilesView: React.FC<{
tests: TestFileSummary[],
Expand Down Expand Up @@ -70,7 +71,7 @@ export const TestFilesHeader: React.FC<{
if (!report)
return null;
return <>
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
<div className='mx-1' style={{ display: 'flex' }}>
<div className='test-file-header-info'>
{!isMetadataEmpty(report.metadata) && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
Expand All @@ -83,6 +84,9 @@ 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.options.title && <div className={clsx('report-title', metadataVisible && 'metadata-visible')}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure the titles is above the metadata.

{report.options.title}
</div>}
{!!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
5 changes: 5 additions & 0 deletions packages/html-reporter/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
41 changes: 20 additions & 21 deletions packages/playwright/src/reporters/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -54,6 +54,7 @@ type HtmlReporterOptions = {
host?: string,
port?: number,
attachmentsBaseURL?: string,
title?: string,
_mode?: 'test' | 'list';
_isTestServer?: boolean;
};
Expand All @@ -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[] = [];

Expand All @@ -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<string>();
for (const project of this.config.projects) {
if (this._isSubdirectory(outputFolder, project.outputDir) || this._isSubdirectory(project.outputDir, outputFolder)) {
Expand All @@ -112,14 +105,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 @@ -134,23 +128,23 @@ 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);
}

async onExit() {
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(`
Expand Down Expand Up @@ -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<string, TestStep>();
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();
Expand Down Expand Up @@ -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),
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('.report-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('.report-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('.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();
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('.report-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