- ;
+ 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();
});