diff --git a/docs/src/test-reporter-api/class-reporter.md b/docs/src/test-reporter-api/class-reporter.md index a5e95710df..72203157d1 100644 --- a/docs/src/test-reporter-api/class-reporter.md +++ b/docs/src/test-reporter-api/class-reporter.md @@ -81,6 +81,7 @@ Here is a typical order of reporter calls: * [`method: Reporter.onStepBegin`] and [`method: Reporter.onStepEnd`] are called for each executed step inside the test. When steps are executed, test run has not finished yet. * [`method: Reporter.onTestEnd`] is called when test run has finished. By this time, [TestResult] is complete and you can use [`property: TestResult.status`], [`property: TestResult.error`] and more. * [`method: Reporter.onEnd`] is called once after all tests that should run had finished. +* [`method: Reporter.onExit`] is called immediately before the test runner exits. Additionally, [`method: Reporter.onStdOut`] and [`method: Reporter.onStdErr`] are called when standard output is produced in the worker process, possibly during a test execution, and [`method: Reporter.onError`] is called when something went wrong outside of the test execution. @@ -131,6 +132,12 @@ Called on some global error, for example unhandled exception in the worker proce The error. +## optional async method: Reporter.onExit +* since: v1.33 + +Called immediately before test runner exists. At this point all the reporters +have recived the [`method: Reporter.onEnd`] signal, so all the reports should +be build. You can run the code that uploads the reports in this hook. ## optional method: Reporter.onStdErr * since: v1.10 diff --git a/packages/playwright-test/src/plugins/index.ts b/packages/playwright-test/src/plugins/index.ts index 4851668307..1a5a663264 100644 --- a/packages/playwright-test/src/plugins/index.ts +++ b/packages/playwright-test/src/plugins/index.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import type { Suite, Reporter } from '../../types/testReporter'; +import type { Suite } from '../../types/testReporter'; import type { FullConfig } from '../common/types'; +import type { Multiplexer } from '../reporters/multiplexer'; export interface TestRunnerPlugin { name: string; - setup?(config: FullConfig, configDir: string, reporter: Reporter): Promise; + setup?(config: FullConfig, configDir: string, reporter: Multiplexer): Promise; begin?(suite: Suite): Promise; end?(): Promise; teardown?(): Promise; diff --git a/packages/playwright-test/src/plugins/webServerPlugin.ts b/packages/playwright-test/src/plugins/webServerPlugin.ts index 8ca2dccc13..ab7fe766a2 100644 --- a/packages/playwright-test/src/plugins/webServerPlugin.ts +++ b/packages/playwright-test/src/plugins/webServerPlugin.ts @@ -19,10 +19,11 @@ import net from 'net'; import { debug } from 'playwright-core/lib/utilsBundle'; import { raceAgainstTimeout, launchProcess, httpRequest } from 'playwright-core/lib/utils'; -import type { FullConfig, Reporter } from '../../types/testReporter'; +import type { FullConfig } from '../../types/testReporter'; import type { TestRunnerPlugin } from '.'; import type { FullConfigInternal } from '../common/types'; import { envWithoutExperimentalLoaderOptions } from '../util'; +import type { Multiplexer } from '../reporters/multiplexer'; export type WebServerPluginOptions = { @@ -47,7 +48,7 @@ export class WebServerPlugin implements TestRunnerPlugin { private _processExitedPromise!: Promise; private _options: WebServerPluginOptions; private _checkPortOnly: boolean; - private _reporter?: Reporter; + private _reporter?: Multiplexer; name = 'playwright:webserver'; constructor(options: WebServerPluginOptions, checkPortOnly: boolean) { @@ -55,7 +56,7 @@ export class WebServerPlugin implements TestRunnerPlugin { this._checkPortOnly = checkPortOnly; } - public async setup(config: FullConfig, configDir: string, reporter: Reporter) { + public async setup(config: FullConfig, configDir: string, reporter: Multiplexer) { this._reporter = 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; @@ -146,7 +147,7 @@ async function isPortUsed(port: number): Promise { return await innerIsPortUsed('127.0.0.1') || await innerIsPortUsed('::1'); } -async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Reporter['onStdErr']) { +async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Multiplexer['onStdErr']) { let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onStdErr); if (statusCode === 404 && url.pathname === '/') { const indexUrl = new URL(url); @@ -156,7 +157,7 @@ async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Re return statusCode >= 200 && statusCode < 404; } -async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Reporter['onStdErr']): Promise { +async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Multiplexer['onStdErr']): Promise { return new Promise(resolve => { debugWebServer(`HTTP GET: ${url}`); httpRequest({ @@ -189,7 +190,7 @@ async function waitFor(waitFn: () => Promise, cancellationToken: { canc } } -function getIsAvailableFunction(url: string, checkPortOnly: boolean, ignoreHTTPSErrors: boolean, onStdErr: Reporter['onStdErr']) { +function getIsAvailableFunction(url: string, checkPortOnly: boolean, ignoreHTTPSErrors: boolean, onStdErr: Multiplexer['onStdErr']) { const urlObject = new URL(url); if (!checkPortOnly) return () => isURLAvailable(urlObject, ignoreHTTPSErrors, onStdErr); diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index 11f1491480..ffc617b9b6 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -112,7 +112,7 @@ class HtmlReporter implements Reporter { this._buildResult = await builder.build({ ...this.config.metadata, duration }, reports); } - async _onExit() { + async onExit() { if (process.env.CI || !this._buildResult) return; diff --git a/packages/playwright-test/src/reporters/multiplexer.ts b/packages/playwright-test/src/reporters/multiplexer.ts index 8e4df71692..dab2d80dc8 100644 --- a/packages/playwright-test/src/reporters/multiplexer.ts +++ b/packages/playwright-test/src/reporters/multiplexer.ts @@ -24,7 +24,7 @@ type StdIOChunk = { result?: TestResult; }; -export class Multiplexer implements Reporter { +export class Multiplexer { private _reporters: Reporter[]; private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = []; private _config!: FullConfig; @@ -99,7 +99,7 @@ export class Multiplexer implements Reporter { await Promise.resolve().then(() => reporter.onEnd?.(result)).catch(e => console.error('Error in reporter', e)); for (const reporter of this._reporters) - await Promise.resolve().then(() => (reporter as any)._onExit?.()).catch(e => console.error('Error in reporter', e)); + await Promise.resolve().then(() => reporter.onExit?.()).catch(e => console.error('Error in reporter', e)); } onError(error: TestError) { diff --git a/packages/playwright-test/src/runner/dispatcher.ts b/packages/playwright-test/src/runner/dispatcher.ts index 36c122f491..1b6f65de8d 100644 --- a/packages/playwright-test/src/runner/dispatcher.ts +++ b/packages/playwright-test/src/runner/dispatcher.ts @@ -16,7 +16,7 @@ import type { TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, RunPayload, SerializedConfig } from '../common/ipc'; import { serializeConfig } from '../common/ipc'; -import type { TestResult, Reporter, TestStep, TestError } from '../../types/testReporter'; +import type { TestResult, TestStep, TestError } from '../../types/testReporter'; import type { Suite } from '../common/test'; import type { ProcessExitData } from './processHost'; import type { TestCase } from '../common/test'; @@ -24,6 +24,7 @@ import { ManualPromise } from 'playwright-core/lib/utils'; import { WorkerHost } from './workerHost'; import type { TestGroup } from './testGroups'; import type { FullConfigInternal } from '../common/types'; +import type { Multiplexer } from '../reporters/multiplexer'; type TestResultData = { result: TestResult; @@ -45,14 +46,14 @@ export class Dispatcher { private _testById = new Map(); private _config: FullConfigInternal; - private _reporter: Reporter; + private _reporter: Multiplexer; private _hasWorkerErrors = false; private _failureCount = 0; private _extraEnvByProjectId: EnvByProjectId = new Map(); private _producedEnvByProjectId: EnvByProjectId = new Map(); - constructor(config: FullConfigInternal, reporter: Reporter) { + constructor(config: FullConfigInternal, reporter: Multiplexer) { this._config = config; this._reporter = reporter; } diff --git a/packages/playwright-test/src/runner/taskRunner.ts b/packages/playwright-test/src/runner/taskRunner.ts index fc077a4743..9b1de708a5 100644 --- a/packages/playwright-test/src/runner/taskRunner.ts +++ b/packages/playwright-test/src/runner/taskRunner.ts @@ -16,22 +16,23 @@ import { debug } from 'playwright-core/lib/utilsBundle'; import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils'; -import type { FullResult, Reporter, TestError } from '../../reporter'; +import type { FullResult, TestError } from '../../reporter'; import { SigIntWatcher } from './sigIntWatcher'; import { serializeError } from '../util'; +import type { Multiplexer } from '../reporters/multiplexer'; type TaskTeardown = () => Promise | undefined; export type Task = (context: Context, errors: TestError[]) => Promise | undefined; export class TaskRunner { private _tasks: { name: string, task: Task }[] = []; - private _reporter: Reporter; + private _reporter: Multiplexer; private _hasErrors = false; private _interrupted = false; private _isTearDown = false; private _globalTimeoutForError: number; - constructor(reporter: Reporter, globalTimeoutForError: number) { + constructor(reporter: Multiplexer, globalTimeoutForError: number) { this._reporter = reporter; this._globalTimeoutForError = globalTimeoutForError; } diff --git a/packages/playwright-test/types/testReporter.d.ts b/packages/playwright-test/types/testReporter.d.ts index 27cd9963bb..ffd9d15ea2 100644 --- a/packages/playwright-test/types/testReporter.d.ts +++ b/packages/playwright-test/types/testReporter.d.ts @@ -372,6 +372,8 @@ export interface FullResult { * [testResult.error](https://playwright.dev/docs/api/class-testresult#test-result-error) and more. * - [reporter.onEnd(result)](https://playwright.dev/docs/api/class-reporter#reporter-on-end) is called once after * all tests that should run had finished. + * - [reporter.onExit()](https://playwright.dev/docs/api/class-reporter#reporter-on-exit) is called immediately + * before the test runner exits. * * Additionally, * [reporter.onStdOut(chunk, test, result)](https://playwright.dev/docs/api/class-reporter#reporter-on-std-out) and @@ -410,6 +412,13 @@ export interface Reporter { */ onError?(error: TestError): void; + /** + * Called immediately before test runner exists. At this point all the reporters have recived the + * [reporter.onEnd(result)](https://playwright.dev/docs/api/class-reporter#reporter-on-end) signal, so all the reports + * should be build. You can run the code that uploads the reports in this hook. + */ + onExit?(): Promise; + /** * Called when something has been written to the standard error in the worker process. * @param chunk Output chunk. diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 0b1f54dafd..cd42edf223 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -33,6 +33,9 @@ class Reporter { onEnd() { console.log('\\n%%end'); } + onExit() { + console.log('\\n%%exit'); + } } module.exports = Reporter; `; @@ -176,6 +179,7 @@ test('should work without a file extension', async ({ runInlineTest }) => { expect(result.outputLines).toEqual([ 'begin', 'end', + 'exit', ]); }); @@ -205,6 +209,7 @@ test('should report onEnd after global teardown', async ({ runInlineTest }) => { 'begin', 'global teardown', 'end', + 'exit', ]); }); @@ -227,6 +232,7 @@ test('should load reporter from node_modules', async ({ runInlineTest }) => { expect(result.outputLines).toEqual([ 'begin', 'end', + 'exit', ]); });