chore: make ReporterV2 a partial interface (#32532)

This makes it easier to write reporters by avoding empty methods.
This commit is contained in:
Dmitry Gozman 2024-09-10 06:08:54 -07:00 committed by GitHub
parent ec40890fd8
commit b5d968fa0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 114 additions and 184 deletions

View file

@ -133,12 +133,12 @@ export class TeleReporterReceiver {
public isListing = false; public isListing = false;
private _rootSuite: TeleSuite; private _rootSuite: TeleSuite;
private _options: TeleReporterReceiverOptions; private _options: TeleReporterReceiverOptions;
private _reporter: Partial<ReporterV2>; private _reporter: ReporterV2;
private _tests = new Map<string, TeleTestCase>(); private _tests = new Map<string, TeleTestCase>();
private _rootDir!: string; private _rootDir!: string;
private _config!: reporterTypes.FullConfig; private _config!: reporterTypes.FullConfig;
constructor(reporter: Partial<ReporterV2>, options: TeleReporterReceiverOptions = {}) { constructor(reporter: ReporterV2, options: TeleReporterReceiverOptions = {}) {
this._rootSuite = new TeleSuite('', 'root'); this._rootSuite = new TeleSuite('', 'root');
this._options = options; this._options = options;
this._reporter = reporter; this._reporter = reporter;

View file

@ -77,6 +77,7 @@ export class TeleSuiteUpdater {
// To work around that, have a dedicated per-run receiver that will only have // To work around that, have a dedicated per-run receiver that will only have
// suite for a single test run, and hence will have correct total. // suite for a single test run, and hence will have correct total.
this._lastRunReceiver = new TeleReporterReceiver({ this._lastRunReceiver = new TeleReporterReceiver({
version: () => 'v2',
onBegin: (suite: reporterTypes.Suite) => { onBegin: (suite: reporterTypes.Suite) => {
this._lastRunTestCount = suite.allTests().length; this._lastRunTestCount = suite.allTests().length;
this._lastRunReceiver = undefined; this._lastRunReceiver = undefined;
@ -127,20 +128,13 @@ export class TeleSuiteUpdater {
onError: (error: reporterTypes.TestError) => this._handleOnError(error), onError: (error: reporterTypes.TestError) => this._handleOnError(error),
printsToStdio: () => { printsToStdio: () => false,
return false;
},
onStdOut: () => { },
onStdErr: () => { },
onExit: () => { },
onStepBegin: () => { },
onStepEnd: () => { },
}; };
} }
processGlobalReport(report: any[]) { processGlobalReport(report: any[]) {
const receiver = new TeleReporterReceiver({ const receiver = new TeleReporterReceiver({
version: () => 'v2',
onConfigure: (c: reporterTypes.FullConfig) => { onConfigure: (c: reporterTypes.FullConfig) => {
this.config = c; this.config = c;
}, },

View file

@ -123,9 +123,6 @@ export class BaseReporter implements ReporterV2 {
(result as any)[kOutputSymbol].push(output); (result as any)[kOutputSymbol].push(output);
} }
onTestBegin(test: TestCase, result: TestResult): void {
}
onTestEnd(test: TestCase, result: TestResult) { onTestEnd(test: TestCase, result: TestResult) {
if (result.status !== 'skipped' && result.status !== test.expectedStatus) if (result.status !== 'skipped' && result.status !== test.expectedStatus)
++this._failureCount; ++this._failureCount;
@ -146,19 +143,6 @@ export class BaseReporter implements ReporterV2 {
this.result = result; this.result = result;
} }
onStepBegin(test: TestCase, result: TestResult, step: TestStep): void {
}
onStepEnd(test: TestCase, result: TestResult, step: TestStep): void {
}
async onExit() {
}
printsToStdio() {
return true;
}
protected fitToScreen(line: string, prefix?: string): string { protected fitToScreen(line: string, prefix?: string): string {
if (!ttyWidth) { if (!ttyWidth) {
// Guard against the case where we cannot determine available width. // Guard against the case where we cannot determine available width.

View file

@ -20,10 +20,6 @@ import type { FullResult, TestCase, TestResult, Suite, TestError } from '../../t
class DotReporter extends BaseReporter { class DotReporter extends BaseReporter {
private _counter = 0; private _counter = 0;
override printsToStdio() {
return true;
}
override onBegin(suite: Suite) { override onBegin(suite: Suite) {
super.onBegin(suite); super.onBegin(suite);
console.log(this.generateStartingMessage()); console.log(this.generateStartingMessage());

View file

@ -15,49 +15,15 @@
*/ */
import type { ReporterV2 } from './reporterV2'; import type { ReporterV2 } from './reporterV2';
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Suite } from '../../types/testReporter';
class EmptyReporter implements ReporterV2 { class EmptyReporter implements ReporterV2 {
onConfigure(config: FullConfig) { version(): 'v2' {
} return 'v2';
onBegin(suite: Suite) {
}
onTestBegin(test: TestCase, result: TestResult) {
}
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
}
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
}
onTestEnd(test: TestCase, result: TestResult) {
}
async onEnd(result: FullResult) {
}
async onExit() {
}
onError(error: TestError) {
}
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
}
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
} }
printsToStdio() { printsToStdio() {
return false; return false;
} }
version(): 'v2' {
return 'v2';
}
} }
export default EmptyReporter; export default EmptyReporter;

View file

@ -59,7 +59,7 @@ class GitHubLogger {
export class GitHubReporter extends BaseReporter { export class GitHubReporter extends BaseReporter {
githubLogger = new GitHubLogger(); githubLogger = new GitHubLogger();
override printsToStdio() { printsToStdio() {
return false; return false;
} }

View file

@ -30,7 +30,7 @@ import type { ZipFile } from 'playwright-core/lib/zipBundle';
import { yazl } from 'playwright-core/lib/zipBundle'; import { yazl } from 'playwright-core/lib/zipBundle';
import { mime } from 'playwright-core/lib/utilsBundle'; import { mime } from 'playwright-core/lib/utilsBundle';
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types'; import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
import EmptyReporter from './empty'; import type { ReporterV2 } from './reporterV2';
type TestEntry = { type TestEntry = {
testCase: TestCase; testCase: TestCase;
@ -55,7 +55,7 @@ type HtmlReporterOptions = {
_isTestServer?: boolean; _isTestServer?: boolean;
}; };
class HtmlReporter extends EmptyReporter { class HtmlReporter implements ReporterV2 {
private config!: FullConfig; private config!: FullConfig;
private suite!: Suite; private suite!: Suite;
private _options: HtmlReporterOptions; private _options: HtmlReporterOptions;
@ -68,19 +68,22 @@ class HtmlReporter extends EmptyReporter {
private _topLevelErrors: TestError[] = []; private _topLevelErrors: TestError[] = [];
constructor(options: HtmlReporterOptions) { constructor(options: HtmlReporterOptions) {
super();
this._options = options; this._options = options;
} }
override printsToStdio() { version(): 'v2' {
return 'v2';
}
printsToStdio() {
return false; return false;
} }
override onConfigure(config: FullConfig) { onConfigure(config: FullConfig) {
this.config = config; this.config = config;
} }
override onBegin(suite: Suite) { onBegin(suite: Suite) {
const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions(); const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
this._outputFolder = outputFolder; this._outputFolder = outputFolder;
this._open = open; this._open = open;
@ -122,18 +125,18 @@ class HtmlReporter extends EmptyReporter {
return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath); return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
} }
override onError(error: TestError): void { onError(error: TestError): void {
this._topLevelErrors.push(error); this._topLevelErrors.push(error);
} }
override async onEnd(result: FullResult) { async onEnd(result: FullResult) {
const projectSuites = this.suite.suites; const projectSuites = this.suite.suites;
await removeFolders([this._outputFolder]); 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._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors); this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
} }
override async onExit() { async onExit() {
if (process.env.CI || !this._buildResult) if (process.env.CI || !this._buildResult)
return; return;
const { ok, singleTestId } = this._buildResult; const { ok, singleTestId } = this._buildResult;

View file

@ -23,7 +23,7 @@ import type { ReporterV2 } from './reporterV2';
import { monotonicTime } from 'playwright-core/lib/utils'; import { monotonicTime } from 'playwright-core/lib/utils';
import { Multiplexer } from './multiplexer'; import { Multiplexer } from './multiplexer';
export class InternalReporter { export class InternalReporter implements ReporterV2 {
private _reporter: ReporterV2; private _reporter: ReporterV2;
private _didBegin = false; private _didBegin = false;
private _config!: FullConfig; private _config!: FullConfig;
@ -42,29 +42,29 @@ export class InternalReporter {
this._config = config; this._config = config;
this._startTime = new Date(); this._startTime = new Date();
this._monotonicStartTime = monotonicTime(); this._monotonicStartTime = monotonicTime();
this._reporter.onConfigure(config); this._reporter.onConfigure?.(config);
} }
onBegin(suite: Suite) { onBegin(suite: Suite) {
this._didBegin = true; this._didBegin = true;
this._reporter.onBegin(suite); this._reporter.onBegin?.(suite);
} }
onTestBegin(test: TestCase, result: TestResult) { onTestBegin(test: TestCase, result: TestResult) {
this._reporter.onTestBegin(test, result); this._reporter.onTestBegin?.(test, result);
} }
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) { onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
this._reporter.onStdOut(chunk, test, result); this._reporter.onStdOut?.(chunk, test, result);
} }
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) { onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
this._reporter.onStdErr(chunk, test, result); this._reporter.onStdErr?.(chunk, test, result);
} }
onTestEnd(test: TestCase, result: TestResult) { onTestEnd(test: TestCase, result: TestResult) {
this._addSnippetToTestErrors(test, result); this._addSnippetToTestErrors(test, result);
this._reporter.onTestEnd(test, result); this._reporter.onTestEnd?.(test, result);
} }
async onEnd(result: { status: FullResult['status'] }) { async onEnd(result: { status: FullResult['status'] }) {
@ -72,7 +72,7 @@ export class InternalReporter {
// onBegin was not reported, emit it. // onBegin was not reported, emit it.
this.onBegin(new Suite('', 'root')); this.onBegin(new Suite('', 'root'));
} }
return await this._reporter.onEnd({ return await this._reporter.onEnd?.({
...result, ...result,
startTime: this._startTime!, startTime: this._startTime!,
duration: monotonicTime() - this._monotonicStartTime!, duration: monotonicTime() - this._monotonicStartTime!,
@ -80,25 +80,25 @@ export class InternalReporter {
} }
async onExit() { async onExit() {
await this._reporter.onExit(); await this._reporter.onExit?.();
} }
onError(error: TestError) { onError(error: TestError) {
addLocationAndSnippetToError(this._config, error); addLocationAndSnippetToError(this._config, error);
this._reporter.onError(error); this._reporter.onError?.(error);
} }
onStepBegin(test: TestCase, result: TestResult, step: TestStep) { onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
this._reporter.onStepBegin(test, result, step); this._reporter.onStepBegin?.(test, result, step);
} }
onStepEnd(test: TestCase, result: TestResult, step: TestStep) { onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
this._addSnippetToStepError(test, step); this._addSnippetToStepError(test, step);
this._reporter.onStepEnd(test, result, step); this._reporter.onStepEnd?.(test, result, step);
} }
printsToStdio() { printsToStdio() {
return this._reporter.printsToStdio(); return this._reporter.printsToStdio ? this._reporter.printsToStdio() : true;
} }
private _addSnippetToTestErrors(test: TestCase, result: TestResult) { private _addSnippetToTestErrors(test: TestCase, result: TestResult) {

View file

@ -20,41 +20,44 @@ import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, Full
import { formatError, prepareErrorStack, resolveOutputFile } from './base'; import { formatError, prepareErrorStack, resolveOutputFile } from './base';
import { MultiMap, toPosixPath } from 'playwright-core/lib/utils'; import { MultiMap, toPosixPath } from 'playwright-core/lib/utils';
import { getProjectId } from '../common/config'; import { getProjectId } from '../common/config';
import EmptyReporter from './empty'; import type { ReporterV2 } from './reporterV2';
type JSONOptions = { type JSONOptions = {
outputFile?: string, outputFile?: string,
configDir: string, configDir: string,
}; };
class JSONReporter extends EmptyReporter { class JSONReporter implements ReporterV2 {
config!: FullConfig; config!: FullConfig;
suite!: Suite; suite!: Suite;
private _errors: TestError[] = []; private _errors: TestError[] = [];
private _resolvedOutputFile: string | undefined; private _resolvedOutputFile: string | undefined;
constructor(options: JSONOptions) { constructor(options: JSONOptions) {
super();
this._resolvedOutputFile = resolveOutputFile('JSON', options)?.outputFile; this._resolvedOutputFile = resolveOutputFile('JSON', options)?.outputFile;
} }
override printsToStdio() { version(): 'v2' {
return 'v2';
}
printsToStdio() {
return !this._resolvedOutputFile; return !this._resolvedOutputFile;
} }
override onConfigure(config: FullConfig) { onConfigure(config: FullConfig) {
this.config = config; this.config = config;
} }
override onBegin(suite: Suite) { onBegin(suite: Suite) {
this.suite = suite; this.suite = suite;
} }
override onError(error: TestError): void { onError(error: TestError): void {
this._errors.push(error); this._errors.push(error);
} }
override async onEnd(result: FullResult) { async onEnd(result: FullResult) {
await outputReport(this._serializeReport(result), this._resolvedOutputFile); await outputReport(this._serializeReport(result), this._resolvedOutputFile);
} }

View file

@ -18,8 +18,8 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter'; import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter';
import { formatFailure, resolveOutputFile, stripAnsiEscapes } from './base'; import { formatFailure, resolveOutputFile, stripAnsiEscapes } from './base';
import EmptyReporter from './empty';
import { getAsBooleanFromENV } from 'playwright-core/lib/utils'; import { getAsBooleanFromENV } from 'playwright-core/lib/utils';
import type { ReporterV2 } from './reporterV2';
type JUnitOptions = { type JUnitOptions = {
outputFile?: string, outputFile?: string,
@ -29,7 +29,7 @@ type JUnitOptions = {
configDir: string, configDir: string,
}; };
class JUnitReporter extends EmptyReporter { class JUnitReporter implements ReporterV2 {
private config!: FullConfig; private config!: FullConfig;
private configDir: string; private configDir: string;
private suite!: Suite; private suite!: Suite;
@ -42,27 +42,30 @@ class JUnitReporter extends EmptyReporter {
private includeProjectInTestName = false; private includeProjectInTestName = false;
constructor(options: JUnitOptions) { constructor(options: JUnitOptions) {
super();
this.stripANSIControlSequences = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_STRIP_ANSI', !!options.stripANSIControlSequences); this.stripANSIControlSequences = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_STRIP_ANSI', !!options.stripANSIControlSequences);
this.includeProjectInTestName = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME', !!options.includeProjectInTestName); this.includeProjectInTestName = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME', !!options.includeProjectInTestName);
this.configDir = options.configDir; this.configDir = options.configDir;
this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile; this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile;
} }
override printsToStdio() { version(): 'v2' {
return 'v2';
}
printsToStdio() {
return !this.resolvedOutputFile; return !this.resolvedOutputFile;
} }
override onConfigure(config: FullConfig) { onConfigure(config: FullConfig) {
this.config = config; this.config = config;
} }
override onBegin(suite: Suite) { onBegin(suite: Suite) {
this.suite = suite; this.suite = suite;
this.timestamp = new Date(); this.timestamp = new Date();
} }
override async onEnd(result: FullResult) { async onEnd(result: FullResult) {
const children: XMLEntry[] = []; const children: XMLEntry[] = [];
for (const projectSuite of this.suite.suites) { for (const projectSuite of this.suite.suites) {
for (const fileSuite of projectSuite.suites) for (const fileSuite of projectSuite.suites)

View file

@ -23,10 +23,6 @@ class LineReporter extends BaseReporter {
private _lastTest: TestCase | undefined; private _lastTest: TestCase | undefined;
private _didBegin = false; private _didBegin = false;
override printsToStdio() {
return true;
}
override onBegin(suite: Suite) { override onBegin(suite: Suite) {
super.onBegin(suite); super.onBegin(suite);
const startingMessage = this.generateStartingMessage(); const startingMessage = this.generateStartingMessage();
@ -66,20 +62,17 @@ class LineReporter extends BaseReporter {
console.log(); console.log();
} }
override onTestBegin(test: TestCase, result: TestResult) { onTestBegin(test: TestCase, result: TestResult) {
super.onTestBegin(test, result);
++this._current; ++this._current;
this._updateLine(test, result, undefined); this._updateLine(test, result, undefined);
} }
override onStepBegin(test: TestCase, result: TestResult, step: TestStep) { onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
super.onStepBegin(test, result, step);
if (step.category === 'test.step') if (step.category === 'test.step')
this._updateLine(test, result, step); this._updateLine(test, result, step);
} }
override onStepEnd(test: TestCase, result: TestResult, step: TestStep) { onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
super.onStepEnd(test, result, step);
if (step.category === 'test.step') if (step.category === 'test.step')
this._updateLine(test, result, step.parent); this._updateLine(test, result, step.parent);
} }

View file

@ -39,10 +39,6 @@ class ListReporter extends BaseReporter {
this._printSteps = getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps); this._printSteps = getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps);
} }
override printsToStdio() {
return true;
}
override onBegin(suite: Suite) { override onBegin(suite: Suite) {
super.onBegin(suite); super.onBegin(suite);
const startingMessage = this.generateStartingMessage(); const startingMessage = this.generateStartingMessage();
@ -52,9 +48,7 @@ class ListReporter extends BaseReporter {
} }
} }
override onTestBegin(test: TestCase, result: TestResult) { onTestBegin(test: TestCase, result: TestResult) {
super.onTestBegin(test, result);
const index = String(this._resultIndex.size + 1); const index = String(this._resultIndex.size + 1);
this._resultIndex.set(result, index); this._resultIndex.set(result, index);
@ -88,8 +82,7 @@ class ListReporter extends BaseReporter {
return stepIndex; return stepIndex;
} }
override onStepBegin(test: TestCase, result: TestResult, step: TestStep) { onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
super.onStepBegin(test, result, step);
if (step.category !== 'test.step') if (step.category !== 'test.step')
return; return;
const testIndex = this._resultIndex.get(result) || ''; const testIndex = this._resultIndex.get(result) || '';
@ -108,8 +101,7 @@ class ListReporter extends BaseReporter {
} }
} }
override onStepEnd(test: TestCase, result: TestResult, step: TestStep) { onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
super.onStepEnd(test, result, step);
if (step.category !== 'test.step') if (step.category !== 'test.step')
return; return;

View file

@ -34,7 +34,7 @@ class MarkdownReporter extends BaseReporter {
this._options = options; this._options = options;
} }
override printsToStdio() { printsToStdio() {
return false; return false;
} }

View file

@ -31,37 +31,37 @@ export class Multiplexer implements ReporterV2 {
onConfigure(config: FullConfig) { onConfigure(config: FullConfig) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onConfigure(config)); wrap(() => reporter.onConfigure?.(config));
} }
onBegin(suite: Suite) { onBegin(suite: Suite) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onBegin(suite)); wrap(() => reporter.onBegin?.(suite));
} }
onTestBegin(test: TestCase, result: TestResult) { onTestBegin(test: TestCase, result: TestResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onTestBegin(test, result)); wrap(() => reporter.onTestBegin?.(test, result));
} }
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) { onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onStdOut(chunk, test, result)); wrap(() => reporter.onStdOut?.(chunk, test, result));
} }
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) { onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onStdErr(chunk, test, result)); wrap(() => reporter.onStdErr?.(chunk, test, result));
} }
onTestEnd(test: TestCase, result: TestResult) { onTestEnd(test: TestCase, result: TestResult) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onTestEnd(test, result)); wrap(() => reporter.onTestEnd?.(test, result));
} }
async onEnd(result: FullResult) { async onEnd(result: FullResult) {
for (const reporter of this._reporters) { for (const reporter of this._reporters) {
const outResult = await wrapAsync(() => reporter.onEnd(result)); const outResult = await wrapAsync(() => reporter.onEnd?.(result));
if (outResult?.status) if (outResult?.status)
result.status = outResult.status; result.status = outResult.status;
} }
@ -70,28 +70,28 @@ export class Multiplexer implements ReporterV2 {
async onExit() { async onExit() {
for (const reporter of this._reporters) for (const reporter of this._reporters)
await wrapAsync(() => reporter.onExit()); await wrapAsync(() => reporter.onExit?.());
} }
onError(error: TestError) { onError(error: TestError) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onError(error)); wrap(() => reporter.onError?.(error));
} }
onStepBegin(test: TestCase, result: TestResult, step: TestStep) { onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onStepBegin(test, result, step)); wrap(() => reporter.onStepBegin?.(test, result, step));
} }
onStepEnd(test: TestCase, result: TestResult, step: TestStep) { onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onStepEnd(test, result, step)); wrap(() => reporter.onStepEnd?.(test, result, step));
} }
printsToStdio(): boolean { printsToStdio(): boolean {
return this._reporters.some(r => { return this._reporters.some(r => {
let prints = true; let prints = false;
wrap(() => prints = r.printsToStdio()); wrap(() => prints = r.printsToStdio ? r.printsToStdio() : true);
return prints; return prints;
}); });
} }

View file

@ -17,18 +17,18 @@
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter, Suite } from '../../types/testReporter'; import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter, Suite } from '../../types/testReporter';
export interface ReporterV2 { export interface ReporterV2 {
onConfigure(config: FullConfig): void; onConfigure?(config: FullConfig): void;
onBegin(suite: Suite): void; onBegin?(suite: Suite): void;
onTestBegin(test: TestCase, result: TestResult): void; onTestBegin?(test: TestCase, result: TestResult): void;
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void; onStdOut?(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void; onStdErr?(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
onTestEnd(test: TestCase, result: TestResult): void; onTestEnd?(test: TestCase, result: TestResult): void;
onEnd(result: FullResult): Promise<{ status?: FullResult['status'] } | undefined | void> | void; onEnd?(result: FullResult): Promise<{ status?: FullResult['status'] } | undefined | void> | void;
onExit(): void | Promise<void>; onExit?(): void | Promise<void>;
onError(error: TestError): void; onError?(error: TestError): void;
onStepBegin(test: TestCase, result: TestResult, step: TestStep): void; onStepBegin?(test: TestCase, result: TestResult, step: TestStep): void;
onStepEnd(test: TestCase, result: TestResult, step: TestStep): void; onStepEnd?(test: TestCase, result: TestResult, step: TestStep): void;
printsToStdio(): boolean; printsToStdio?(): boolean;
version(): 'v2'; version(): 'v2';
} }

View file

@ -145,9 +145,6 @@ export class TeleReporterEmitter implements ReporterV2 {
}); });
} }
async onExit() {
}
printsToStdio() { printsToStdio() {
return false; return false;
} }

View file

@ -198,17 +198,17 @@ export class Dispatcher {
worker.on('stdOut', (params: TestOutputPayload) => { worker.on('stdOut', (params: TestOutputPayload) => {
const { chunk, test, result } = handleOutput(params); const { chunk, test, result } = handleOutput(params);
result?.stdout.push(chunk); result?.stdout.push(chunk);
this._reporter.onStdOut(chunk, test, result); this._reporter.onStdOut?.(chunk, test, result);
}); });
worker.on('stdErr', (params: TestOutputPayload) => { worker.on('stdErr', (params: TestOutputPayload) => {
const { chunk, test, result } = handleOutput(params); const { chunk, test, result } = handleOutput(params);
result?.stderr.push(chunk); result?.stderr.push(chunk);
this._reporter.onStdErr(chunk, test, result); this._reporter.onStdErr?.(chunk, test, result);
}); });
worker.on('teardownErrors', (params: TeardownErrorsPayload) => { worker.on('teardownErrors', (params: TeardownErrorsPayload) => {
this._failureTracker.onWorkerError(); this._failureTracker.onWorkerError();
for (const error of params.fatalErrors) for (const error of params.fatalErrors)
this._reporter.onError(error); this._reporter.onError?.(error);
}); });
worker.on('exit', () => { worker.on('exit', () => {
const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {}; const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {};
@ -257,7 +257,7 @@ class JobDispatcher {
result.parallelIndex = this._parallelIndex; result.parallelIndex = this._parallelIndex;
result.workerIndex = this._workerIndex; result.workerIndex = this._workerIndex;
result.startTime = new Date(params.startWallTime); result.startTime = new Date(params.startWallTime);
this._reporter.onTestBegin(test, result); this._reporter.onTestBegin?.(test, result);
this._currentlyRunning = { test, result }; this._currentlyRunning = { test, result };
} }
@ -323,7 +323,7 @@ class JobDispatcher {
}; };
steps.set(params.stepId, step); steps.set(params.stepId, step);
(parentStep || result).steps.push(step); (parentStep || result).steps.push(step);
this._reporter.onStepBegin(test, result, step); this._reporter.onStepBegin?.(test, result, step);
} }
private _onStepEnd(params: StepEndPayload) { private _onStepEnd(params: StepEndPayload) {
@ -335,14 +335,14 @@ class JobDispatcher {
const { result, steps, test } = data; const { result, steps, test } = data;
const step = steps.get(params.stepId); const step = steps.get(params.stepId);
if (!step) { if (!step) {
this._reporter.onStdErr('Internal error: step end without step begin: ' + params.stepId, test, result); this._reporter.onStdErr?.('Internal error: step end without step begin: ' + params.stepId, test, result);
return; return;
} }
step.duration = params.wallTime - step.startTime.getTime(); step.duration = params.wallTime - step.startTime.getTime();
if (params.error) if (params.error)
step.error = params.error; step.error = params.error;
steps.delete(params.stepId); steps.delete(params.stepId);
this._reporter.onStepEnd(test, result, step); this._reporter.onStepEnd?.(test, result, step);
} }
private _onAttach(params: AttachmentPayload) { private _onAttach(params: AttachmentPayload) {
@ -368,7 +368,7 @@ class JobDispatcher {
result = runData.result; result = runData.result;
} else { } else {
result = test._appendTestResult(); result = test._appendTestResult();
this._reporter.onTestBegin(test, result); this._reporter.onTestBegin?.(test, result);
} }
result.errors = [...errors]; result.errors = [...errors];
result.error = result.errors[0]; result.error = result.errors[0];
@ -392,7 +392,7 @@ class JobDispatcher {
// Let's just fail the test run. // Let's just fail the test run.
this._failureTracker.onWorkerError(); this._failureTracker.onWorkerError();
for (const error of errors) for (const error of errors)
this._reporter.onError(error); this._reporter.onError?.(error);
} }
} }
@ -526,7 +526,7 @@ class JobDispatcher {
if (allTestsSkipped && !this._failureTracker.hasReachedMaxFailures()) { if (allTestsSkipped && !this._failureTracker.hasReachedMaxFailures()) {
for (const test of this._job.tests) { for (const test of this._job.tests) {
const result = test._appendTestResult(); const result = test._appendTestResult();
this._reporter.onTestBegin(test, result); this._reporter.onTestBegin?.(test, result);
result.status = 'skipped'; result.status = 'skipped';
this._reportTestEnd(test, result); this._reportTestEnd(test, result);
} }
@ -540,13 +540,13 @@ class JobDispatcher {
} }
private _reportTestEnd(test: TestCase, result: TestResult) { private _reportTestEnd(test: TestCase, result: TestResult) {
this._reporter.onTestEnd(test, result); this._reporter.onTestEnd?.(test, result);
const hadMaxFailures = this._failureTracker.hasReachedMaxFailures(); const hadMaxFailures = this._failureTracker.hasReachedMaxFailures();
this._failureTracker.onTestEnd(test, result); this._failureTracker.onTestEnd(test, result);
if (this._failureTracker.hasReachedMaxFailures()) { if (this._failureTracker.hasReachedMaxFailures()) {
this._stopCallback(); this._stopCallback();
if (!hadMaxFailures) if (!hadMaxFailures)
this._reporter.onError({ message: colors.red(`Testing stopped early after ${this._failureTracker.maxFailures()} maximum allowed failures.`) }); this._reporter.onError?.({ message: colors.red(`Testing stopped early after ${this._failureTracker.maxFailures()} maximum allowed failures.`) });
} }
} }
} }

View file

@ -67,7 +67,7 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
reporters.push(wrapReporterAsV2(new reporterConstructor(runOptions))); reporters.push(wrapReporterAsV2(new reporterConstructor(runOptions)));
} }
const someReporterPrintsToStdio = reporters.some(r => r.printsToStdio()); const someReporterPrintsToStdio = reporters.some(r => r.printsToStdio ? r.printsToStdio() : true);
if (reporters.length && !someReporterPrintsToStdio) { if (reporters.length && !someReporterPrintsToStdio) {
// Add a line/dot/list-mode reporter for convenience. // Add a line/dot/list-mode reporter for convenience.
// Important to put it first, just in case some other reporter stalls onEnd. // Important to put it first, just in case some other reporter stalls onEnd.
@ -92,16 +92,15 @@ interface ErrorCollectingReporter extends ReporterV2 {
export function createErrorCollectingReporter(writeToConsole?: boolean): ErrorCollectingReporter { export function createErrorCollectingReporter(writeToConsole?: boolean): ErrorCollectingReporter {
const errors: TestError[] = []; const errors: TestError[] = [];
const reporterV2 = wrapReporterAsV2({ return {
version: () => 'v2',
onError(error: TestError) { onError(error: TestError) {
errors.push(error); errors.push(error);
if (writeToConsole) if (writeToConsole)
process.stdout.write(formatError(error, colors.enabled).message + '\n'); process.stdout.write(formatError(error, colors.enabled).message + '\n');
} },
}); errors: () => errors,
const reporter = reporterV2 as ErrorCollectingReporter; };
reporter.errors = () => errors;
return reporter;
} }
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean) { function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean) {
@ -132,14 +131,18 @@ function computeCommandHash(config: FullConfigInternal) {
return parts.join('-'); return parts.join('-');
} }
class ListModeReporter extends EmptyReporter { class ListModeReporter implements ReporterV2 {
private config!: FullConfig; private config!: FullConfig;
override onConfigure(config: FullConfig) { version(): 'v2' {
return 'v2';
}
onConfigure(config: FullConfig) {
this.config = config; this.config = config;
} }
override onBegin(suite: Suite): void { onBegin(suite: Suite): void {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`Listing tests:`); console.log(`Listing tests:`);
const tests = suite.allTests(); const tests = suite.allTests();
@ -157,12 +160,8 @@ class ListModeReporter extends EmptyReporter {
console.log(`Total: ${tests.length} ${tests.length === 1 ? 'test' : 'tests'} in ${files.size} ${files.size === 1 ? 'file' : 'files'}`); console.log(`Total: ${tests.length} ${tests.length === 1 ? 'test' : 'tests'} in ${files.size} ${files.size === 1 ? 'file' : 'files'}`);
} }
override onError(error: TestError) { onError(error: TestError) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('\n' + formatError(error, false).message); console.error('\n' + formatError(error, false).message);
} }
override printsToStdio(): boolean {
return true;
}
} }

View file

@ -159,7 +159,7 @@ export function createTaskRunnerForClearCache(config: FullConfigInternal, report
function createReportBeginTask(): Task<TestRun> { function createReportBeginTask(): Task<TestRun> {
return { return {
setup: async (reporter, { rootSuite }) => { setup: async (reporter, { rootSuite }) => {
reporter.onBegin(rootSuite!); reporter.onBegin?.(rootSuite!);
}, },
teardown: async ({}) => {}, teardown: async ({}) => {},
}; };

View file

@ -91,7 +91,7 @@ export const TraceView: React.FC<{
if (pollTimer.current) if (pollTimer.current)
clearTimeout(pollTimer.current); clearTimeout(pollTimer.current);
}; };
}, [outputDir, item, setModel, counter, setCounter]); }, [outputDir, item, setModel, counter, setCounter, pathSeparator]);
return <Workbench return <Workbench
key='workbench' key='workbench'