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