diff --git a/docs/src/test-reporter-api/class-reporter.md b/docs/src/test-reporter-api/class-reporter.md
index 7e3d5cfd43..d2c8b111b3 100644
--- a/docs/src/test-reporter-api/class-reporter.md
+++ b/docs/src/test-reporter-api/class-reporter.md
@@ -113,6 +113,8 @@ Called after all tests have been run, or testing has been interrupted. Note that
* since: v1.10
- `result` <[Object]>
- `status` <[FullStatus]<"passed"|"failed"|"timedout"|"interrupted">>
+ - `startTime` <[Date]>
+ - `duration` <[int]>
Result of the full test run.
* `'passed'` - Everything went as expected.
diff --git a/packages/html-reporter/src/chip.css b/packages/html-reporter/src/chip.css
index f8d7938f90..1b40275166 100644
--- a/packages/html-reporter/src/chip.css
+++ b/packages/html-reporter/src/chip.css
@@ -21,7 +21,7 @@
background-color: var(--color-canvas-subtle);
padding: 0 8px;
border-bottom: none;
- margin-top: 24px;
+ margin-top: 12px;
font-weight: 600;
line-height: 38px;
white-space: nowrap;
@@ -44,6 +44,7 @@
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
padding: 16px;
+ margin-bottom: 12px;
}
.chip-body-no-insets {
diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx
index b6a57fdd5e..c0154af4d6 100644
--- a/packages/html-reporter/src/testFilesView.tsx
+++ b/packages/html-reporter/src/testFilesView.tsx
@@ -41,10 +41,11 @@ export const TestFilesView: React.FC<{
return result;
}, [report, filter]);
return <>
-
+
{projectNames.length === 1 && !!projectNames[0] &&
Project: {projectNames[0]}
}
{!filter.empty() &&
Filtered: {filteredStats.total}
}
+
{report ? new Date(report.startTime).toLocaleString() : ''}
Total time: {msToString(filteredStats.duration)}
{report && filteredFiles.map(({ file, defaultExpanded }) => {
diff --git a/packages/html-reporter/src/types.ts b/packages/html-reporter/src/types.ts
index 65db7d7716..021218abc5 100644
--- a/packages/html-reporter/src/types.ts
+++ b/packages/html-reporter/src/types.ts
@@ -23,11 +23,10 @@ export type Stats = {
flaky: number;
skipped: number;
ok: boolean;
- duration: number;
};
export type FilteredStats = {
- total: number
+ total: number,
duration: number,
};
@@ -42,6 +41,8 @@ export type HTMLReport = {
files: TestFileSummary[];
stats: Stats;
projectNames: string[];
+ startTime: number;
+ duration: number;
};
export type TestFile = {
diff --git a/packages/playwright-test/src/isomorphic/teleReceiver.ts b/packages/playwright-test/src/isomorphic/teleReceiver.ts
index 2bda3d1b01..a7e818e6ea 100644
--- a/packages/playwright-test/src/isomorphic/teleReceiver.ts
+++ b/packages/playwright-test/src/isomorphic/teleReceiver.ts
@@ -115,6 +115,12 @@ export type JsonTestStepEnd = {
error?: TestError;
};
+export type JsonFullResult = {
+ status: FullResult['status'];
+ startTime: number;
+ duration: number;
+};
+
export type JsonEvent = {
method: string;
params: any
@@ -300,8 +306,12 @@ export class TeleReporterReceiver {
}
}
- private _onEnd(result: FullResult): Promise
| void {
- return this._reporter.onEnd?.(result);
+ private _onEnd(result: JsonFullResult): Promise | void {
+ return this._reporter.onEnd?.({
+ status: result.status,
+ startTime: new Date(result.startTime),
+ duration: result.duration,
+ });
}
private _onExit(): Promise | void {
diff --git a/packages/playwright-test/src/reporters/base.ts b/packages/playwright-test/src/reporters/base.ts
index a8c8688962..205e58da09 100644
--- a/packages/playwright-test/src/reporters/base.ts
+++ b/packages/playwright-test/src/reporters/base.ts
@@ -18,7 +18,7 @@ import { colors, ms as milliseconds, parseStackTraceLine } from 'playwright-core
import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location } from '../../types/testReporter';
import type { SuitePrivate } from '../../types/reporterPrivate';
-import { getPackageManagerExecCommand, monotonicTime } from 'playwright-core/lib/utils';
+import { getPackageManagerExecCommand } from 'playwright-core/lib/utils';
import type { ReporterV2 } from './reporterV2';
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
export const kOutputSymbol = Symbol('output');
@@ -45,13 +45,11 @@ type TestSummary = {
};
export class BaseReporter implements ReporterV2 {
- duration = 0;
config!: FullConfig;
suite!: Suite;
totalTestCount = 0;
result!: FullResult;
private fileDurations = new Map();
- private monotonicStartTime: number = 0;
private _omitFailures: boolean;
private readonly _ttyWidthForTest: number;
private _fatalErrors: TestError[] = [];
@@ -71,7 +69,6 @@ export class BaseReporter implements ReporterV2 {
}
onBegin(suite: Suite) {
- this.monotonicStartTime = monotonicTime();
this.suite = suite;
this.totalTestCount = suite.allTests().length;
}
@@ -114,7 +111,6 @@ export class BaseReporter implements ReporterV2 {
}
async onEnd(result: FullResult) {
- this.duration = monotonicTime() - this.monotonicStartTime;
this.result = result;
}
@@ -182,7 +178,7 @@ export class BaseReporter implements ReporterV2 {
if (skipped)
tokens.push(colors.yellow(` ${skipped} skipped`));
if (expected)
- tokens.push(colors.green(` ${expected} passed`) + colors.dim(` (${milliseconds(this.duration)})`));
+ tokens.push(colors.green(` ${expected} passed`) + colors.dim(` (${milliseconds(this.result.duration)})`));
if (this.result.status === 'timedout')
tokens.push(colors.red(` Timed out waiting ${this.config.globalTimeout / 1000}s for the entire test run`));
if (fatalErrors.length && expected + unexpected.length + interrupted.length + flaky.length > 0)
diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts
index c3130c6817..396518da83 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 { toPosixPath } from './json';
import { codeFrameColumns } from '../transform/babelBundle';
-import type { FullConfig, Location, Suite, TestCase as TestCasePublic, TestResult as TestResultPublic, TestStep as TestStepPublic } from '../../types/testReporter';
+import type { FullResult, FullConfig, Location, Suite, TestCase as TestCasePublic, TestResult as TestResultPublic, TestStep as TestStepPublic } from '../../types/testReporter';
import type { SuitePrivate } from '../../types/reporterPrivate';
import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath } from 'playwright-core/lib/utils';
import { formatResultFailure, stripAnsiEscapes } from './base';
@@ -40,7 +40,6 @@ type TestEntry = {
testCaseSummary: TestCaseSummary
};
-
const htmlReportOptions = ['always', 'never', 'on-failure'];
type HtmlReportOpenOption = (typeof htmlReportOptions)[number];
@@ -112,11 +111,11 @@ class HtmlReporter extends EmptyReporter {
};
}
- override async onEnd() {
+ override async onEnd(result: FullResult) {
const projectSuites = this.suite.suites;
await removeFolders([this._outputFolder]);
const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
- this._buildResult = await builder.build(this.config.metadata, projectSuites);
+ this._buildResult = await builder.build(this.config.metadata, projectSuites, result);
}
override async onExit() {
@@ -218,7 +217,7 @@ class HtmlBuilder {
this._attachmentsBaseURL = attachmentsBaseURL;
}
- async build(metadata: Metadata, projectSuites: Suite[]): Promise<{ ok: boolean, singleTestId: string | undefined }> {
+ async build(metadata: Metadata, projectSuites: Suite[], result: FullResult): Promise<{ ok: boolean, singleTestId: string | undefined }> {
const data = new Map();
for (const projectSuite of projectSuites) {
@@ -257,7 +256,6 @@ class HtmlBuilder {
if (test.outcome === 'flaky')
++stats.flaky;
++stats.total;
- stats.duration += test.duration;
}
stats.ok = stats.unexpected + stats.flaky === 0;
if (!stats.ok)
@@ -274,9 +272,11 @@ class HtmlBuilder {
}
const htmlReport: HTMLReport = {
metadata,
+ startTime: result.startTime.getTime(),
+ duration: result.duration,
files: [...data.values()].map(e => e.testFileSummary),
projectNames: projectSuites.map(r => r.project()!.name),
- stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()), duration: metadata.totalTime }
+ stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) }
};
htmlReport.files.sort((f1, f2) => {
const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky;
@@ -501,7 +501,6 @@ const emptyStats = (): Stats => {
flaky: 0,
skipped: 0,
ok: true,
- duration: 0,
};
};
@@ -512,7 +511,6 @@ const addStats = (stats: Stats, delta: Stats): Stats => {
stats.unexpected += delta.unexpected;
stats.flaky += delta.flaky;
stats.ok = stats.ok && delta.ok;
- stats.duration += delta.duration;
return stats;
};
diff --git a/packages/playwright-test/src/reporters/internalReporter.ts b/packages/playwright-test/src/reporters/internalReporter.ts
index a796803a47..4075e92c48 100644
--- a/packages/playwright-test/src/reporters/internalReporter.ts
+++ b/packages/playwright-test/src/reporters/internalReporter.ts
@@ -21,11 +21,14 @@ import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep
import { Suite } from '../common/test';
import { prepareErrorStack, relativeFilePath } from './base';
import type { ReporterV2 } from './reporterV2';
+import { monotonicTime } from 'playwright-core/lib/utils';
-export class InternalReporter implements ReporterV2 {
+export class InternalReporter {
private _reporter: ReporterV2;
private _didBegin = false;
private _config!: FullConfig;
+ private _startTime: Date | undefined;
+ private _monotonicStartTime: number | undefined;
constructor(reporter: ReporterV2) {
this._reporter = reporter;
@@ -37,6 +40,8 @@ export class InternalReporter implements ReporterV2 {
onConfigure(config: FullConfig) {
this._config = config;
+ this._startTime = new Date();
+ this._monotonicStartTime = monotonicTime();
this._reporter.onConfigure(config);
}
@@ -62,12 +67,16 @@ export class InternalReporter implements ReporterV2 {
this._reporter.onTestEnd(test, result);
}
- async onEnd(result: FullResult) {
+ async onEnd(result: { status: FullResult['status'] }) {
if (!this._didBegin) {
// onBegin was not reported, emit it.
this.onBegin(new Suite('', 'root'));
}
- await this._reporter.onEnd(result);
+ await this._reporter.onEnd({
+ ...result,
+ startTime: this._startTime!,
+ duration: monotonicTime() - this._monotonicStartTime!,
+ });
}
async onExit() {
diff --git a/packages/playwright-test/src/reporters/merge.ts b/packages/playwright-test/src/reporters/merge.ts
index 448c050033..e114455eab 100644
--- a/packages/playwright-test/src/reporters/merge.ts
+++ b/packages/playwright-test/src/reporters/merge.ts
@@ -17,9 +17,8 @@
import fs from 'fs';
import path from 'path';
import type { ReporterDescription } from '../../types/test';
-import type { FullResult } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
-import type { JsonConfig, JsonEvent, JsonProject, JsonSuite, JsonTestResultEnd } from '../isomorphic/teleReceiver';
+import type { JsonConfig, JsonEvent, JsonFullResult, JsonProject, JsonSuite, JsonTestResultEnd } from '../isomorphic/teleReceiver';
import { TeleReporterReceiver } from '../isomorphic/teleReceiver';
import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool';
import { createReporters } from '../runner/reporters';
@@ -228,7 +227,6 @@ function mergeConfigureEvents(configureEvents: JsonEvent[]): JsonEvent {
globalTimeout: 0,
maxFailures: 0,
metadata: {
- totalTime: 0,
},
rootDir: '',
version: '',
@@ -252,7 +250,6 @@ function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig {
metadata: {
...to.metadata,
...from.metadata,
- totalTime: to.metadata.totalTime + from.metadata.totalTime,
actualWorkers: (to.metadata.actualWorkers || 0) + (from.metadata.actualWorkers || 0),
},
workers: to.workers + from.workers,
@@ -260,16 +257,26 @@ function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig {
}
function mergeEndEvents(endEvents: JsonEvent[]): JsonEvent {
- const result: FullResult = { status: 'passed' };
+ let startTime = endEvents.length ? 10000000000000 : Date.now();
+ let status: JsonFullResult['status'] = 'passed';
+ let duration: number = 0;
+
for (const event of endEvents) {
- const shardResult: FullResult = event.params.result;
+ const shardResult: JsonFullResult = event.params.result;
if (shardResult.status === 'failed')
- result.status = 'failed';
- else if (shardResult.status === 'timedout' && result.status !== 'failed')
- result.status = 'timedout';
- else if (shardResult.status === 'interrupted' && result.status !== 'failed' && result.status !== 'timedout')
- result.status = 'interrupted';
+ status = 'failed';
+ else if (shardResult.status === 'timedout' && status !== 'failed')
+ status = 'timedout';
+ else if (shardResult.status === 'interrupted' && status !== 'failed' && status !== 'timedout')
+ status = 'interrupted';
+ startTime = Math.min(startTime, shardResult.startTime);
+ duration = Math.max(duration, shardResult.duration);
}
+ const result: JsonFullResult = {
+ status,
+ startTime,
+ duration,
+ };
return {
method: 'onEnd',
params: {
diff --git a/packages/playwright-test/src/reporters/teleEmitter.ts b/packages/playwright-test/src/reporters/teleEmitter.ts
index 04e41102c6..113856ce62 100644
--- a/packages/playwright-test/src/reporters/teleEmitter.ts
+++ b/packages/playwright-test/src/reporters/teleEmitter.ts
@@ -20,7 +20,7 @@ import type { SuitePrivate } from '../../types/reporterPrivate';
import type { FullConfig, FullResult, Location, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
import { FullConfigInternal, getProjectId } from '../common/config';
import type { Suite } from '../common/test';
-import type { JsonAttachment, JsonConfig, JsonEvent, JsonProject, JsonStdIOType, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
+import type { JsonAttachment, JsonConfig, JsonEvent, JsonFullResult, JsonProject, JsonStdIOType, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
import type { ReporterV2 } from './reporterV2';
@@ -125,7 +125,17 @@ export class TeleReporterEmitter implements ReporterV2 {
}
async onEnd(result: FullResult) {
- this._messageSink({ method: 'onEnd', params: { result } });
+ const resultPayload: JsonFullResult = {
+ status: result.status,
+ startTime: result.startTime.getTime(),
+ duration: result.duration,
+ };
+ this._messageSink({
+ method: 'onEnd',
+ params: {
+ result: resultPayload
+ }
+ });
}
async onExit() {
diff --git a/packages/playwright-test/src/runner/tasks.ts b/packages/playwright-test/src/runner/tasks.ts
index 8a5399680e..96982eebc0 100644
--- a/packages/playwright-test/src/runner/tasks.ts
+++ b/packages/playwright-test/src/runner/tasks.ts
@@ -29,7 +29,6 @@ import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGloba
import type { Matcher } from '../util';
import type { Suite } from '../common/test';
import { buildDependentProjects, buildTeardownToSetupsMap } from './projectUtils';
-import { monotonicTime } from 'playwright-core/lib/utils';
const readDirAsync = promisify(fs.readdir);
@@ -105,15 +104,11 @@ export function createTaskRunnerForList(config: FullConfigInternal, reporter: Re
}
function createReportBeginTask(): Task {
- let montonicStartTime = 0;
return {
- setup: async ({ config, reporter, rootSuite }) => {
- montonicStartTime = monotonicTime();
+ setup: async ({ reporter, rootSuite }) => {
reporter.onBegin(rootSuite!);
},
- teardown: async ({ config }) => {
- config.config.metadata.totalTime = monotonicTime() - montonicStartTime;
- },
+ teardown: async ({}) => {},
};
}
diff --git a/packages/playwright-test/types/testReporter.d.ts b/packages/playwright-test/types/testReporter.d.ts
index 45f2d6b312..9f18d3114e 100644
--- a/packages/playwright-test/types/testReporter.d.ts
+++ b/packages/playwright-test/types/testReporter.d.ts
@@ -305,6 +305,16 @@ export interface FullResult {
* - 'interrupted' - interrupted by the user.
*/
status: 'passed' | 'failed' | 'timedout' | 'interrupted';
+
+ /**
+ * Test start wall time.
+ */
+ startTime: Date;
+
+ /**
+ * Test duration in milliseconds.
+ */
+ duration: number;
}
/**
diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts
index af138a6902..2924625e54 100644
--- a/tests/playwright-test/reporter-blob.spec.ts
+++ b/tests/playwright-test/reporter-blob.spec.ts
@@ -996,7 +996,6 @@ test('preserve config fields', async ({ runInlineTest, mergeReports }) => {
expect(json.globalTimeout).toBe(config.globalTimeout);
expect(json.maxFailures).toBe(config.maxFailures);
expect(json.metadata).toEqual(expect.objectContaining(config.metadata));
- expect(json.metadata.totalTime).toBeTruthy();
expect(json.workers).toBe(2);
expect(json.version).toBeTruthy();
expect(json.version).not.toEqual(test.info().config.version);
diff --git a/utils/generate_types/overrides-testReporter.d.ts b/utils/generate_types/overrides-testReporter.d.ts
index 484c63a7f2..ead17e201c 100644
--- a/utils/generate_types/overrides-testReporter.d.ts
+++ b/utils/generate_types/overrides-testReporter.d.ts
@@ -41,6 +41,16 @@ export interface FullResult {
* - 'interrupted' - interrupted by the user.
*/
status: 'passed' | 'failed' | 'timedout' | 'interrupted';
+
+ /**
+ * Test start wall time.
+ */
+ startTime: Date;
+
+ /**
+ * Test duration in milliseconds.
+ */
+ duration: number;
}
export interface Reporter {