From 08443942701aefa959fcf928eb166dd1741e6fce Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Tue, 20 Dec 2022 17:13:10 -0500 Subject: [PATCH] feat(html): display overall duration (#19576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2022-12-19 at 4 15 33 PM * Adds duration (time ellapsed from `onBegin` to `onEnd`); roughly equivalent to `time npx playwright test …`. * Removes cumulative per-file time Resolves #19566. --- packages/html-reporter/src/headerView.tsx | 36 ++++++++++--------- packages/html-reporter/src/testFileView.tsx | 1 - packages/html-reporter/src/types.ts | 2 +- .../playwright-test/src/reporters/base.ts | 6 +--- .../playwright-test/src/reporters/html.ts | 11 +++--- packages/playwright-test/src/util.ts | 2 +- tests/playwright-test/reporter-html.spec.ts | 2 ++ 7 files changed, 32 insertions(+), 28 deletions(-) diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index d75c74d7cc..d5d6483a24 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -22,6 +22,7 @@ import './headerView.css'; import * as icons from './icons'; import { Link, navigate } from './links'; import { statusIcon } from './statusIcon'; +import { msToString } from './uiUtils'; export const HeaderView: React.FC -
- + return (<> +
+
+ +
+
{ + event.preventDefault(); + navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`); + } + }> + {icons.search()} + {/* Use navigationId to reset defaultValue */} + { + setFilterText(e.target.value); + }}> +
-
{ - event.preventDefault(); - navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`); - } - }> - {icons.search()} - {/* Use navigationId to reset defaultValue */} - { - setFilterText(e.target.value); - }}> -
-
; +
Total time: {msToString(stats.duration)}
+ ); }; const StatsNavView: React.FC<{ diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx index a382296b6d..c80f6535af 100644 --- a/packages/html-reporter/src/testFileView.tsx +++ b/packages/html-reporter/src/testFileView.tsx @@ -36,7 +36,6 @@ export const TestFileView: React.FC setFileExpanded(file.fileId, expanded))} header={ - {msToString(file.stats.duration)} {file.fileName} }> {file.tests.filter(t => filter.matches(t)).map(test => diff --git a/packages/html-reporter/src/types.ts b/packages/html-reporter/src/types.ts index ec6191bd12..5ecbb00086 100644 --- a/packages/html-reporter/src/types.ts +++ b/packages/html-reporter/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Metadata } from '@protocol/channels'; +import type { Metadata } from '@playwright/test'; export type Stats = { total: number; diff --git a/packages/playwright-test/src/reporters/base.ts b/packages/playwright-test/src/reporters/base.ts index 45b8de86ef..7aa6600f06 100644 --- a/packages/playwright-test/src/reporters/base.ts +++ b/packages/playwright-test/src/reporters/base.ts @@ -20,6 +20,7 @@ import path from 'path'; import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location } from '../../types/testReporter'; import type { FullConfigInternal, ReporterInternal } from '../types'; import { codeFrameColumns } from '../babelBundle'; +import { monotonicTime } from 'playwright-core/lib/utils'; export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' }; export const kOutputSymbol = Symbol('output'); @@ -447,11 +448,6 @@ export function prepareErrorStack(stack: string): { return { message, stackLines, location }; } -function monotonicTime(): number { - const [seconds, nanoseconds] = process.hrtime(); - return seconds * 1000 + (nanoseconds / 1000000 | 0); -} - const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g'); export function stripAnsiEscapes(str: string): string { return str.replace(ansiRegex, ''); diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index adbee0334f..8c2ef35fa5 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -22,7 +22,7 @@ import type { TransformCallback } from 'stream'; import { Transform } from 'stream'; import type { FullConfig, Suite } from '../../types/testReporter'; import { HttpServer } from 'playwright-core/lib/utils/httpServer'; -import { assert, calculateSha1 } from 'playwright-core/lib/utils'; +import { assert, calculateSha1, monotonicTime } from 'playwright-core/lib/utils'; import { copyFileAndMakeWritable, removeFolders } from 'playwright-core/lib/utils/fileUtils'; import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from './raw'; import RawReporter from './raw'; @@ -52,6 +52,7 @@ type HtmlReporterOptions = { class HtmlReporter implements ReporterInternal { private config!: FullConfigInternal; private suite!: Suite; + private _montonicStartTime: number = 0; private _options: HtmlReporterOptions; private _outputFolder!: string; private _open: string | undefined; @@ -66,6 +67,7 @@ class HtmlReporter implements ReporterInternal { } onBegin(config: FullConfig, suite: Suite) { + this._montonicStartTime = monotonicTime(); this.config = config as FullConfigInternal; const { outputFolder, open } = this._resolveOptions(); this._outputFolder = outputFolder; @@ -100,6 +102,7 @@ class HtmlReporter implements ReporterInternal { } async onEnd() { + const duration = monotonicTime() - this._montonicStartTime; const projectSuites = this.suite.suites; const reports = projectSuites.map(suite => { const rawReporter = new RawReporter(); @@ -108,7 +111,7 @@ class HtmlReporter implements ReporterInternal { }); await removeFolders([this._outputFolder]); const builder = new HtmlBuilder(this._outputFolder); - this._buildResult = await builder.build(this.config.metadata, reports); + this._buildResult = await builder.build({ ...this.config.metadata, duration }, reports); } async _onExit() { @@ -201,7 +204,7 @@ class HtmlBuilder { this._dataZipFile = new yazl.ZipFile(); } - async build(metadata: Metadata, rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { + async build(metadata: Metadata & { duration: number }, rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { const data = new Map(); for (const projectJson of rawReports) { @@ -260,7 +263,7 @@ class HtmlBuilder { metadata, files: [...data.values()].map(e => e.testFileSummary), projectNames: rawReports.map(r => r.project.name), - stats: [...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) + stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()), duration: metadata.duration } }; htmlReport.files.sort((f1, f2) => { const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky; diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index d67620b5d5..aed694a4bb 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -301,4 +301,4 @@ export async function normalizeAndSaveAttachment(outputPath: string, name: strin const contentType = options.contentType ?? (typeof options.body === 'string' ? 'text/plain' : 'application/octet-stream'); return { name, contentType, body: typeof options.body === 'string' ? Buffer.from(options.body) : options.body }; } -} +} \ No newline at end of file diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 157c7428f5..194d8981eb 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -70,6 +70,8 @@ test('should generate report', async ({ runInlineTest, showReport, page }) => { await expect(page.locator('.test-file-test-outcome-expected >> text=passes')).toBeVisible(); await expect(page.locator('.test-file-test-outcome-skipped >> text=skipped')).toBeVisible(); + await expect(page.getByTestId('overall-duration')).toContainText(/^Total time: \d+(\.\d+)?(ms|s|m)$/); // e.g. 1.2s + await expect(page.locator('.metadata-view')).not.toBeVisible(); });