chore: introduce ReporterV2 interface (#23983)

This commit is contained in:
Dmitry Gozman 2023-06-30 13:36:50 -07:00 committed by GitHub
parent 92c738b14a
commit 86c1abd934
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 315 additions and 152 deletions

View file

@ -102,7 +102,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
// New file, just compress the entries. // New file, just compress the entries.
await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true }); await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true });
zipFile.end(undefined, () => { zipFile.end(undefined, () => {
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => promise.resolve()); zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile))
.on('close', () => promise.resolve())
.on('error', error => promise.reject(error));
}); });
await promise; await promise;
await this._deleteStackSession(params.stacksId); await this._deleteStackSession(params.stacksId);

View file

@ -329,7 +329,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
const artifact = new Artifact(this._context, zipFileName); const artifact = new Artifact(this._context, zipFileName);
artifact.reportFinished(); artifact.reportFinished();
result.resolve(artifact); result.resolve(artifact);
}); }).on('error', error => result.reject(error));
return result; return result;
} }

View file

@ -91,7 +91,7 @@ export async function mergeTraceFiles(fileName: string, temporaryTraceFiles: str
Promise.all(temporaryTraceFiles.map(tempFile => fs.promises.unlink(tempFile))).then(() => { Promise.all(temporaryTraceFiles.map(tempFile => fs.promises.unlink(tempFile))).then(() => {
mergePromise.resolve(); mergePromise.resolve();
}); });
}); }).on('error', error => mergePromise.reject(error));
}); });
await mergePromise; await mergePromise;
} }

View file

