diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 1bf4acfc37..1cd57034e4 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -83,6 +83,14 @@ export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: R return taskRunner; } +export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: ReporterV2): TaskRunner { + const taskRunner = new TaskRunner(reporter, 0); + addGlobalSetupTasks(taskRunner, config); + taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunTestsOutsideProjectFilter: true })); + addRunTasks(taskRunner, config); + return taskRunner; +} + function addGlobalSetupTasks(taskRunner: TaskRunner, config: FullConfigInternal) { if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) taskRunner.addTask('clear output', createRemoveOutputDirsTask()); diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 6fcdd18f6d..27a9c7afb0 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -24,7 +24,7 @@ import { loadConfigFromFile } from '../common/configLoader'; import { InternalReporter } from '../reporters/internalReporter'; import { Multiplexer } from '../reporters/multiplexer'; import { createReporters } from './reporters'; -import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch } from './tasks'; +import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer } from './tasks'; type PlaywrightTestOptions = { headed?: boolean, @@ -44,13 +44,18 @@ export async function runTestServer(configFile: string) { if (!config) return; - const dispatcher = new Dispatcher(config); const wss = new WSServer({ onConnection(request: http.IncomingMessage, url: URL, ws: WebSocket, id: string) { + const dispatcher = new Dispatcher(config, ws); ws.on('message', async message => { const { id, method, params } = JSON.parse(message.toString()); - const result = await (dispatcher as any)[method](params); - ws.send(JSON.stringify({ id, result })); + try { + const result = await (dispatcher as any)[method](params); + ws.send(JSON.stringify({ id, result })); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } }); return { async close() {} @@ -64,62 +69,111 @@ export async function runTestServer(configFile: string) { class Dispatcher { private _config: FullConfigInternal; + private _testRun: { run: Promise, stop: ManualPromise } | undefined; + private _ws: WebSocket; - constructor(config: FullConfigInternal) { + constructor(config: FullConfigInternal, ws: WebSocket) { this._config = config; + this._ws = ws; + + process.stdout.write = ((chunk: string | Buffer, cb?: Buffer | Function, cb2?: Function) => { + this._dispatchEvent('stdio', chunkToPayload('stdout', chunk)); + if (typeof cb === 'function') + (cb as any)(); + if (typeof cb2 === 'function') + (cb2 as any)(); + return true; + }) as any; + process.stderr.write = ((chunk: string | Buffer, cb?: Buffer | Function, cb2?: Function) => { + this._dispatchEvent('stdio', chunkToPayload('stderr', chunk)); + if (typeof cb === 'function') + (cb as any)(); + if (typeof cb2 === 'function') + (cb2 as any)(); + return true; + }) as any; } - async test(params: { mode: 'list' | 'run', locations: string[], options: PlaywrightTestOptions, reporter: string, env: NodeJS.ProcessEnv }) { + async list(params: { locations: string[], reporter: string, env: NodeJS.ProcessEnv }) { for (const name in params.env) process.env[name] = params.env[name]; - if (params.mode === 'list') - await listTests(this._config, params.reporter, params.locations); - if (params.mode === 'run') - await runTests(this._config, params.reporter, params.locations, params.options); + await this._listTests(params.reporter, params.locations); + } + + async test(params: { locations: string[], options: PlaywrightTestOptions, reporter: string, env: NodeJS.ProcessEnv }) { + for (const name in params.env) + process.env[name] = params.env[name]; + await this._runTests(params.reporter, params.locations, params.options); + } + + async stop() { + await this._stopTests(); + } + + private async _listTests(reporterPath: string, locations: string[] | undefined) { + this._config.cliArgs = [...(locations || []), '--reporter=null']; + const reporter = new InternalReporter(new Multiplexer(await createReporters(this._config, 'list', [[reporterPath]]))); + const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: true }); + const testRun = new TestRun(this._config, reporter); + reporter.onConfigure(this._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 }); + if (modifiedResult && modifiedResult.status) + status = modifiedResult.status; + await reporter.onExit(); + } + + private async _runTests(reporterPath: string, locations: string[] | undefined, options: PlaywrightTestOptions) { + await this._stopTests(); + this._config.cliListOnly = false; + this._config.cliArgs = locations || []; + this._config.cliGrep = options.grep; + this._config.cliProjectFilter = options.projects?.length ? options.projects : undefined; + this._config.configCLIOverrides.reporter = [[reporterPath]]; + this._config.configCLIOverrides.repeatEach = 1; + this._config.configCLIOverrides.retries = 0; + this._config.configCLIOverrides.preserveOutputDir = true; + this._config.configCLIOverrides.use = { + trace: options.trace, + headless: options.headed ? false : undefined, + _optionContextReuseMode: options.reuseContext ? 'when-possible' : undefined, + _optionConnectOptions: options.connectWsEndpoint ? { wsEndpoint: options.connectWsEndpoint } : undefined, + }; + // Too late to adjust via overrides for this one. + if (options.oneWorker) + this._config.config.workers = 1; + + const reporter = new InternalReporter(new Multiplexer(await createReporters(this._config, 'run'))); + const taskRunner = createTaskRunnerForTestServer(this._config, reporter); + const testRun = new TestRun(this._config, reporter); + reporter.onConfigure(this._config.config); + const stop = new ManualPromise(); + const run = taskRunner.run(testRun, 0, stop).then(async status => { + await reporter.onEnd({ status }); + await reporter.onExit(); + this._testRun = undefined; + return status; + }); + this._testRun = { run, stop }; + await run; + } + + private async _stopTests() { + this._testRun?.stop?.resolve(); + await this._testRun?.run; + } + + private _dispatchEvent(method: string, params: any) { + this._ws.send(JSON.stringify({ method, params })); } } - -async function listTests(config: FullConfigInternal, reporterPath: string, locations: string[] | undefined) { - config.cliArgs = [...(locations || []), '--reporter=null']; - const reporter = new InternalReporter(new Multiplexer(await createReporters(config, 'list', [[reporterPath]]))); - const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false }); - const testRun = new TestRun(config, reporter); - 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 }); - if (modifiedResult && modifiedResult.status) - status = modifiedResult.status; - await reporter.onExit(); -} - -async function runTests(config: FullConfigInternal, reporterPath: string, locations: string[] | undefined, options: PlaywrightTestOptions) { - config.cliArgs = locations || []; - config.cliGrep = options.grep; - config.cliProjectFilter = options.projects; - - config.configCLIOverrides.reporter = [[reporterPath]]; - config.configCLIOverrides.repeatEach = 1; - config.configCLIOverrides.retries = 0; - config.configCLIOverrides.workers = options.oneWorker ? 1 : undefined; - config.configCLIOverrides.preserveOutputDir = true; - config.configCLIOverrides.use = { - trace: options.trace, - headless: options.headed ? false : undefined, - _optionContextReuseMode: options.reuseContext ? 'when-possible' : undefined, - _optionConnectOptions: options.connectWsEndpoint ? { wsEndpoint: options.connectWsEndpoint } : undefined, - }; - - const reporter = new InternalReporter(new Multiplexer(await createReporters(config, 'run'))); - const taskRunner = createTaskRunnerForWatch(config, reporter); - const testRun = new TestRun(config, reporter); - reporter.onConfigure(config.config); - const stop = new ManualPromise(); - const status = await taskRunner.run(testRun, 0, stop); - await reporter.onEnd({ status }); - await reporter.onExit(); +function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string) { + if (chunk instanceof Buffer) + return { type, buffer: chunk.toString('base64') }; + return { type, text: chunk }; }