diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts index 9a7f79a17f..a7fd28ec87 100644 --- a/packages/playwright/src/runner/runner.ts +++ b/packages/playwright/src/runner/runner.ts @@ -25,8 +25,6 @@ import { createReporters } from './reporters'; import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks'; import type { FullConfigInternal } from '../common/config'; import { runWatchModeLoop } from './watchMode'; -import { InternalReporter } from '../reporters/internalReporter'; -import { Multiplexer } from '../reporters/multiplexer'; import type { Suite } from '../common/test'; import { wrapReporterAsV2 } from '../reporters/reporterV2'; import { affectedTestFiles } from '../transform/compilationCache'; @@ -79,25 +77,28 @@ export class Runner { // Legacy webServer support. webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); - const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'test', false))); - const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true }) - : createTaskRunner(config, reporter); + const reporters = await createReporters(config, listOnly ? 'list' : 'test', false); + const taskRunner = listOnly ? createTaskRunnerForList( + config, + reporters, + 'in-process', + { failOnLoadErrors: true }) : createTaskRunner(config, reporters); - const testRun = new TestRun(config, reporter); - reporter.onConfigure(config.config); + const testRun = new TestRun(config); + taskRunner.reporter.onConfigure(config.config); const taskStatus = await taskRunner.run(testRun, deadline); let status: FullResult['status'] = testRun.failureTracker.result(); if (status === 'passed' && taskStatus !== 'passed') status = taskStatus; - const modifiedResult = await reporter.onEnd({ status }); + const modifiedResult = await taskRunner.reporter.onEnd({ status }); if (modifiedResult && modifiedResult.status) status = modifiedResult.status; if (!listOnly) await writeLastRunInfo(testRun, status); - await reporter.onExit(); + await taskRunner.reporter.onExit(); // Calling process.exit() might truncate large stdout/stderr output. // See https://github.com/nodejs/node/issues/6456. @@ -110,23 +111,23 @@ export class Runner { async loadAllTests(mode: 'in-process' | 'out-of-process' = 'in-process'): Promise<{ status: FullResult['status'], suite?: Suite, errors: TestError[] }> { const config = this._config; const errors: TestError[] = []; - const reporter = new InternalReporter(new Multiplexer([wrapReporterAsV2({ + const reporters = [wrapReporterAsV2({ onError(error: TestError) { errors.push(error); } - })])); - const taskRunner = createTaskRunnerForList(config, reporter, mode, { failOnLoadErrors: true }); - const testRun = new TestRun(config, reporter); - reporter.onConfigure(config.config); + })]; + const taskRunner = createTaskRunnerForList(config, reporters, mode, { failOnLoadErrors: true }); + const testRun = new TestRun(config); + taskRunner.reporter.onConfigure(config.config); const taskStatus = await taskRunner.run(testRun, 0); let status: FullResult['status'] = testRun.failureTracker.result(); if (status === 'passed' && taskStatus !== 'passed') status = taskStatus; - const modifiedResult = await reporter.onEnd({ status }); + const modifiedResult = await taskRunner.reporter.onEnd({ status }); if (modifiedResult && modifiedResult.status) status = modifiedResult.status; - await reporter.onExit(); + await taskRunner.reporter.onExit(); return { status, suite: testRun.rootSuite, errors }; } diff --git a/packages/playwright/src/runner/taskRunner.ts b/packages/playwright/src/runner/taskRunner.ts index a5508c32be..96505bef2a 100644 --- a/packages/playwright/src/runner/taskRunner.ts +++ b/packages/playwright/src/runner/taskRunner.ts @@ -20,20 +20,26 @@ import type { FullResult, TestError } from '../../types/testReporter'; import { SigIntWatcher } from './sigIntWatcher'; import { serializeError } from '../util'; import type { ReporterV2 } from '../reporters/reporterV2'; +import { InternalReporter } from '../reporters/internalReporter'; +import { Multiplexer } from '../reporters/multiplexer'; -type TaskPhase = (context: Context, errors: TestError[], softErrors: TestError[]) => Promise | void; +type TaskPhase = (reporter: ReporterV2, context: Context, errors: TestError[], softErrors: TestError[]) => Promise | void; export type Task = { setup?: TaskPhase, teardown?: TaskPhase }; export class TaskRunner { private _tasks: { name: string, task: Task }[] = []; - private _reporter: ReporterV2; + readonly reporter: InternalReporter; private _hasErrors = false; private _interrupted = false; private _isTearDown = false; private _globalTimeoutForError: number; - constructor(reporter: ReporterV2, globalTimeoutForError: number) { - this._reporter = reporter; + static create(reporters: ReporterV2[], globalTimeoutForError: number = 0) { + return new TaskRunner(createInternalReporter(reporters), globalTimeoutForError); + } + + private constructor(reporter: InternalReporter, globalTimeoutForError: number) { + this.reporter = reporter; this._globalTimeoutForError = globalTimeoutForError; } @@ -50,7 +56,7 @@ export class TaskRunner { async runDeferCleanup(context: Context, deadline: number, cancelPromise = new ManualPromise()): Promise<{ status: FullResult['status'], cleanup: () => Promise }> { const sigintWatcher = new SigIntWatcher(); const timeoutWatcher = new TimeoutWatcher(deadline); - const teardownRunner = new TaskRunner(this._reporter, this._globalTimeoutForError); + const teardownRunner = new TaskRunner(this.reporter, this._globalTimeoutForError); teardownRunner._isTearDown = true; let currentTaskName: string | undefined; @@ -65,13 +71,13 @@ export class TaskRunner { const softErrors: TestError[] = []; try { teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: { setup: task.teardown } }); - await task.setup?.(context, errors, softErrors); + await task.setup?.(this.reporter, context, errors, softErrors); } catch (e) { debug('pw:test:task')(`error in "${name}": `, e); errors.push(serializeError(e)); } finally { for (const error of [...softErrors, ...errors]) - this._reporter.onError?.(error); + this.reporter.onError?.(error); if (errors.length) { if (!this._isTearDown) this._interrupted = true; @@ -99,7 +105,7 @@ export class TaskRunner { if (sigintWatcher.hadSignal() || cancelPromise?.isDone()) { status = 'interrupted'; } else if (timeoutWatcher.timedOut()) { - this._reporter.onError?.({ message: colors.red(`Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run`) }); + this.reporter.onError?.({ message: colors.red(`Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run`) }); status = 'timedout'; } else if (this._hasErrors) { status = 'failed'; @@ -140,3 +146,7 @@ class TimeoutWatcher { clearTimeout(this._timer); } } + +function createInternalReporter(reporters: ReporterV2[]): InternalReporter { + return new InternalReporter(new Multiplexer(reporters)); +} diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 624b6e1d9f..7abd1f4749 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -47,7 +47,6 @@ export type Phase = { }; export class TestRun { - readonly reporter: ReporterV2; readonly config: FullConfigInternal; readonly failureTracker: FailureTracker; rootSuite: Suite | undefined = undefined; @@ -55,36 +54,35 @@ export class TestRun { projectFiles: Map = new Map(); projectSuites: Map = new Map(); - constructor(config: FullConfigInternal, reporter: ReporterV2) { + constructor(config: FullConfigInternal) { this.config = config; - this.reporter = reporter; this.failureTracker = new FailureTracker(config); } } -export function createTaskRunner(config: FullConfigInternal, reporter: ReporterV2): TaskRunner { - const taskRunner = new TaskRunner(reporter, config.config.globalTimeout); +export function createTaskRunner(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { + const taskRunner = TaskRunner.create(reporters, config.config.globalTimeout); addGlobalSetupTasks(taskRunner, config); taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: true })); addRunTasks(taskRunner, config); return taskRunner; } -export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: ReporterV2): TaskRunner { - const taskRunner = new TaskRunner(reporter, 0); +export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { + const taskRunner = TaskRunner.create(reporters); addGlobalSetupTasks(taskRunner, config); return taskRunner; } -export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: ReporterV2, additionalFileMatcher?: Matcher): TaskRunner { - const taskRunner = new TaskRunner(reporter, 0); +export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner { + const taskRunner = TaskRunner.create(reporters); taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher })); addRunTasks(taskRunner, config); return taskRunner; } -export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: ReporterV2): TaskRunner { - const taskRunner = new TaskRunner(reporter, 0); +export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { + const taskRunner = TaskRunner.create(reporters); taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true })); addRunTasks(taskRunner, config); return taskRunner; @@ -108,15 +106,15 @@ function addRunTasks(taskRunner: TaskRunner, config: FullConfigInternal return taskRunner; } -export function createTaskRunnerForList(config: FullConfigInternal, reporter: ReporterV2, mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner { - const taskRunner = new TaskRunner(reporter, config.config.globalTimeout); +export function createTaskRunnerForList(config: FullConfigInternal, reporters: ReporterV2[], mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner { + const taskRunner = TaskRunner.create(reporters, config.config.globalTimeout); taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false, filterOnlyChanged: false })); taskRunner.addTask('report begin', createReportBeginTask()); return taskRunner; } -export function createTaskRunnerForListFiles(config: FullConfigInternal, reporter: ReporterV2): TaskRunner { - const taskRunner = new TaskRunner(reporter, config.config.globalTimeout); +export function createTaskRunnerForListFiles(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { + const taskRunner = TaskRunner.create(reporters, config.config.globalTimeout); taskRunner.addTask('load tests', createListFilesTask()); taskRunner.addTask('report begin', createReportBeginTask()); return taskRunner; @@ -124,7 +122,7 @@ export function createTaskRunnerForListFiles(config: FullConfigInternal, reporte function createReportBeginTask(): Task { return { - setup: async ({ reporter, rootSuite }) => { + setup: async (reporter, { rootSuite }) => { reporter.onBegin(rootSuite!); }, teardown: async ({}) => {}, @@ -133,7 +131,7 @@ function createReportBeginTask(): Task { function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task { return { - setup: async ({ config, reporter }) => { + setup: async (reporter, { config }) => { if (typeof plugin.factory === 'function') plugin.instance = await plugin.factory(); else @@ -148,7 +146,7 @@ function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task { return { - setup: async ({ rootSuite }) => { + setup: async (reporter, { rootSuite }) => { await plugin.instance?.begin?.(rootSuite!); }, teardown: async () => { @@ -162,13 +160,13 @@ function createGlobalSetupTask(): Task { let globalSetupFinished = false; let teardownHook: any; return { - setup: async ({ config }) => { + setup: async (reporter, { config }) => { const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined; teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined; globalSetupResult = setupHook ? await setupHook(config.config) : undefined; globalSetupFinished = true; }, - teardown: async ({ config }) => { + teardown: async (reporter, { config }) => { if (typeof globalSetupResult === 'function') await globalSetupResult(); if (globalSetupFinished) @@ -179,7 +177,7 @@ function createGlobalSetupTask(): Task { function createRemoveOutputDirsTask(): Task { return { - setup: async ({ config }) => { + setup: async (reporter, { config }) => { const outputDirs = new Set(); const projects = filterProjects(config.projects, config.cliProjectFilter); projects.forEach(p => outputDirs.add(p.project.outputDir)); @@ -203,7 +201,7 @@ function createRemoveOutputDirsTask(): Task { function createListFilesTask(): Task { return { - setup: async (testRun, errors) => { + setup: async (reporter, testRun, errors) => { testRun.rootSuite = await createRootSuite(testRun, errors, false); testRun.failureTracker.onRootSuite(testRun.rootSuite); await collectProjectsAndTestFiles(testRun, false); @@ -226,7 +224,7 @@ function createListFilesTask(): Task { function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, filterOnlyChanged: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task { return { - setup: async (testRun, errors, softErrors) => { + setup: async (reporter, testRun, errors, softErrors) => { await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher); await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors); @@ -257,7 +255,7 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filter function createPhasesTask(): Task { return { - setup: async testRun => { + setup: async (reporter, testRun) => { let maxConcurrentTestGroups = 0; const processed = new Set(); @@ -288,7 +286,7 @@ function createPhasesTask(): Task { processed.add(project); if (phaseProjects.length) { let testGroupsInPhase = 0; - const phase: Phase = { dispatcher: new Dispatcher(testRun.config, testRun.reporter, testRun.failureTracker), projects: [] }; + const phase: Phase = { dispatcher: new Dispatcher(testRun.config, reporter, testRun.failureTracker), projects: [] }; testRun.phases.push(phase); for (const project of phaseProjects) { const projectSuite = projectToSuite.get(project)!; @@ -308,7 +306,7 @@ function createPhasesTask(): Task { function createRunTestsTask(): Task { return { - setup: async ({ phases, failureTracker }) => { + setup: async (reporter, { phases, failureTracker }) => { const successfulProjects = new Set(); const extraEnvByProjectId: EnvByProjectId = new Map(); const teardownToSetups = buildTeardownToSetupsMap(phases.map(phase => phase.projects.map(p => p.project)).flat()); @@ -352,7 +350,7 @@ function createRunTestsTask(): Task { } } }, - teardown: async ({ phases }) => { + teardown: async (reporter, { phases }) => { for (const { dispatcher } of phases.reverse()) await dispatcher.stop(); }, diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index b6c0f2fd72..559f95514a 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -22,12 +22,10 @@ import type { Transport, HttpServer } from 'playwright-core/lib/utils'; import type * as reporterTypes from '../../types/testReporter'; import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache'; import type { ConfigLocation, FullConfigInternal } from '../common/config'; -import { InternalReporter } from '../reporters/internalReporter'; import { createReporterForTestServer, createReporters } from './reporters'; import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer, createTaskRunnerForWatchSetup, createTaskRunnerForListFiles } from './tasks'; import { open } from 'playwright-core/lib/utilsBundle'; import ListReporter from '../reporters/list'; -import { Multiplexer } from '../reporters/multiplexer'; import { SigIntWatcher } from './sigIntWatcher'; import { Watcher } from '../fsWatcher'; import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface'; @@ -40,6 +38,7 @@ import type { TestRunnerPluginRegistration } from '../plugins'; import { serializeError } from '../util'; import { cacheDir } from '../transform/compilationCache'; import { baseFullConfig } from '../isomorphic/teleReceiver'; +import { InternalReporter } from '../reporters/internalReporter'; const originalStdoutWrite = process.stdout.write; const originalStderrWrite = process.stderr.write; @@ -102,9 +101,13 @@ class TestServerDispatcher implements TestServerInterface { private async _collectingReporter() { const report: ReportEntry[] = []; - const wireReporter = await createReporterForTestServer(this._serializer, e => report.push(e)); - const reporter = new InternalReporter(wireReporter); - return { reporter, report }; + const collectingReporter = await createReporterForTestServer(this._serializer, e => report.push(e)); + return { collectingReporter, report }; + } + + private async _collectingInternalReporter() { + const { collectingReporter, report } = await this._collectingReporter(); + return { reporter: new InternalReporter(collectingReporter), report }; } async initialize(params: Parameters[0]): ReturnType { @@ -145,9 +148,9 @@ class TestServerDispatcher implements TestServerInterface { async runGlobalSetup(params: Parameters[0]): ReturnType { await this.runGlobalTeardown(); - const { reporter, report } = await this._collectingReporter(); const { config, error } = await this._loadConfig(); if (!config) { + const { reporter, report } = await this._collectingInternalReporter(); // Produce dummy config when it has an error. reporter.onConfigure(baseFullConfig); reporter.onError(error!); @@ -156,13 +159,14 @@ class TestServerDispatcher implements TestServerInterface { } webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); - const listReporter = new InternalReporter(new ListReporter()); - const taskRunner = createTaskRunnerForWatchSetup(config, new Multiplexer([reporter, listReporter])); - reporter.onConfigure(config.config); - const testRun = new TestRun(config, reporter); + const { collectingReporter, report } = await this._collectingReporter(); + const listReporter = new ListReporter(); + const taskRunner = createTaskRunnerForWatchSetup(config, [collectingReporter, listReporter]); + taskRunner.reporter.onConfigure(config.config); + const testRun = new TestRun(config); const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0); - await reporter.onEnd({ status }); - await reporter.onExit(); + await taskRunner.reporter.onEnd({ status }); + await taskRunner.reporter.onExit(); if (status !== 'passed') { await globalCleanup(); return { report, status }; @@ -181,7 +185,7 @@ class TestServerDispatcher implements TestServerInterface { async startDevServer(params: Parameters[0]): ReturnType { if (this._devServerHandle) return { status: 'failed', report: [] }; - const { reporter, report } = await this._collectingReporter(); + const { reporter, report } = await this._collectingInternalReporter(); const { config, error } = await this._loadConfig(); if (!config) { reporter.onError(error!); @@ -209,7 +213,7 @@ class TestServerDispatcher implements TestServerInterface { this._devServerHandle = undefined; return { status: 'passed', report: [] }; } catch (e) { - const { reporter, report } = await this._collectingReporter(); + const { reporter, report } = await this._collectingInternalReporter(); reporter.onError(serializeError(e)); return { status: 'failed', report }; } @@ -222,20 +226,21 @@ class TestServerDispatcher implements TestServerInterface { } async listFiles(params: Parameters[0]): ReturnType { - const { reporter, report } = await this._collectingReporter(); const { config, error } = await this._loadConfig(); if (!config) { + const { reporter, report } = await this._collectingInternalReporter(); reporter.onError(error!); return { status: 'failed', report }; } + const { collectingReporter, report } = await this._collectingReporter(); config.cliProjectFilter = params.projects?.length ? params.projects : undefined; - const taskRunner = createTaskRunnerForListFiles(config, reporter); - reporter.onConfigure(config.config); - const testRun = new TestRun(config, reporter); + const taskRunner = createTaskRunnerForListFiles(config, [collectingReporter]); + taskRunner.reporter.onConfigure(config.config); + const testRun = new TestRun(config); const status = await taskRunner.run(testRun, 0); - await reporter.onEnd({ status }); - await reporter.onExit(); + await taskRunner.reporter.onEnd({ status }); + await taskRunner.reporter.onExit(); return { report, status }; } @@ -253,11 +258,11 @@ class TestServerDispatcher implements TestServerInterface { repeatEach: 1, retries: 0, }; - const { reporter, report } = await this._collectingReporter(); const { config, error } = await this._loadConfig(overrides); if (!config) { + const { reporter, report } = await this._collectingInternalReporter(); reporter.onError(error!); - return { report: [], status: 'failed' }; + return { report, status: 'failed' }; } config.cliArgs = params.locations || []; @@ -266,12 +271,13 @@ class TestServerDispatcher implements TestServerInterface { config.cliProjectFilter = params.projects?.length ? params.projects : undefined; config.cliListOnly = true; - const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false }); - const testRun = new TestRun(config, reporter); - reporter.onConfigure(config.config); + const { collectingReporter, report } = await this._collectingReporter(); + const taskRunner = createTaskRunnerForList(config, [collectingReporter], 'out-of-process', { failOnLoadErrors: false }); + const testRun = new TestRun(config); + taskRunner.reporter.onConfigure(config.config); const status = await taskRunner.run(testRun, 0); - await reporter.onEnd({ status }); - await reporter.onExit(); + await taskRunner.reporter.onEnd({ status }); + await taskRunner.reporter.onExit(); const projectDirs = new Set(); const projectOutputs = new Set(); @@ -343,14 +349,13 @@ class TestServerDispatcher implements TestServerInterface { const reporters = await createReporters(config, 'test', true); const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e)); reporters.push(wireReporter); - const reporter = new InternalReporter(new Multiplexer(reporters)); - const taskRunner = createTaskRunnerForTestServer(config, reporter); - const testRun = new TestRun(config, reporter); - reporter.onConfigure(config.config); + const taskRunner = createTaskRunnerForTestServer(config, reporters); + const testRun = new TestRun(config); + taskRunner.reporter.onConfigure(config.config); const stop = new ManualPromise(); const run = taskRunner.run(testRun, 0, stop).then(async status => { - await reporter.onEnd({ status }); - await reporter.onExit(); + await taskRunner.reporter.onEnd({ status }); + await taskRunner.reporter.onExit(); this._testRun = undefined; return status; }); diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index 5758bf673e..709e39100b 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -17,7 +17,6 @@ import readline from 'readline'; import { createGuid, getPackageManagerExecCommand, ManualPromise } from 'playwright-core/lib/utils'; import type { FullConfigInternal, FullProjectInternal } from '../common/config'; -import { InternalReporter } from '../reporters/internalReporter'; import { createFileMatcher, createFileMatcherFromArguments } from '../util'; import type { Matcher } from '../util'; import { TestRun, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks'; @@ -112,15 +111,14 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise { diff --git a/tests/playwright-test/assets/simple-server.js b/tests/playwright-test/assets/simple-server.js index e6db0c69c7..3ca8d7a8ad 100644 --- a/tests/playwright-test/assets/simple-server.js +++ b/tests/playwright-test/assets/simple-server.js @@ -1,5 +1,6 @@ const http = require('http'); +console.log('output from server'); console.error('error from server'); const port = process.argv[2] || 3000; diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts index 9e44804dcf..b8a6cfddd1 100644 --- a/tests/playwright-test/ui-mode-test-output.spec.ts +++ b/tests/playwright-test/ui-mode-test-output.spec.ts @@ -15,6 +15,7 @@ */ import { test, expect, retries } from './ui-mode-fixtures'; +import path from 'path'; test.describe.configure({ mode: 'parallel', retries }); @@ -202,3 +203,29 @@ test('should print beforeAll console messages once', async ({ runUITest }, testI 'test log', ]); }); + +test('should print web server output', async ({ runUITest }, { workerIndex }) => { + const port = workerIndex * 2 + 10500; + const serverPath = path.join(__dirname, 'assets', 'simple-server.js'); + const { page } = await runUITest({ + 'test.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('connect to the server', async ({baseURL, page}) => { + expect(baseURL).toBe('http://localhost:${port}'); + }); + `, + 'playwright.config.ts': ` + module.exports = { + webServer: { + command: 'node ${JSON.stringify(serverPath)} ${port}', + port: ${port}, + stdout: 'pipe', + stderr: 'pipe', + } + }; + `, + }); + await page.getByTitle('Toggle output').click(); + await expect(page.getByTestId('output')).toContainText('output from server'); + await expect(page.getByTestId('output')).toContainText('error from server'); +});