mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(html): allow setting a title to display (#35659)
This commit is contained in:
parent
ed23a93512
commit
9b59a6aea6
@ -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`
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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 <div className='htmlreport vbox px-4 pb-4'>
|
||||
<main>
|
||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -28,6 +28,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[],
|
||||
@ -51,7 +52,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}`}>
|
||||
|
||||
@ -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<{
|
||||
<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>}
|
||||
|
||||
1
packages/html-reporter/src/types.d.ts
vendored
1
packages/html-reporter/src/types.d.ts
vendored
@ -38,6 +38,7 @@ export type Location = {
|
||||
|
||||
export type HTMLReport = {
|
||||
metadata: Metadata;
|
||||
title: string | undefined;
|
||||
files: TestFileSummary[];
|
||||
stats: Stats;
|
||||
projectNames: string[];
|
||||
|
||||
@ -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<string>();
|
||||
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),
|
||||
|
||||
2
packages/playwright/types/test.d.ts
vendored
2
packages/playwright/types/test.d.ts
vendored
@ -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]
|
||||
>;
|
||||
|
||||
@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
2
utils/generate_types/overrides-test.d.ts
vendored
2
utils/generate_types/overrides-test.d.ts
vendored
@ -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]
|
||||
>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user