mirror of
https://github.com/microsoft/playwright.git
synced 2024-11-28 09:23:42 +03:00
chore: include start/endTime and duration in onEnd report callback (#26760)
Fixes https://github.com/microsoft/playwright/issues/23637
This commit is contained in:
parent
a9bc1a1707
commit
34c6197f9e
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -41,10 +41,11 @@ export const TestFilesView: React.FC<{
|
||||
return result;
|
||||
}, [report, filter]);
|
||||
return <>
|
||||
<div className='p-2' style={{ display: 'flex' }}>
|
||||
<div className='mt-2 mx-1' style={{ display: 'flex' }}>
|
||||
{projectNames.length === 1 && !!projectNames[0] && <div data-testid="project-name" style={{ color: 'var(--color-fg-subtle)' }}>Project: {projectNames[0]}</div>}
|
||||
{!filter.empty() && <div data-testid="filtered-tests-count" style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total}</div>}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div data-testid="overall-time" style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||
<div data-testid="overall-duration" style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(filteredStats.duration)}</div>
|
||||
</div>
|
||||
{report && filteredFiles.map(({ file, defaultExpanded }) => {
|
||||
|
@ -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 = {
|
||||
|
@ -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> | void {
|
||||
return this._reporter.onEnd?.(result);
|
||||
private _onEnd(result: JsonFullResult): Promise<void> | void {
|
||||
return this._reporter.onEnd?.({
|
||||
status: result.status,
|
||||
startTime: new Date(result.startTime),
|
||||
duration: result.duration,
|
||||
});
|
||||
}
|
||||
|
||||
private _onExit(): Promise<void> | void {
|
||||
|
@ -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<string, number>();
|
||||
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)
|
||||
|
@ -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<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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: {
|
||||
|
@ -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() {
|
||||
|
@ -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<TestRun> {
|
||||
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 ({}) => {},
|
||||
};
|
||||
}
|
||||
|
||||
|
10
packages/playwright-test/types/testReporter.d.ts
vendored
10
packages/playwright-test/types/testReporter.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
10
utils/generate_types/overrides-testReporter.d.ts
vendored
10
utils/generate_types/overrides-testReporter.d.ts
vendored
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user