@ -14,11 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
import type { FullConfig, FullResult, Location, Reporter, TestError, TestResult, TestStatus, TestStep } from '../../types/testReporter'; import type { FullConfig, FullResult, Location, TestError, TestResult, TestStatus, TestStep } from '../../types/testReporter';
import type { Annotation } from '../common/config'; import type { Annotation } from '../common/config';
import type { FullProject, Metadata } from '../../types/test'; import type { FullProject, Metadata } from '../../types/test';
import type * as reporterTypes from '../../types/testReporter'; import type * as reporterTypes from '../../types/testReporter';
import type { SuitePrivate } from '../../types/reporterPrivate'; import type { SuitePrivate } from '../../types/reporterPrivate';
import type { ReporterV2 } from '../reporters/reporterV2';
export type JsonLocation = Location; export type JsonLocation = Location;
export type JsonError = string; export type JsonError = string;
@ -121,14 +122,16 @@ export type JsonEvent = {
export class TeleReporterReceiver { export class TeleReporterReceiver {
private _rootSuite: TeleSuite; private _rootSuite: TeleSuite;
private _pathSeparator: string; private _pathSeparator: string;
private _reporter: Reporter; private _reporter: ReporterV2;
private _tests = new Map<string, TeleTestCase>(); private _tests = new Map<string, TeleTestCase>();
private _rootDir!: string; private _rootDir!: string;
private _listOnly = false;
private _clearPreviousResultsWhenTestBegins: boolean = false; private _clearPreviousResultsWhenTestBegins: boolean = false;
private _reuseTestCases: boolean; private _reuseTestCases: boolean;
private _reportConfig: MergeReporterConfig | undefined; private _reportConfig: MergeReporterConfig | undefined;
private _config!: FullConfig;
constructor(pathSeparator: string, reporter: Reporter, reuseTestCases: boolean, reportConfig?: MergeReporterConfig) { constructor(pathSeparator: string, reporter: ReporterV2, reuseTestCases: boolean, reportConfig?: MergeReporterConfig) {
this._rootSuite = new TeleSuite('', 'root'); this._rootSuite = new TeleSuite('', 'root');
this._pathSeparator = pathSeparator; this._pathSeparator = pathSeparator;
this._reporter = reporter; this._reporter = reporter;
@ -136,10 +139,14 @@ export class TeleReporterReceiver {
this._reportConfig = reportConfig; this._reportConfig = reportConfig;
} }
dispatch(message: JsonEvent): Promise<void> | undefined { dispatch(message: JsonEvent): Promise<void> | void {
const { method, params } = message; const { method, params } = message;
if (method === 'onConfigure') {
this._onConfigure(params.config);
return;
}
if (method === 'onBegin') { if (method === 'onBegin') {
this._onBegin(params.config, params.projects); this._onBegin(params.projects);
return; return;
} }
if (method === 'onTestBegin') { if (method === 'onTestBegin') {
@ -176,8 +183,14 @@ export class TeleReporterReceiver {
this._clearPreviousResultsWhenTestBegins = true; this._clearPreviousResultsWhenTestBegins = true;
} }
private _onBegin(config: JsonConfig, projects: JsonProject[]) { private _onConfigure(config: JsonConfig) {
this._rootDir = this._reportConfig?.rootDir || config.rootDir; this._rootDir = this._reportConfig?.rootDir || config.rootDir;
this._listOnly = config.listOnly;
this._config = this._parseConfig(config);
this._reporter.onConfigure(this._config);
}
private _onBegin(projects: JsonProject[]) {
for (const project of projects) { for (const project of projects) {
let projectSuite = this._rootSuite.suites.find(suite => suite.project()!.id === project.id); let projectSuite = this._rootSuite.suites.find(suite => suite.project()!.id === project.id);
if (!projectSuite) { if (!projectSuite) {
@ -191,7 +204,7 @@ export class TeleReporterReceiver {
// Remove deleted tests when listing. Empty suites will be auto-filtered // Remove deleted tests when listing. Empty suites will be auto-filtered
// in the UI layer. // in the UI layer.
if (config.listOnly) { if (this._listOnly) {
const testIds = new Set<string>(); const testIds = new Set<string>();
const collectIds = (suite: JsonSuite) => { const collectIds = (suite: JsonSuite) => {
suite.tests.map(t => t.testId).forEach(testId => testIds.add(testId)); suite.tests.map(t => t.testId).forEach(testId => testIds.add(testId));
@ -206,7 +219,7 @@ export class TeleReporterReceiver {
filterTests(projectSuite); filterTests(projectSuite);
} }
} }
this._reporter.onBegin?.(this._parseConfig(config), this._rootSuite); this._reporter.onBegin?.(this._rootSuite);
} }
private _onTestBegin(testId: string, payload: JsonTestResultStart) { private _onTestBegin(testId: string, payload: JsonTestResultStart) {
@ -289,11 +302,11 @@ export class TeleReporterReceiver {
} }
} }
private _onEnd(result: FullResult): Promise<void> | undefined { private _onEnd(result: FullResult): Promise<void> | void {
return this._reporter.onEnd?.(result) || undefined; return this._reporter.onEnd?.(result);
} }
private _onExit(): Promise<void> | undefined { private _onExit(): Promise<void> | void {
return this._reporter.onExit?.(); return this._reporter.onExit?.();
} }

View file

@ -15,11 +15,11 @@
*/ */
import type { FullConfig, Suite } from '../../types/testReporter'; import type { FullConfig, Suite } from '../../types/testReporter';
import type { InternalReporter } from '../reporters/internalReporter'; import type { ReporterV2 } from '../reporters/reporterV2';
export interface TestRunnerPlugin { export interface TestRunnerPlugin {
name: string; name: string;
setup?(config: FullConfig, configDir: string, reporter: InternalReporter): Promise<void>; setup?(config: FullConfig, configDir: string, reporter: ReporterV2): Promise<void>;
begin?(suite: Suite): Promise<void>; begin?(suite: Suite): Promise<void>;
end?(): Promise<void>; end?(): Promise<void>;
teardown?(): Promise<void>; teardown?(): Promise<void>;

View file

@ -23,7 +23,7 @@ import type { FullConfig } from '../../types/testReporter';
import type { TestRunnerPlugin } from '.'; import type { TestRunnerPlugin } from '.';
import type { FullConfigInternal } from '../common/config'; import type { FullConfigInternal } from '../common/config';
import { envWithoutExperimentalLoaderOptions } from '../util'; import { envWithoutExperimentalLoaderOptions } from '../util';
import type { InternalReporter } from '../reporters/internalReporter'; import type { ReporterV2 } from '../reporters/reporterV2';
export type WebServerPluginOptions = { export type WebServerPluginOptions = {
@ -50,7 +50,7 @@ export class WebServerPlugin implements TestRunnerPlugin {
private _processExitedPromise!: Promise<any>; private _processExitedPromise!: Promise<any>;
private _options: WebServerPluginOptions; private _options: WebServerPluginOptions;
private _checkPortOnly: boolean; private _checkPortOnly: boolean;
private _reporter?: InternalReporter; private _reporter?: ReporterV2;
name = 'playwright:webserver'; name = 'playwright:webserver';
constructor(options: WebServerPluginOptions, checkPortOnly: boolean) { constructor(options: WebServerPluginOptions, checkPortOnly: boolean) {
@ -58,7 +58,7 @@ export class WebServerPlugin implements TestRunnerPlugin {
this._checkPortOnly = checkPortOnly; this._checkPortOnly = checkPortOnly;
} }
public async setup(config: FullConfig, configDir: string, reporter: InternalReporter) { public async setup(config: FullConfig, configDir: string, reporter: ReporterV2) {
this._reporter = reporter; this._reporter = reporter;
this._isAvailable = getIsAvailableFunction(this._options.url, this._checkPortOnly, !!this._options.ignoreHTTPSErrors, this._reporter.onStdErr?.bind(this._reporter)); this._isAvailable = getIsAvailableFunction(this._options.url, this._checkPortOnly, !!this._options.ignoreHTTPSErrors, this._reporter.onStdErr?.bind(this._reporter));
this._options.cwd = this._options.cwd ? path.resolve(configDir, this._options.cwd) : configDir; this._options.cwd = this._options.cwd ? path.resolve(configDir, this._options.cwd) : configDir;
@ -152,7 +152,7 @@ async function isPortUsed(port: number): Promise<boolean> {
return await innerIsPortUsed('127.0.0.1') || await innerIsPortUsed('::1'); return await innerIsPortUsed('127.0.0.1') || await innerIsPortUsed('::1');
} }
async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: InternalReporter['onStdErr']) { async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']) {
let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onStdErr); let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onStdErr);
if (statusCode === 404 && url.pathname === '/') { if (statusCode === 404 && url.pathname === '/') {
const indexUrl = new URL(url); const indexUrl = new URL(url);
@ -162,7 +162,7 @@ async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: In
return statusCode >= 200 && statusCode < 404; return statusCode >= 200 && statusCode < 404;
} }
async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onStdErr: InternalReporter['onStdErr']): Promise<number> { async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']): Promise<number> {
return new Promise(resolve => { return new Promise(resolve => {
debugWebServer(`HTTP GET: ${url}`); debugWebServer(`HTTP GET: ${url}`);
httpRequest({ httpRequest({
@ -195,7 +195,7 @@ async function waitFor(waitFn: () => Promise<boolean>, cancellationToken: { canc
} }
} }
function getIsAvailableFunction(url: string, checkPortOnly: boolean, ignoreHTTPSErrors: boolean, onStdErr: InternalReporter['onStdErr']) { function getIsAvailableFunction(url: string, checkPortOnly: boolean, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']) {
const urlObject = new URL(url); const urlObject = new URL(url);
if (!checkPortOnly) if (!checkPortOnly)
return () => isURLAvailable(urlObject, ignoreHTTPSErrors, onStdErr); return () => isURLAvailable(urlObject, ignoreHTTPSErrors, onStdErr);

View file

@ -118,7 +118,7 @@ export class BaseReporter implements Reporter {
} }
protected generateStartingMessage() { protected generateStartingMessage() {
const jobs = this.config.workers; const jobs = this.config.metadata.actualWorkers ?? this.config.workers;
const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : ''; const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : '';
if (!this.totalTestCount) if (!this.totalTestCount)
return ''; return '';

View file

@ -57,15 +57,16 @@ export class BlobReporter extends TeleReporterEmitter {
}); });
} }
printsToStdio() { override onConfigure(config: FullConfig) {
return false; this._outputDir = path.resolve(this._options.configDir, this._options.outputDir || 'blob-report');
this._reportName = this._computeReportName(config);
super.onConfigure(config);
} }
override onBegin(config: FullConfig<{}, {}>, suite: Suite): void { override onBegin(suite: Suite): void {
this._outputDir = path.resolve(this._options.configDir, this._options.outputDir || 'blob-report'); // Note: config.outputDir is cleared betwee onConfigure and onBegin, so we call mkdir here.
fs.mkdirSync(path.join(this._outputDir, 'resources'), { recursive: true }); fs.mkdirSync(path.join(this._outputDir, 'resources'), { recursive: true });
this._reportName = this._computeReportName(config); super.onBegin(suite);
super.onBegin(config, suite);
} }
override async onEnd(result: FullResult): Promise<void> { override async onEnd(result: FullResult): Promise<void> {
@ -79,14 +80,16 @@ export class BlobReporter extends TeleReporterEmitter {
const zipFileName = path.join(this._outputDir, this._reportName + '.zip'); const zipFileName = path.join(this._outputDir, this._reportName + '.zip');
zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', () => { zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', () => {
zipFinishPromise.resolve(undefined); zipFinishPromise.resolve(undefined);
}); }).on('error', error => zipFinishPromise.reject(error));
zipFile.addReadStream(content, this._reportName + '.jsonl'); zipFile.addReadStream(content, this._reportName + '.jsonl');
zipFile.end(); zipFile.end();
await Promise.all([ await Promise.all([
...this._copyFilePromises, ...this._copyFilePromises,
// Requires Node v14.18.0+ // Requires Node v14.18.0+
zipFinishPromise.catch(e => console.error(`Failed to write report ${zipFileName}: ${e}`)) zipFinishPromise.catch(e => {
throw new Error(`Failed to write report ${zipFileName}: ` + e.message);
}),
]); ]);
} }

View file

@ -17,44 +17,29 @@
import fs from 'fs'; import fs from 'fs';
import { colors } from 'playwright-core/lib/utilsBundle'; 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, Reporter } 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 type { FullConfigInternal } from '../common/config';
import { Multiplexer } from './multiplexer'; import { Multiplexer } from './multiplexer';
import { prepareErrorStack, relativeFilePath } from './base'; import { prepareErrorStack, relativeFilePath } from './base';
import type { ReporterV2 } from './reporterV2';
type StdIOChunk = { export class InternalReporter implements ReporterV2 {
chunk: string | Buffer;
test?: TestCase;
result?: TestResult;
};
export class InternalReporter {
private _multiplexer: Multiplexer; private _multiplexer: Multiplexer;
private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = []; private _didBegin = false;
private _config!: FullConfigInternal; private _config!: FullConfig;
constructor(reporters: Reporter[]) { constructor(reporters: ReporterV2[]) {
this._multiplexer = new Multiplexer(reporters); this._multiplexer = new Multiplexer(reporters);
} }
onConfigure(config: FullConfigInternal) { onConfigure(config: FullConfig) {
this._config = config; this._config = config;
this._multiplexer.onConfigure(config);
} }
onBegin(config: FullConfig, suite: Suite) { onBegin(suite: Suite) {
this._multiplexer.onBegin(config, suite); this._didBegin = true;
this._multiplexer.onBegin(suite);
const deferred = this._deferred!;
this._deferred = null;
for (const item of deferred) {
if (item.error)
this.onError(item.error);
if (item.stdout)
this.onStdOut(item.stdout.chunk, item.stdout.test, item.stdout.result);
if (item.stderr)
this.onStdErr(item.stderr.chunk, item.stderr.test, item.stderr.result);
}
} }
onTestBegin(test: TestCase, result: TestResult) { onTestBegin(test: TestCase, result: TestResult) {
@ -62,19 +47,10 @@ export class InternalReporter {
} }
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) { onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
if (this._deferred) {
this._deferred.push({ stdout: { chunk, test, result } });
return;
}
this._multiplexer.onStdOut(chunk, test, result); this._multiplexer.onStdOut(chunk, test, result);
} }
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) { onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
if (this._deferred) {
this._deferred.push({ stderr: { chunk, test, result } });
return;
}
this._multiplexer.onStdErr(chunk, test, result); this._multiplexer.onStdErr(chunk, test, result);
} }
@ -84,9 +60,9 @@ export class InternalReporter {
} }
async onEnd(result: FullResult) { async onEnd(result: FullResult) {
if (this._deferred) { if (!this._didBegin) {
// onBegin was not reported, emit it. // onBegin was not reported, emit it.
this.onBegin(this._config.config, new Suite('', 'root')); this.onBegin(new Suite('', 'root'));
} }
await this._multiplexer.onEnd(result); await this._multiplexer.onEnd(result);
} }
@ -96,11 +72,7 @@ export class InternalReporter {
} }
onError(error: TestError) { onError(error: TestError) {
if (this._deferred) { addLocationAndSnippetToError(this._config, error);
this._deferred.push({ error });
return;
}
addLocationAndSnippetToError(this._config.config, error);
this._multiplexer.onError(error); this._multiplexer.onError(error);
} }
@ -113,14 +85,18 @@ export class InternalReporter {
this._multiplexer.onStepEnd(test, result, step); this._multiplexer.onStepEnd(test, result, step);
} }
printsToStdio() {
return this._multiplexer.printsToStdio();
}
private _addSnippetToTestErrors(test: TestCase, result: TestResult) { private _addSnippetToTestErrors(test: TestCase, result: TestResult) {
for (const error of result.errors) for (const error of result.errors)
addLocationAndSnippetToError(this._config.config, error, test.location.file); addLocationAndSnippetToError(this._config, error, test.location.file);
} }
private _addSnippetToStepError(test: TestCase, step: TestStep) { private _addSnippetToStepError(test: TestCase, step: TestStep) {
if (step.error) if (step.error)
addLocationAndSnippetToError(this._config.config, step.error, test.location.file); addLocationAndSnippetToError(this._config, step.error, test.location.file);
} }
} }

View file

@ -73,13 +73,16 @@ async function extractReportFromZip(file: string): Promise<Buffer> {
async function mergeEvents(dir: string, shardReportFiles: string[]) { async function mergeEvents(dir: string, shardReportFiles: string[]) {
const events: JsonEvent[] = []; const events: JsonEvent[] = [];
const configureEvents: JsonEvent[] = [];
const beginEvents: JsonEvent[] = []; const beginEvents: JsonEvent[] = [];
const endEvents: JsonEvent[] = []; const endEvents: JsonEvent[] = [];
for (const reportFile of shardReportFiles) { for (const reportFile of shardReportFiles) {
const reportJsonl = await extractReportFromZip(path.join(dir, reportFile)); const reportJsonl = await extractReportFromZip(path.join(dir, reportFile));
const parsedEvents = parseEvents(reportJsonl); const parsedEvents = parseEvents(reportJsonl);
for (const event of parsedEvents) { for (const event of parsedEvents) {
if (event.method === 'onBegin') if (event.method === 'onConfigure')
configureEvents.push(event);
else if (event.method === 'onBegin')
beginEvents.push(event); beginEvents.push(event);
else if (event.method === 'onEnd') else if (event.method === 'onEnd')
endEvents.push(event); endEvents.push(event);
@ -89,13 +92,12 @@ async function mergeEvents(dir: string, shardReportFiles: string[]) {
events.push(event); events.push(event);
} }
} }
return [mergeBeginEvents(beginEvents), ...events, mergeEndEvents(endEvents), { method: 'onExit', params: undefined }]; return [mergeConfigureEvents(configureEvents), mergeBeginEvents(beginEvents), ...events, mergeEndEvents(endEvents), { method: 'onExit', params: undefined }];
} }
function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent { function mergeConfigureEvents(configureEvents: JsonEvent[]): JsonEvent {
if (!beginEvents.length) if (!configureEvents.length)
throw new Error('No begin events found'); throw new Error('No configure events found');
const projects: JsonProject[] = [];
let config: JsonConfig = { let config: JsonConfig = {
configFile: undefined, configFile: undefined,
globalTimeout: 0, globalTimeout: 0,
@ -108,8 +110,21 @@ function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent {
workers: 0, workers: 0,
listOnly: false listOnly: false
}; };
for (const event of beginEvents) { for (const event of configureEvents)
config = mergeConfigs(config, event.params.config); config = mergeConfigs(config, event.params.config);
return {
method: 'onConfigure',
params: {
config,
}
};
}
function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent {
if (!beginEvents.length)
throw new Error('No begin events found');
const projects: JsonProject[] = [];
for (const event of beginEvents) {
const shardProjects: JsonProject[] = event.params.projects; const shardProjects: JsonProject[] = event.params.projects;
for (const shardProject of shardProjects) { for (const shardProject of shardProjects) {
const mergedProject = projects.find(p => p.id === shardProject.id); const mergedProject = projects.find(p => p.id === shardProject.id);
@ -122,7 +137,6 @@ function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent {
return { return {
method: 'onBegin', method: 'onBegin',
params: { params: {
config,
projects, projects,
} }
}; };
@ -136,6 +150,7 @@ function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig {
...to.metadata, ...to.metadata,
...from.metadata, ...from.metadata,
totalTime: to.metadata.totalTime + from.metadata.totalTime, totalTime: to.metadata.totalTime + from.metadata.totalTime,
actualWorkers: (to.metadata.actualWorkers || 0) + (from.metadata.actualWorkers || 0),
}, },
workers: to.workers + from.workers, workers: to.workers + from.workers,
}; };

View file

@ -14,64 +14,78 @@
* limitations under the License. * limitations under the License.
*/ */
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter } from '../../types/testReporter'; import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep } from '../../types/testReporter';
import type { Suite } from '../common/test'; import type { Suite } from '../common/test';
import type { ReporterV2 } from './reporterV2';
export class Multiplexer implements Reporter { export class Multiplexer implements ReporterV2 {
private _reporters: Reporter[]; private _reporters: ReporterV2[];
constructor(reporters: Reporter[]) { constructor(reporters: ReporterV2[]) {
this._reporters = reporters; this._reporters = reporters;
} }
onBegin(config: FullConfig, suite: Suite) { onConfigure(config: FullConfig) {
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onBegin?.(config, suite)); wrap(() => reporter.onConfigure(config));
}
onBegin(suite: Suite) {
for (const reporter of this._reporters)
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)
await wrapAsync(() => reporter.onEnd?.(result)); await wrapAsync(() => reporter.onEnd(result));
} }
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 {
return this._reporters.some(r => {
let prints = true;
wrap(() => prints = r.printsToStdio());
return prints;
});
} }
} }

View file

@ -0,0 +1,119 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter, Suite } from '../../types/testReporter';
export interface ReporterV2 {
onConfigure(config: FullConfig): void;
onBegin(suite: Suite): void;
onTestBegin(test: TestCase, result: TestResult): void;
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
onTestEnd(test: TestCase, result: TestResult): void;
onEnd(result: FullResult): void | Promise<void>;
onExit(): void | Promise<void>;
onError(error: TestError): void;
onStepBegin(test: TestCase, result: TestResult, step: TestStep): void;
onStepEnd(test: TestCase, result: TestResult, step: TestStep): void;
printsToStdio(): boolean;
}
type StdIOChunk = {
chunk: string | Buffer;
test?: TestCase;
result?: TestResult;
};
export class ReporterV2Wrapper implements ReporterV2 {
private _reporter: Reporter;
private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = [];
private _config!: FullConfig;
constructor(reporter: Reporter) {
this._reporter = reporter;
}
onConfigure(config: FullConfig) {
this._config = config;
}
onBegin(suite: Suite) {
this._reporter.onBegin?.(this._config, suite);
const deferred = this._deferred!;
this._deferred = null;
for (const item of deferred) {
if (item.error)
this.onError(item.error);
if (item.stdout)
this.onStdOut(item.stdout.chunk, item.stdout.test, item.stdout.result);
if (item.stderr)
this.onStdErr(item.stderr.chunk, item.stderr.test, item.stderr.result);
}
}
onTestBegin(test: TestCase, result: TestResult) {
this._reporter.onTestBegin?.(test, result);
}
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
if (this._deferred) {
this._deferred.push({ stdout: { chunk, test, result } });
return;
}
this._reporter.onStdOut?.(chunk, test, result);
}
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
if (this._deferred) {
this._deferred.push({ stderr: { chunk, test, result } });
return;
}
this._reporter.onStdErr?.(chunk, test, result);
}
onTestEnd(test: TestCase, result: TestResult) {
this._reporter.onTestEnd?.(test, result);
}
async onEnd(result: FullResult) {
await this._reporter.onEnd?.(result);
}
async onExit() {
await this._reporter.onExit?.();
}
onError(error: TestError) {
if (this._deferred) {
this._deferred.push({ error });
return;
}
this._reporter.onError?.(error);
}
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
this._reporter.onStepBegin?.(test, result, step);
}
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
this._reporter.onStepEnd?.(test, result, step);
}
printsToStdio() {
return this._reporter.printsToStdio ? this._reporter.printsToStdio() : true;
}
}

View file

@ -17,13 +17,14 @@
import path from 'path'; import path from 'path';
import { createGuid } from 'playwright-core/lib/utils'; import { createGuid } from 'playwright-core/lib/utils';
import type { SuitePrivate } from '../../types/reporterPrivate'; import type { SuitePrivate } from '../../types/reporterPrivate';
import type { FullConfig, FullResult, Location, Reporter, TestError, TestResult, TestStep } from '../../types/testReporter'; import type { FullConfig, FullResult, Location, TestError, TestResult, TestStep } from '../../types/testReporter';
import { FullConfigInternal, FullProjectInternal } from '../common/config'; import { FullConfigInternal, FullProjectInternal } from '../common/config';
import type { Suite, TestCase } from '../common/test'; import type { Suite, TestCase } 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, JsonProject, JsonStdIOType, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
import { serializeRegexPatterns } from '../isomorphic/teleReceiver'; import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
import type { ReporterV2 } from './reporterV2';
export class TeleReporterEmitter implements Reporter { export class TeleReporterEmitter implements ReporterV2 {
private _messageSink: (message: JsonEvent) => void; private _messageSink: (message: JsonEvent) => void;
private _rootDir!: string; private _rootDir!: string;
private _skipBuffers: boolean; private _skipBuffers: boolean;
@ -33,10 +34,14 @@ export class TeleReporterEmitter implements Reporter {
this._skipBuffers = skipBuffers; this._skipBuffers = skipBuffers;
} }
onBegin(config: FullConfig, suite: Suite) { onConfigure(config: FullConfig) {
this._rootDir = config.rootDir; this._rootDir = config.rootDir;
this._messageSink({ method: 'onConfigure', params: { config: this._serializeConfig(config) } });
}
onBegin(suite: Suite) {
const projects = suite.suites.map(projectSuite => this._serializeProject(projectSuite)); const projects = suite.suites.map(projectSuite => this._serializeProject(projectSuite));
this._messageSink({ method: 'onBegin', params: { config: this._serializeConfig(config), projects } }); this._messageSink({ method: 'onBegin', params: { projects } });
} }
onTestBegin(test: TestCase, result: TestResult): void { onTestBegin(test: TestCase, result: TestResult): void {
@ -96,11 +101,11 @@ export class TeleReporterEmitter implements Reporter {
}); });
} }
onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void { onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void {
this._onStdIO('stdout', chunk, test, result); this._onStdIO('stdout', chunk, test, result);
} }
onStdErr(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void { onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void {
this._onStdIO('stderr', chunk, test, result); this._onStdIO('stderr', chunk, test, result);
} }
@ -120,6 +125,10 @@ export class TeleReporterEmitter implements Reporter {
async onExit() { async onExit() {
} }
printsToStdio() {
return false;
}
private _serializeConfig(config: FullConfig): JsonConfig { private _serializeConfig(config: FullConfig): JsonConfig {
return { return {
configFile: this._relativePath(config.configFile), configFile: this._relativePath(config.configFile),

View file

@ -24,7 +24,7 @@ import { ManualPromise } from 'playwright-core/lib/utils';
import { WorkerHost } from './workerHost'; import { WorkerHost } from './workerHost';
import type { TestGroup } from './testGroups'; import type { TestGroup } from './testGroups';
import type { FullConfigInternal } from '../common/config'; import type { FullConfigInternal } from '../common/config';
import type { InternalReporter } from '../reporters/internalReporter'; import type { ReporterV2 } from '../reporters/reporterV2';
type TestResultData = { type TestResultData = {
result: TestResult; result: TestResult;
@ -46,14 +46,14 @@ export class Dispatcher {
private _testById = new Map<string, TestData>(); private _testById = new Map<string, TestData>();
private _config: FullConfigInternal; private _config: FullConfigInternal;
private _reporter: InternalReporter; private _reporter: ReporterV2;
private _hasWorkerErrors = false; private _hasWorkerErrors = false;
private _failureCount = 0; private _failureCount = 0;
private _extraEnvByProjectId: EnvByProjectId = new Map(); private _extraEnvByProjectId: EnvByProjectId = new Map();
private _producedEnvByProjectId: EnvByProjectId = new Map(); private _producedEnvByProjectId: EnvByProjectId = new Map();
constructor(config: FullConfigInternal, reporter: InternalReporter) { constructor(config: FullConfigInternal, reporter: ReporterV2) {
this._config = config; this._config = config;
this._reporter = reporter; this._reporter = reporter;
} }

View file

@ -31,9 +31,10 @@ 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';
export async function createReporters(config: FullConfigInternal, mode: 'list' | 'run' | 'ui' | 'merge', descriptions?: ReporterDescription[]): Promise<Reporter[]> { export async function createReporters(config: FullConfigInternal, mode: 'list' | 'run' | 'ui' | 'merge', descriptions?: ReporterDescription[]): Promise<ReporterV2[]> {
const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = { const defaultReporters: { [key in Exclude<BuiltInReporter, 'blob'>]: new(arg: any) => Reporter } = {
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,
@ -42,42 +43,40 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
junit: JUnitReporter, junit: JUnitReporter,
null: EmptyReporter, null: EmptyReporter,
html: mode === 'ui' ? LineReporter : HtmlReporter, html: mode === 'ui' ? LineReporter : HtmlReporter,
blob: BlobReporter,
markdown: MarkdownReporter, markdown: MarkdownReporter,
}; };
const reporters: Reporter[] = []; const reporters: ReporterV2[] = [];
descriptions ??= config.config.reporter; descriptions ??= config.config.reporter;
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 in defaultReporters) { if (name === 'blob') {
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](options)); reporters.push(new BlobReporter(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 reporterConstructor(options)); reporters.push(new ReporterV2Wrapper(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 reporterConstructor()); reporters.push(new ReporterV2Wrapper(new reporterConstructor()));
} }
const someReporterPrintsToStdio = reporters.some(r => { const someReporterPrintsToStdio = reporters.some(r => r.printsToStdio());
const prints = r.printsToStdio ? r.printsToStdio() : true;
return prints;
});
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, 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 ListModeReporter()); reporters.unshift(new ReporterV2Wrapper(new ListModeReporter()));
else else
reporters.unshift(!process.env.CI ? new LineReporter({ omitFailures: true }) : new DotReporter()); reporters.unshift(new ReporterV2Wrapper(!process.env.CI ? new LineReporter({ omitFailures: true }) : new DotReporter()));
} }
return reporters; return reporters;
} }
export class ListModeReporter implements Reporter { class ListModeReporter implements Reporter {
private config!: FullConfig; private config!: FullConfig;
onBegin(config: FullConfig, suite: Suite): void { onBegin(config: FullConfig, suite: Suite): void {

View file

@ -74,7 +74,7 @@ export class Runner {
: createTaskRunner(config, reporter); : createTaskRunner(config, reporter);
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);
reporter.onConfigure(config); reporter.onConfigure(config.config);
if (!listOnly && config.ignoreSnapshots) { if (!listOnly && config.ignoreSnapshots) {
reporter.onStdOut(colors.dim([ reporter.onStdOut(colors.dim([

View file

@ -19,20 +19,20 @@ import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils';
import type { FullResult, TestError } from '../../reporter'; import type { FullResult, TestError } from '../../reporter';
import { SigIntWatcher } from './sigIntWatcher'; import { SigIntWatcher } from './sigIntWatcher';
import { serializeError } from '../util'; import { serializeError } from '../util';
import type { InternalReporter } from '../reporters/internalReporter'; import type { ReporterV2 } from '../reporters/reporterV2';
type TaskTeardown = () => Promise<any> | undefined; type TaskTeardown = () => Promise<any> | undefined;
export type Task<Context> = (context: Context, errors: TestError[], softErrors: TestError[]) => Promise<TaskTeardown | void> | undefined; export type Task<Context> = (context: Context, errors: TestError[], softErrors: TestError[]) => Promise<TaskTeardown | void> | undefined;
export class TaskRunner<Context> { export class TaskRunner<Context> {
private _tasks: { name: string, task: Task<Context> }[] = []; private _tasks: { name: string, task: Task<Context> }[] = [];
private _reporter: InternalReporter; private _reporter: ReporterV2;
private _hasErrors = false; private _hasErrors = false;
private _interrupted = false; private _interrupted = false;
private _isTearDown = false; private _isTearDown = false;
private _globalTimeoutForError: number; private _globalTimeoutForError: number;
constructor(reporter: InternalReporter, globalTimeoutForError: number) { constructor(reporter: ReporterV2, globalTimeoutForError: number) {
this._reporter = reporter; this._reporter = reporter;
this._globalTimeoutForError = globalTimeoutForError; this._globalTimeoutForError = globalTimeoutForError;
} }

View file

@ -20,7 +20,7 @@ import { promisify } from 'util';
import { debug, rimraf } from 'playwright-core/lib/utilsBundle'; import { debug, rimraf } from 'playwright-core/lib/utilsBundle';
import { Dispatcher, type EnvByProjectId } from './dispatcher'; import { Dispatcher, type EnvByProjectId } from './dispatcher';
import type { TestRunnerPluginRegistration } from '../plugins'; import type { TestRunnerPluginRegistration } from '../plugins';
import type { InternalReporter } from '../reporters/internalReporter'; import type { ReporterV2 } from '../reporters/reporterV2';
import { createTestGroups, type TestGroup } from '../runner/testGroups'; import { createTestGroups, type TestGroup } from '../runner/testGroups';
import type { Task } from './taskRunner'; import type { Task } from './taskRunner';
import { TaskRunner } from './taskRunner'; import { TaskRunner } from './taskRunner';
@ -46,7 +46,7 @@ export type Phase = {
}; };
export class TestRun { export class TestRun {
readonly reporter: InternalReporter; readonly reporter: ReporterV2;
readonly config: FullConfigInternal; readonly config: FullConfigInternal;
rootSuite: Suite | undefined = undefined; rootSuite: Suite | undefined = undefined;
readonly phases: Phase[] = []; readonly phases: Phase[] = [];
@ -55,13 +55,13 @@ export class TestRun {
projectType: Map<FullProjectInternal, 'top-level' | 'dependency'> = new Map(); projectType: Map<FullProjectInternal, 'top-level' | 'dependency'> = new Map();
projectSuites: Map<FullProjectInternal, Suite[]> = new Map(); projectSuites: Map<FullProjectInternal, Suite[]> = new Map();
constructor(config: FullConfigInternal, reporter: InternalReporter) { constructor(config: FullConfigInternal, reporter: ReporterV2) {
this.config = config; this.config = config;
this.reporter = reporter; this.reporter = reporter;
} }
} }
export function createTaskRunner(config: FullConfigInternal, reporter: InternalReporter): TaskRunner<TestRun> { export function createTaskRunner(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout); const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
addGlobalSetupTasks(taskRunner, config); addGlobalSetupTasks(taskRunner, config);
taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true })); taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true }));
@ -69,13 +69,13 @@ export function createTaskRunner(config: FullConfigInternal, reporter: InternalR
return taskRunner; return taskRunner;
} }
export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: InternalReporter): TaskRunner<TestRun> { export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, 0); const taskRunner = new TaskRunner<TestRun>(reporter, 0);
addGlobalSetupTasks(taskRunner, config); addGlobalSetupTasks(taskRunner, config);
return taskRunner; return taskRunner;
} }
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: InternalReporter, additionalFileMatcher?: Matcher): TaskRunner<TestRun> { export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: ReporterV2, additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, 0); const taskRunner = new TaskRunner<TestRun>(reporter, 0);
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, additionalFileMatcher })); taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, additionalFileMatcher }));
addRunTasks(taskRunner, config); addRunTasks(taskRunner, config);
@ -100,7 +100,7 @@ function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal
return taskRunner; return taskRunner;
} }
export function createTaskRunnerForList(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner<TestRun> { export function createTaskRunnerForList(config: FullConfigInternal, reporter: ReporterV2, mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout); const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false })); taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false }));
taskRunner.addTask('report begin', createReportBeginTask()); taskRunner.addTask('report begin', createReportBeginTask());
@ -110,7 +110,7 @@ export function createTaskRunnerForList(config: FullConfigInternal, reporter: In
function createReportBeginTask(): Task<TestRun> { function createReportBeginTask(): Task<TestRun> {
return async ({ config, reporter, rootSuite }) => { return async ({ config, reporter, rootSuite }) => {
const montonicStartTime = monotonicTime(); const montonicStartTime = monotonicTime();
reporter.onBegin(config.config, rootSuite!); reporter.onBegin(rootSuite!);
return async () => { return async () => {
config.config.metadata.totalTime = monotonicTime() - montonicStartTime; config.config.metadata.totalTime = monotonicTime() - montonicStartTime;
}; };
@ -228,7 +228,7 @@ function createPhasesTask(): Task<TestRun> {
} }
} }
testRun.config.config.workers = Math.min(testRun.config.config.workers, maxConcurrentTestGroups); testRun.config.config.metadata.actualWorkers = Math.min(testRun.config.config.workers, maxConcurrentTestGroups);
}; };
} }

View file

@ -28,6 +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';
class UIMode { class UIMode {
private _config: FullConfigInternal; private _config: FullConfigInternal;
@ -67,9 +68,9 @@ class UIMode {
} }
async runGlobalSetup(): Promise<FullResult['status']> { async runGlobalSetup(): Promise<FullResult['status']> {
const reporter = new InternalReporter([new ListReporter()]); const reporter = new InternalReporter([new ReporterV2Wrapper(new ListReporter())]);
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter); const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
reporter.onConfigure(this._config); reporter.onConfigure(this._config.config);
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(this._config, reporter);
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0); const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
await reporter.onEnd({ status }); await reporter.onEnd({ status });
@ -167,7 +168,7 @@ class UIMode {
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false }); const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(this._config, reporter);
clearCompilationCache(); clearCompilationCache();
reporter.onConfigure(this._config); reporter.onConfigure(this._config.config);
const status = await taskRunner.run(testRun, 0); const status = await taskRunner.run(testRun, 0);
await reporter.onEnd({ status }); await reporter.onEnd({ status });
await reporter.onExit(); await reporter.onExit();
@ -191,7 +192,7 @@ class UIMode {
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();
reporter.onConfigure(this._config); reporter.onConfigure(this._config.config);
const stop = new ManualPromise(); const stop = new ManualPromise();
const run = taskRunner.run(testRun, 0, stop).then(async status => { const run = taskRunner.run(testRun, 0, stop).then(async status => {
await reporter.onEnd({ status }); await reporter.onEnd({ status });

View file

@ -31,6 +31,7 @@ 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>>();
@ -112,10 +113,10 @@ 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 ListReporter()]); const reporter = new InternalReporter([new ReporterV2Wrapper(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); reporter.onConfigure(config.config);
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0); const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
if (status !== 'passed') if (status !== 'passed')
await globalCleanup(); await globalCleanup();
@ -280,11 +281,11 @@ 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 ListReporter()]); const reporter = new InternalReporter([new ReporterV2Wrapper(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();
reporter.onConfigure(config); reporter.onConfigure(config.config);
const taskStatus = await taskRunner.run(testRun, 0); const taskStatus = await taskRunner.run(testRun, 0);
let status: FullResult['status'] = 'passed'; let status: FullResult['status'] = 'passed';

View file

@ -598,7 +598,7 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
return sendMessage('list', {}); return sendMessage('list', {});
let rootSuite: Suite; let rootSuite: Suite;
let loadErrors: TestError[]; const loadErrors: TestError[] = [];
const progress: Progress = { const progress: Progress = {
passed: 0, passed: 0,
failed: 0, failed: 0,
@ -606,12 +606,13 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
}; };
let config: FullConfig; let config: FullConfig;
receiver = new TeleReporterReceiver(pathSeparator, { receiver = new TeleReporterReceiver(pathSeparator, {
onBegin: (c: FullConfig, suite: Suite) => { onConfigure: (c: FullConfig) => {
if (!rootSuite) {
rootSuite = suite;
loadErrors = [];
}
config = c; config = c;
},
onBegin: (suite: Suite) => {
if (!rootSuite)
rootSuite = suite;
progress.passed = 0; progress.passed = 0;
progress.failed = 0; progress.failed = 0;
progress.skipped = 0; progress.skipped = 0;
@ -639,8 +640,18 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
onError: (error: TestError) => { onError: (error: TestError) => {
xtermDataSource.write((error.stack || error.value || '') + '\n'); xtermDataSource.write((error.stack || error.value || '') + '\n');
loadErrors.push(error); loadErrors.push(error);
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress); throttleUpdateRootSuite(config, rootSuite ?? new TeleSuite('', 'root'), loadErrors, progress);
}, },
printsToStdio: () => {
return false;
},
onStdOut: () => {},
onStdErr: () => {},
onExit: () => {},
onStepBegin: () => {},
onStepEnd: () => {},
}, true); }, true);
receiver._setClearPreviousResultsWhenTestBegins(); receiver._setClearPreviousResultsWhenTestBegins();
return sendMessage('list', {}); return sendMessage('list', {});

View file

@ -836,8 +836,8 @@ test('preserve config fields', async ({ runInlineTest, mergeReports }) => {
` `
}; };
await runInlineTest(files, { shard: `1/3` }); await runInlineTest(files, { shard: `1/3`, workers: 1 });
await runInlineTest(files, { shard: `3/3` }); await runInlineTest(files, { shard: `3/3`, workers: 1 });
const mergeConfig = { const mergeConfig = {
reportSlowTests: { reportSlowTests: {