chore: migrate builtin reporters to ReporterV2 (#23985)

This allows builtin reporters to handle stdio between onConfigure and
onBegin.

Fixes #23539.
This commit is contained in:
Dmitry Gozman 2023-06-30 16:21:31 -07:00 committed by GitHub
parent 86c1abd934
commit 7e310f79af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 228 additions and 85 deletions

View file

@ -16,9 +16,10 @@
import { colors, ms as milliseconds, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; import { colors, ms as milliseconds, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
import path from 'path'; import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location, Reporter } from '../../types/testReporter'; import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location } from '../../types/testReporter';
import type { SuitePrivate } from '../../types/reporterPrivate'; import type { SuitePrivate } from '../../types/reporterPrivate';
import { monotonicTime } from 'playwright-core/lib/utils'; import { monotonicTime } from 'playwright-core/lib/utils';
import type { ReporterV2 } from './reporterV2';
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' }; export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
export const kOutputSymbol = Symbol('output'); export const kOutputSymbol = Symbol('output');
@ -43,7 +44,7 @@ type TestSummary = {
fatalErrors: TestError[]; fatalErrors: TestError[];
}; };
export class BaseReporter implements Reporter { export class BaseReporter implements ReporterV2 {
duration = 0; duration = 0;
config!: FullConfig; config!: FullConfig;
suite!: Suite; suite!: Suite;
@ -60,9 +61,16 @@ export class BaseReporter implements Reporter {
this._ttyWidthForTest = parseInt(process.env.PWTEST_TTY_WIDTH || '', 10); this._ttyWidthForTest = parseInt(process.env.PWTEST_TTY_WIDTH || '', 10);
} }
onBegin(config: FullConfig, suite: Suite) { version(): 'v2' {
this.monotonicStartTime = monotonicTime(); return 'v2';
}
onConfigure(config: FullConfig) {
this.config = config; this.config = config;
}
onBegin(suite: Suite) {
this.monotonicStartTime = monotonicTime();
this.suite = suite; this.suite = suite;
this.totalTestCount = suite.allTests().length; this.totalTestCount = suite.allTests().length;
} }
@ -82,6 +90,9 @@ export class BaseReporter implements Reporter {
(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) {
// Ignore any tests that are run in parallel. // Ignore any tests that are run in parallel.
for (let suite: Suite | undefined = test.parent; suite; suite = suite.parent) { for (let suite: Suite | undefined = test.parent; suite; suite = suite.parent) {
@ -104,6 +115,19 @@ export class BaseReporter implements Reporter {
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 ttyWidth() { protected ttyWidth() {
return this._ttyWidthForTest || process.stdout.columns || 0; return this._ttyWidthForTest || process.stdout.columns || 0;
} }

View file

@ -16,17 +16,17 @@
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { BaseReporter, formatError } from './base'; import { BaseReporter, formatError } from './base';
import type { FullResult, TestCase, TestResult, FullConfig, Suite, TestError } from '../../types/testReporter'; import type { FullResult, TestCase, TestResult, Suite, TestError } from '../../types/testReporter';
class DotReporter extends BaseReporter { class DotReporter extends BaseReporter {
private _counter = 0; private _counter = 0;
printsToStdio() { override printsToStdio() {
return true; return true;
} }
override onBegin(config: FullConfig, suite: Suite) { override onBegin(suite: Suite) {
super.onBegin(config, suite); super.onBegin(suite);
console.log(this.generateStartingMessage()); console.log(this.generateStartingMessage());
} }

View file

@ -14,9 +14,50 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Reporter } from '../../types/testReporter'; import type { ReporterV2 } from './reporterV2';
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Suite } from '../../types/testReporter';
class EmptyReporter implements Reporter { class EmptyReporter implements ReporterV2 {
onConfigure(config: FullConfig) {
}
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() {
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();
printsToStdio() { override printsToStdio() {
return false; return false;
} }

View file

@ -19,7 +19,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type { TransformCallback } from 'stream'; import type { TransformCallback } from 'stream';
import { Transform } from 'stream'; import { Transform } from 'stream';
import type { FullConfig, Reporter, Suite } from '../../types/testReporter'; import type { FullConfig, Suite } from '../../types/testReporter';
import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, removeFolders } from 'playwright-core/lib/utils'; import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, removeFolders } from 'playwright-core/lib/utils';
import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from './raw'; import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from './raw';
import RawReporter from './raw'; import RawReporter from './raw';
@ -31,6 +31,7 @@ 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 { FullConfigInternal } from '../common/config'; import { FullConfigInternal } from '../common/config';
import EmptyReporter from './empty';
type TestEntry = { type TestEntry = {
testCase: TestCase; testCase: TestCase;
@ -47,7 +48,7 @@ type HtmlReporterOptions = {
attachmentsBaseURL?: string, attachmentsBaseURL?: string,
}; };
class HtmlReporter implements Reporter { class HtmlReporter extends EmptyReporter {
private config!: FullConfig; private config!: FullConfig;
private suite!: Suite; private suite!: Suite;
private _options: HtmlReporterOptions; private _options: HtmlReporterOptions;
@ -57,21 +58,25 @@ class HtmlReporter implements Reporter {
private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined; private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined;
constructor(options: HtmlReporterOptions) { constructor(options: HtmlReporterOptions) {
super();
this._options = options; this._options = options;
} }
printsToStdio() { override printsToStdio() {
return false; return false;
} }
onBegin(config: FullConfig, suite: Suite) { override onConfigure(config: FullConfig) {
this.config = config; this.config = config;
}
override onBegin(suite: Suite) {
const { outputFolder, open, attachmentsBaseURL } = this._resolveOptions(); const { outputFolder, open, attachmentsBaseURL } = this._resolveOptions();
this._outputFolder = outputFolder; this._outputFolder = outputFolder;
this._open = open; this._open = open;
this._attachmentsBaseURL = attachmentsBaseURL; this._attachmentsBaseURL = attachmentsBaseURL;
const reportedWarnings = new Set<string>(); const reportedWarnings = new Set<string>();
for (const project of config.projects) { for (const project of this.config.projects) {
if (outputFolder.startsWith(project.outputDir) || project.outputDir.startsWith(outputFolder)) { if (outputFolder.startsWith(project.outputDir) || project.outputDir.startsWith(outputFolder)) {
const key = outputFolder + '|' + project.outputDir; const key = outputFolder + '|' + project.outputDir;
if (reportedWarnings.has(key)) if (reportedWarnings.has(key))
@ -100,7 +105,7 @@ class HtmlReporter implements Reporter {
}; };
} }
async onEnd() { override async onEnd() {
const projectSuites = this.suite.suites; const projectSuites = this.suite.suites;
const reports = projectSuites.map(suite => { const reports = projectSuites.map(suite => {
const rawReporter = new RawReporter(); const rawReporter = new RawReporter();
@ -112,7 +117,7 @@ class HtmlReporter implements Reporter {
this._buildResult = await builder.build(this.config.metadata, reports); this._buildResult = await builder.build(this.config.metadata, reports);
} }
async onExit() { override async onExit() {
if (process.env.CI || !this._buildResult) if (process.env.CI || !this._buildResult)
return; return;

View file

@ -19,44 +19,47 @@ import { colors } from 'playwright-core/lib/utilsBundle';
import { codeFrameColumns } from '../transform/babelBundle'; import { codeFrameColumns } from '../transform/babelBundle';
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep } from '../../types/testReporter'; import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep } from '../../types/testReporter';
import { Suite } from '../common/test'; import { Suite } from '../common/test';
import { Multiplexer } from './multiplexer';
import { prepareErrorStack, relativeFilePath } from './base'; import { prepareErrorStack, relativeFilePath } from './base';
import type { ReporterV2 } from './reporterV2'; import type { ReporterV2 } from './reporterV2';
export class InternalReporter implements ReporterV2 { export class InternalReporter implements ReporterV2 {
private _multiplexer: Multiplexer; private _reporter: ReporterV2;
private _didBegin = false; private _didBegin = false;
private _config!: FullConfig; private _config!: FullConfig;
constructor(reporters: ReporterV2[]) { constructor(reporter: ReporterV2) {
this._multiplexer = new Multiplexer(reporters); this._reporter = reporter;
}
version(): 'v2' {
return 'v2';
} }
onConfigure(config: FullConfig) { onConfigure(config: FullConfig) {
this._config = config; this._config = config;
this._multiplexer.onConfigure(config); this._reporter.onConfigure(config);
} }
onBegin(suite: Suite) { onBegin(suite: Suite) {
this._didBegin = true; this._didBegin = true;
this._multiplexer.onBegin(suite); this._reporter.onBegin(suite);
} }
onTestBegin(test: TestCase, result: TestResult) { onTestBegin(test: TestCase, result: TestResult) {
this._multiplexer.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._multiplexer.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._multiplexer.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._multiplexer.onTestEnd(test, result); this._reporter.onTestEnd(test, result);
} }
async onEnd(result: FullResult) { async onEnd(result: FullResult) {
@ -64,29 +67,29 @@ export class InternalReporter implements ReporterV2 {
// onBegin was not reported, emit it. // onBegin was not reported, emit it.
this.onBegin(new Suite('', 'root')); this.onBegin(new Suite('', 'root'));
} }
await this._multiplexer.onEnd(result); await this._reporter.onEnd(result);
} }
async onExit() { async onExit() {
await this._multiplexer.onExit(); await this._reporter.onExit();
} }
onError(error: TestError) { onError(error: TestError) {
addLocationAndSnippetToError(this._config, error); addLocationAndSnippetToError(this._config, error);
this._multiplexer.onError(error); this._reporter.onError(error);
} }
onStepBegin(test: TestCase, result: TestResult, step: TestStep) { onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
this._multiplexer.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._multiplexer.onStepEnd(test, result, step); this._reporter.onStepEnd(test, result, step);
} }
printsToStdio() { printsToStdio() {
return this._multiplexer.printsToStdio(); return this._reporter.printsToStdio();
} }
private _addSnippetToTestErrors(test: TestCase, result: TestResult) { private _addSnippetToTestErrors(test: TestCase, result: TestResult) {

View file

@ -16,40 +16,45 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, Reporter, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep, JSONReportError } from '../../types/testReporter'; import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep, JSONReportError } from '../../types/testReporter';
import { formatError, prepareErrorStack } from './base'; import { formatError, prepareErrorStack } from './base';
import { MultiMap } from 'playwright-core/lib/utils'; import { MultiMap } from 'playwright-core/lib/utils';
import { assert } from 'playwright-core/lib/utils'; import { assert } from 'playwright-core/lib/utils';
import { FullProjectInternal } from '../common/config'; import { FullProjectInternal } from '../common/config';
import EmptyReporter from './empty';
export function toPosixPath(aPath: string): string { export function toPosixPath(aPath: string): string {
return aPath.split(path.sep).join(path.posix.sep); return aPath.split(path.sep).join(path.posix.sep);
} }
class JSONReporter implements Reporter { class JSONReporter extends EmptyReporter {
config!: FullConfig; config!: FullConfig;
suite!: Suite; suite!: Suite;
private _errors: TestError[] = []; private _errors: TestError[] = [];
private _outputFile: string | undefined; private _outputFile: string | undefined;
constructor(options: { outputFile?: string } = {}) { constructor(options: { outputFile?: string } = {}) {
super();
this._outputFile = options.outputFile || reportOutputNameFromEnv(); this._outputFile = options.outputFile || reportOutputNameFromEnv();
} }
printsToStdio() { override printsToStdio() {
return !this._outputFile; return !this._outputFile;
} }
onBegin(config: FullConfig, suite: Suite) { override onConfigure(config: FullConfig) {
this.config = config; this.config = config;
}
override onBegin(suite: Suite) {
this.suite = suite; this.suite = suite;
} }
onError(error: TestError): void { override onError(error: TestError): void {
this._errors.push(error); this._errors.push(error);
} }
async onEnd(result: FullResult) { override async onEnd(result: FullResult) {
outputReport(this._serializeReport(), this.config, this._outputFile); outputReport(this._serializeReport(), this.config, this._outputFile);
} }

View file

@ -16,12 +16,13 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import type { FullConfig, FullResult, Reporter, Suite, TestCase } from '../../types/testReporter'; import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter';
import { monotonicTime } from 'playwright-core/lib/utils'; import { monotonicTime } from 'playwright-core/lib/utils';
import { formatFailure, stripAnsiEscapes } from './base'; import { formatFailure, stripAnsiEscapes } from './base';
import { assert } from 'playwright-core/lib/utils'; import { assert } from 'playwright-core/lib/utils';
import EmptyReporter from './empty';
class JUnitReporter implements Reporter { class JUnitReporter extends EmptyReporter {
private config!: FullConfig; private config!: FullConfig;
private suite!: Suite; private suite!: Suite;
private timestamp!: Date; private timestamp!: Date;
@ -33,22 +34,26 @@ class JUnitReporter implements Reporter {
private stripANSIControlSequences = false; private stripANSIControlSequences = false;
constructor(options: { outputFile?: string, stripANSIControlSequences?: boolean } = {}) { constructor(options: { outputFile?: string, stripANSIControlSequences?: boolean } = {}) {
super();
this.outputFile = options.outputFile || reportOutputNameFromEnv(); this.outputFile = options.outputFile || reportOutputNameFromEnv();
this.stripANSIControlSequences = options.stripANSIControlSequences || false; this.stripANSIControlSequences = options.stripANSIControlSequences || false;
} }
printsToStdio() { override printsToStdio() {
return !this.outputFile; return !this.outputFile;
} }
onBegin(config: FullConfig, suite: Suite) { override onConfigure(config: FullConfig) {
this.config = config; this.config = config;
}
override onBegin(suite: Suite) {
this.suite = suite; this.suite = suite;
this.timestamp = new Date(); this.timestamp = new Date();
this.startTime = monotonicTime(); this.startTime = monotonicTime();
} }
async onEnd(result: FullResult) { override async onEnd(result: FullResult) {
const duration = monotonicTime() - this.startTime; const duration = monotonicTime() - this.startTime;
const children: XMLEntry[] = []; const children: XMLEntry[] = [];
for (const projectSuite of this.suite.suites) { for (const projectSuite of this.suite.suites) {

View file

@ -16,19 +16,19 @@
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { BaseReporter, formatError, formatFailure, formatTestTitle } from './base'; import { BaseReporter, formatError, formatFailure, formatTestTitle } from './base';
import type { FullConfig, TestCase, Suite, TestResult, FullResult, TestStep, TestError } from '../../types/testReporter'; import type { TestCase, Suite, TestResult, FullResult, TestStep, TestError } from '../../types/testReporter';
class LineReporter extends BaseReporter { class LineReporter extends BaseReporter {
private _current = 0; private _current = 0;
private _failures = 0; private _failures = 0;
private _lastTest: TestCase | undefined; private _lastTest: TestCase | undefined;
printsToStdio() { override printsToStdio() {
return true; return true;
} }
override onBegin(config: FullConfig, suite: Suite) { override onBegin(suite: Suite) {
super.onBegin(config, suite); super.onBegin(suite);
console.log(this.generateStartingMessage()); console.log(this.generateStartingMessage());
console.log(); console.log();
} }
@ -62,17 +62,20 @@ class LineReporter extends BaseReporter {
console.log(); console.log();
} }
onTestBegin(test: TestCase, result: TestResult) { override onTestBegin(test: TestCase, result: TestResult) {
super.onTestBegin(test, result);
++this._current; ++this._current;
this._updateLine(test, result, undefined); this._updateLine(test, result, undefined);
} }
onStepBegin(test: TestCase, result: TestResult, step: TestStep) { override 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);
} }
onStepEnd(test: TestCase, result: TestResult, step: TestStep) { override 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

@ -17,7 +17,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { colors, ms as milliseconds } from 'playwright-core/lib/utilsBundle'; import { colors, ms as milliseconds } from 'playwright-core/lib/utilsBundle';
import { BaseReporter, formatError, formatTestTitle, stepSuffix, stripAnsiEscapes } from './base'; import { BaseReporter, formatError, formatTestTitle, stepSuffix, stripAnsiEscapes } from './base';
import type { FullConfig, FullResult, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter'; import type { FullResult, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
// Allow it in the Visual Studio Code Terminal and the new Windows Terminal // Allow it in the Visual Studio Code Terminal and the new Windows Terminal
const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION; const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION;
@ -41,12 +41,12 @@ class ListReporter extends BaseReporter {
this._liveTerminal = process.stdout.isTTY || !!process.env.PWTEST_TTY_WIDTH; this._liveTerminal = process.stdout.isTTY || !!process.env.PWTEST_TTY_WIDTH;
} }
printsToStdio() { override printsToStdio() {
return true; return true;
} }
override onBegin(config: FullConfig, suite: Suite) { override onBegin(suite: Suite) {
super.onBegin(config, suite); super.onBegin(suite);
const startingMessage = this.generateStartingMessage(); const startingMessage = this.generateStartingMessage();
if (startingMessage) { if (startingMessage) {
console.log(startingMessage); console.log(startingMessage);
@ -54,7 +54,8 @@ class ListReporter extends BaseReporter {
} }
} }
onTestBegin(test: TestCase, result: TestResult) { override onTestBegin(test: TestCase, result: TestResult) {
super.onTestBegin(test, result);
if (this._liveTerminal) if (this._liveTerminal)
this._maybeWriteNewLine(); this._maybeWriteNewLine();
this._resultIndex.set(result, String(this._resultIndex.size + 1)); this._resultIndex.set(result, String(this._resultIndex.size + 1));
@ -77,7 +78,8 @@ class ListReporter extends BaseReporter {
this._dumpToStdio(test, chunk, process.stderr); this._dumpToStdio(test, chunk, process.stderr);
} }
onStepBegin(test: TestCase, result: TestResult, step: TestStep) { override 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)!;
@ -102,7 +104,8 @@ class ListReporter extends BaseReporter {
} }
} }
onStepEnd(test: TestCase, result: TestResult, step: TestStep) { override 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

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

View file

@ -25,6 +25,10 @@ export class Multiplexer implements ReporterV2 {
this._reporters = reporters; this._reporters = reporters;
} }
version(): 'v2' {
return 'v2';
}
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));

View file

@ -29,6 +29,7 @@ export interface ReporterV2 {
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';
} }
type StdIOChunk = { type StdIOChunk = {
@ -37,7 +38,16 @@ type StdIOChunk = {
result?: TestResult; result?: TestResult;
}; };
export class ReporterV2Wrapper implements ReporterV2 { export function wrapReporterAsV2(reporter: Reporter | ReporterV2): ReporterV2 {
try {
if ('version' in reporter && reporter.version() === 'v2')
return reporter as ReporterV2;
} catch (e) {
}
return new ReporterV2Wrapper(reporter as Reporter);
}
class ReporterV2Wrapper implements ReporterV2 {
private _reporter: Reporter; private _reporter: Reporter;
private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = []; private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = [];
private _config!: FullConfig; private _config!: FullConfig;
@ -46,6 +56,10 @@ export class ReporterV2Wrapper implements ReporterV2 {
this._reporter = reporter; this._reporter = reporter;
} }
version(): 'v2' {
return 'v2';
}
onConfigure(config: FullConfig) { onConfigure(config: FullConfig) {
this._config = config; this._config = config;
} }

View file

@ -34,6 +34,10 @@ export class TeleReporterEmitter implements ReporterV2 {
this._skipBuffers = skipBuffers; this._skipBuffers = skipBuffers;
} }
version(): 'v2' {
return 'v2';
}
onConfigure(config: FullConfig) { onConfigure(config: FullConfig) {
this._rootDir = config.rootDir; this._rootDir = config.rootDir;
this._messageSink({ method: 'onConfigure', params: { config: this._serializeConfig(config) } }); this._messageSink({ method: 'onConfigure', params: { config: this._serializeConfig(config) } });

View file

@ -15,7 +15,7 @@
*/ */
import path from 'path'; import path from 'path';
import type { FullConfig, Reporter, TestError } from '../../types/testReporter'; import type { FullConfig, TestError } from '../../types/testReporter';
import { formatError } from '../reporters/base'; import { formatError } from '../reporters/base';
import DotReporter from '../reporters/dot'; import DotReporter from '../reporters/dot';
import EmptyReporter from '../reporters/empty'; import EmptyReporter from '../reporters/empty';
@ -31,10 +31,11 @@ import type { BuiltInReporter, FullConfigInternal } from '../common/config';
import { loadReporter } from './loadUtils'; import { loadReporter } from './loadUtils';
import { BlobReporter } from '../reporters/blob'; import { BlobReporter } from '../reporters/blob';
import type { ReporterDescription } from '../../types/test'; import type { ReporterDescription } from '../../types/test';
import { type ReporterV2, ReporterV2Wrapper } from '../reporters/reporterV2'; import { type ReporterV2, wrapReporterAsV2 } from '../reporters/reporterV2';
export async function createReporters(config: FullConfigInternal, mode: 'list' | 'run' | 'ui' | 'merge', descriptions?: ReporterDescription[]): Promise<ReporterV2[]> { export async function createReporters(config: FullConfigInternal, mode: 'list' | 'run' | 'ui' | 'merge', descriptions?: ReporterDescription[]): Promise<ReporterV2[]> {
const defaultReporters: { [key in Exclude<BuiltInReporter, 'blob'>]: new(arg: any) => Reporter } = { const defaultReporters: { [key in BuiltInReporter]: new(arg: any) => ReporterV2 } = {
blob: BlobReporter,
dot: mode === 'list' ? ListModeReporter : DotReporter, dot: mode === 'list' ? ListModeReporter : DotReporter,
line: mode === 'list' ? ListModeReporter : LineReporter, line: mode === 'list' ? ListModeReporter : LineReporter,
list: mode === 'list' ? ListModeReporter : ListReporter, list: mode === 'list' ? ListModeReporter : ListReporter,
@ -50,18 +51,16 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
for (const r of descriptions) { for (const r of descriptions) {
const [name, arg] = r; const [name, arg] = r;
const options = { ...arg, configDir: config.configDir }; const options = { ...arg, configDir: config.configDir };
if (name === 'blob') { if (name in defaultReporters) {
reporters.push(new BlobReporter(options)); reporters.push(new defaultReporters[name as keyof typeof defaultReporters](options));
} else if (name in defaultReporters) {
reporters.push(new ReporterV2Wrapper(new defaultReporters[name as keyof typeof defaultReporters](options)));
} else { } else {
const reporterConstructor = await loadReporter(config, name); const reporterConstructor = await loadReporter(config, name);
reporters.push(new ReporterV2Wrapper(new reporterConstructor(options))); reporters.push(wrapReporterAsV2(new reporterConstructor(options)));
} }
} }
if (process.env.PW_TEST_REPORTER) { if (process.env.PW_TEST_REPORTER) {
const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER); const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER);
reporters.push(new ReporterV2Wrapper(new reporterConstructor())); reporters.push(wrapReporterAsV2(new reporterConstructor()));
} }
const someReporterPrintsToStdio = reporters.some(r => r.printsToStdio()); const someReporterPrintsToStdio = reporters.some(r => r.printsToStdio());
@ -69,18 +68,21 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
// Add a line/dot/list-mode reporter for convenience. // Add a line/dot/list-mode reporter for convenience.
// Important to put it first, jsut in case some other reporter stalls onEnd. // Important to put it first, jsut in case some other reporter stalls onEnd.
if (mode === 'list') if (mode === 'list')
reporters.unshift(new ReporterV2Wrapper(new ListModeReporter())); reporters.unshift(new ListModeReporter());
else else
reporters.unshift(new ReporterV2Wrapper(!process.env.CI ? new LineReporter({ omitFailures: true }) : new DotReporter())); reporters.unshift(!process.env.CI ? new LineReporter({ omitFailures: true }) : new DotReporter());
} }
return reporters; return reporters;
} }
class ListModeReporter implements Reporter { class ListModeReporter extends EmptyReporter {
private config!: FullConfig; private config!: FullConfig;
onBegin(config: FullConfig, suite: Suite): void { override onConfigure(config: FullConfig) {
this.config = config; this.config = config;
}
override 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();
@ -88,7 +90,7 @@ class ListModeReporter implements Reporter {
for (const test of tests) { for (const test of tests) {
// root, project, file, ...describes, test // root, project, file, ...describes, test
const [, projectName, , ...titles] = test.titlePath(); const [, projectName, , ...titles] = test.titlePath();
const location = `${path.relative(config.rootDir, test.location.file)}:${test.location.line}:${test.location.column}`; const location = `${path.relative(this.config.rootDir, test.location.file)}:${test.location.line}:${test.location.column}`;
const projectTitle = projectName ? `[${projectName}] ` : ''; const projectTitle = projectName ? `[${projectName}] ` : '';
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(` ${projectTitle}${location} ${titles.join(' ')}`); console.log(` ${projectTitle}${location} ${titles.join(' ')}`);
@ -98,7 +100,7 @@ class ListModeReporter implements Reporter {
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'}`);
} }
onError(error: TestError) { override onError(error: TestError) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('\n' + formatError(this.config, error, false).message); console.error('\n' + formatError(this.config, error, false).message);
} }

View file

@ -26,6 +26,7 @@ import { colors } from 'playwright-core/lib/utilsBundle';
import { runWatchModeLoop } from './watchMode'; import { runWatchModeLoop } from './watchMode';
import { runUIMode } from './uiMode'; import { runUIMode } from './uiMode';
import { InternalReporter } from '../reporters/internalReporter'; import { InternalReporter } from '../reporters/internalReporter';
import { Multiplexer } from '../reporters/multiplexer';
type ProjectConfigWithFiles = { type ProjectConfigWithFiles = {
name: string; name: string;
@ -69,7 +70,7 @@ export class Runner {
// Legacy webServer support. // Legacy webServer support.
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
const reporter = new InternalReporter(await createReporters(config, listOnly ? 'list' : 'run')); const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'run')));
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true }) const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true })
: createTaskRunner(config, reporter); : createTaskRunner(config, reporter);

View file

@ -28,7 +28,7 @@ import type { FSWatcher } from 'chokidar';
import { open } from 'playwright-core/lib/utilsBundle'; import { open } from 'playwright-core/lib/utilsBundle';
import ListReporter from '../reporters/list'; import ListReporter from '../reporters/list';
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer'; import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
import { ReporterV2Wrapper } from '../reporters/reporterV2'; import { Multiplexer } from '../reporters/multiplexer';
class UIMode { class UIMode {
private _config: FullConfigInternal; private _config: FullConfigInternal;
@ -68,7 +68,7 @@ class UIMode {
} }
async runGlobalSetup(): Promise<FullResult['status']> { async runGlobalSetup(): Promise<FullResult['status']> {
const reporter = new InternalReporter([new ReporterV2Wrapper(new ListReporter())]); const reporter = new InternalReporter(new ListReporter());
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter); const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
reporter.onConfigure(this._config.config); reporter.onConfigure(this._config.config);
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(this._config, reporter);
@ -161,8 +161,7 @@ class UIMode {
} }
private async _listTests() { private async _listTests() {
const listReporter = new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true); const reporter = new InternalReporter(new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true));
const reporter = new InternalReporter([listReporter]);
this._config.cliListOnly = true; this._config.cliListOnly = true;
this._config.testIdMatcher = undefined; this._config.testIdMatcher = undefined;
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false }); const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
@ -188,7 +187,7 @@ class UIMode {
const reporters = await createReporters(this._config, 'ui'); const reporters = await createReporters(this._config, 'ui');
reporters.push(new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true)); reporters.push(new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true));
const reporter = new InternalReporter(reporters); const reporter = new InternalReporter(new Multiplexer(reporters));
const taskRunner = createTaskRunnerForWatch(this._config, reporter); const taskRunner = createTaskRunnerForWatch(this._config, reporter);
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(this._config, reporter);
clearCompilationCache(); clearCompilationCache();

View file

@ -31,7 +31,6 @@ import { enquirer } from '../utilsBundle';
import { separator } from '../reporters/base'; import { separator } from '../reporters/base';
import { PlaywrightServer } from 'playwright-core/lib/remote/playwrightServer'; import { PlaywrightServer } from 'playwright-core/lib/remote/playwrightServer';
import ListReporter from '../reporters/list'; import ListReporter from '../reporters/list';
import { ReporterV2Wrapper } from '../reporters/reporterV2';
class FSWatcher { class FSWatcher {
private _dirtyTestFiles = new Map<FullProjectInternal, Set<string>>(); private _dirtyTestFiles = new Map<FullProjectInternal, Set<string>>();
@ -113,7 +112,7 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
p.project.retries = 0; p.project.retries = 0;
// Perform global setup. // Perform global setup.
const reporter = new InternalReporter([new ReporterV2Wrapper(new ListReporter())]); const reporter = new InternalReporter(new ListReporter());
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);
const taskRunner = createTaskRunnerForWatchSetup(config, reporter); const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
reporter.onConfigure(config.config); reporter.onConfigure(config.config);
@ -281,7 +280,7 @@ async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<s
title?: string, title?: string,
}) { }) {
printConfiguration(config, options?.title); printConfiguration(config, options?.title);
const reporter = new InternalReporter([new ReporterV2Wrapper(new ListReporter())]); const reporter = new InternalReporter(new ListReporter());
const taskRunner = createTaskRunnerForWatch(config, reporter, options?.additionalFileMatcher); const taskRunner = createTaskRunnerForWatch(config, reporter, options?.additionalFileMatcher);
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);
clearCompilationCache(); clearCompilationCache();

View file

@ -606,6 +606,8 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
}; };
let config: FullConfig; let config: FullConfig;
receiver = new TeleReporterReceiver(pathSeparator, { receiver = new TeleReporterReceiver(pathSeparator, {
version: () => 'v2',
onConfigure: (c: FullConfig) => { onConfigure: (c: FullConfig) => {
config = c; config = c;
}, },

View file

@ -704,3 +704,32 @@ test('should be able to ignore "stderr"', async ({ runInlineTest }, { workerInde
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
expect(result.output).not.toContain('error from server'); expect(result.output).not.toContain('error from server');
}); });
test('should forward stdout when set to "pipe" before server is ready', async ({ runInlineTest }, { workerIndex }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
'web-server.js': `
console.log('output from server');
console.log('\\n%%SEND-SIGINT%%');
setTimeout(() => {}, 10000000);
`,
'test.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({}) => {});
`,
'playwright.config.ts': `
module.exports = {
webServer: {
command: 'node web-server.js',
port: 12345,
stdout: 'pipe',
timeout: 3000,
},
};
`,
}, { workers: 1 }, {}, { sendSIGINTAfter: 1 });
expect(result.passed).toBe(0);
expect(result.output).toContain('[WebServer] output from server');
expect(result.output).not.toContain('Timed out waiting 3000ms');
});