diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts index 70c20c045e..59ba309b2e 100644 --- a/packages/playwright/src/common/configLoader.ts +++ b/packages/playwright/src/common/configLoader.ts @@ -93,7 +93,7 @@ export async function deserializeConfig(data: SerializedConfig): Promise { +async function loadUserConfig(location: ConfigLocation): Promise { let object = location.resolvedConfigFile ? await requireOrImport(location.resolvedConfigFile) : {}; if (object && typeof object === 'object' && ('default' in object)) object = object['default']; @@ -333,12 +333,12 @@ export async function loadEmptyConfigForMergeReports() { return await loadConfig({ configDir: process.cwd() }); } -export function restartWithExperimentalTsEsm(configFile: string | undefined): boolean { +export function restartWithExperimentalTsEsm(configFile: string | undefined, force: boolean = false): boolean { const nodeVersion = +process.versions.node.split('.')[0]; // New experimental loader is only supported on Node 16+. if (nodeVersion < 16) return false; - if (!configFile) + if (!configFile && !force) return false; if (process.env.PW_DISABLE_TS_ESM) return false; @@ -348,9 +348,10 @@ export function restartWithExperimentalTsEsm(configFile: string | undefined): bo process.execArgv = execArgvWithoutExperimentalLoaderOptions(); return false; } - if (!fileIsModule(configFile)) + if (!force && !fileIsModule(configFile!)) return false; - // Node.js < 20 + + // Node.js < 20 if (!require('node:module').register) { const innerProcess = (require('child_process') as typeof import('child_process')).fork(require.resolve('../../cli'), process.argv.slice(2), { env: { diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index ccd0ec9a1d..5d2322c783 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -108,9 +108,8 @@ function addFindRelatedTestFilesCommand(program: Command) { function addTestServerCommand(program: Command) { const command = program.command('test-server', { hidden: true }); command.description('start test server'); - command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); - command.action(options => { - void runTestServer(options.config); + command.action(() => { + void runTestServer(); }); } diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index e07fc1b9f1..6f03c640eb 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -19,8 +19,8 @@ import path from 'path'; import { ManualPromise, createGuid, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils'; import { WSServer } from 'playwright-core/lib/utils'; import type { WebSocket } from 'playwright-core/lib/utilsBundle'; -import type { FullResult } from 'playwright/types/testReporter'; -import { loadConfig, resolveConfigFile, restartWithExperimentalTsEsm } from '../common/configLoader'; +import type { FullResult, TestError } from 'playwright/types/testReporter'; +import { loadConfig, restartWithExperimentalTsEsm } from '../common/configLoader'; import { InternalReporter } from '../reporters/internalReporter'; import { Multiplexer } from '../reporters/multiplexer'; import { createReporters } from './reporters'; @@ -28,33 +28,15 @@ import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer } from import type { ConfigCLIOverrides } from '../common/ipc'; import { Runner } from './runner'; import type { FindRelatedTestFilesReport } from './runner'; -import type { ConfigLocation } from '../common/config'; +import type { FullConfigInternal } from '../common/config'; -type PlaywrightTestOptions = { - headed?: boolean, - oneWorker?: boolean, - trace?: 'on' | 'off', - projects?: string[]; - grep?: string; - reuseContext?: boolean, - connectWsEndpoint?: string; -}; - -export async function runTestServer(configFile: string | undefined) { - process.env.PW_TEST_HTML_REPORT_OPEN = 'never'; - - const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd(); - const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); - if (restartWithExperimentalTsEsm(resolvedConfigFile)) +export async function runTestServer() { + if (restartWithExperimentalTsEsm(undefined, true)) return null; - const configPaths: ConfigLocation = { - resolvedConfigFile, - configDir: resolvedConfigFile ? path.dirname(resolvedConfigFile) : configFileOrDirectory - }; - + process.env.PW_TEST_HTML_REPORT_OPEN = 'never'; const wss = new WSServer({ onConnection(request: http.IncomingMessage, url: URL, ws: WebSocket, id: string) { - const dispatcher = new Dispatcher(configPaths, ws); + const dispatcher = new Dispatcher(ws); ws.on('message', async message => { const { id, method, params } = JSON.parse(message.toString()); try { @@ -78,13 +60,49 @@ export async function runTestServer(configFile: string | undefined) { process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0)); } -class Dispatcher { +export interface TestServerInterface { + list(params: { + configFile: string; + locations: string[]; + reporter: string; + env: NodeJS.ProcessEnv; + }): Promise; + + test(params: { + configFile: string; + locations: string[]; + reporter: string; + env: NodeJS.ProcessEnv; + headed?: boolean; + oneWorker?: boolean; + trace?: 'on' | 'off'; + projects?: string[]; + grep?: string; + reuseContext?: boolean; + connectWsEndpoint?: string; + }): Promise; + + findRelatedTestFiles(params: { + configFile: string; + files: string[]; + }): Promise<{ testFiles: string[]; errors?: TestError[]; }>; + + stop(params: { + configFile: string; + }): Promise; + + closeGracefully(): Promise; +} + +export interface TestServerEvents { + on(event: 'stdio', listener: (params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }) => void): void; +} + +class Dispatcher implements TestServerInterface { private _testRun: { run: Promise, stop: ManualPromise } | undefined; private _ws: WebSocket; - private _configLocation: ConfigLocation; - constructor(configLocation: ConfigLocation, ws: WebSocket) { - this._configLocation = configLocation; + constructor(ws: WebSocket) { this._ws = ws; process.stdout.write = ((chunk: string | Buffer, cb?: Buffer | Function, cb2?: Function) => { @@ -105,32 +123,16 @@ class Dispatcher { }) as any; } - async list(params: { locations: string[], reporter: string, env: NodeJS.ProcessEnv }) { - for (const name in params.env) - process.env[name] = params.env[name]; - 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 findRelatedTestFiles(params: { files: string[] }): Promise { - const config = await this._loadConfig({}); - const runner = new Runner(config); - return runner.findRelatedTestFiles('out-of-process', params.files); - } - - async stop() { - await this._stopTests(); - } - - private async _listTests(reporterPath: string, locations: string[] | undefined) { - const config = await this._loadConfig({}); - config.cliArgs = [...(locations || []), '--reporter=null']; - const reporter = new InternalReporter(new Multiplexer(await createReporters(config, 'list', [[reporterPath]]))); + async list(params: { + configFile: string; + locations: string[]; + reporter: string; + env: NodeJS.ProcessEnv; + }) { + this._syncEnv(params.env); + const config = await this._loadConfig(params.configFile); + config.cliArgs = params.locations || []; + const reporter = new InternalReporter(new Multiplexer(await createReporters(config, 'list', [[params.reporter]]))); const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: true }); const testRun = new TestRun(config, reporter); reporter.onConfigure(config.config); @@ -145,27 +147,41 @@ class Dispatcher { await reporter.onExit(); } - private async _runTests(reporterPath: string, locations: string[] | undefined, options: PlaywrightTestOptions) { + async test(params: { + configFile: string; + locations: string[]; + reporter: string; + env: NodeJS.ProcessEnv; + headed?: boolean; + oneWorker?: boolean; + trace?: 'on' | 'off'; + projects?: string[]; + grep?: string; + reuseContext?: boolean; + connectWsEndpoint?: string; + }) { + this._syncEnv(params.env); await this._stopTests(); + const overrides: ConfigCLIOverrides = { - additionalReporters: [[reporterPath]], + additionalReporters: [[params.reporter]], repeatEach: 1, retries: 0, preserveOutputDir: true, use: { - trace: options.trace, - headless: options.headed ? false : undefined, - _optionContextReuseMode: options.reuseContext ? 'when-possible' : undefined, - _optionConnectOptions: options.connectWsEndpoint ? { wsEndpoint: options.connectWsEndpoint } : undefined, + trace: params.trace, + headless: params.headed ? false : undefined, + _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, + _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, }, - workers: options.oneWorker ? 1 : undefined, + workers: params.oneWorker ? 1 : undefined, }; - const config = await this._loadConfig(overrides); + const config = await this._loadConfig(params.configFile, overrides); config.cliListOnly = false; - config.cliArgs = locations || []; - config.cliGrep = options.grep; - config.cliProjectFilter = options.projects?.length ? options.projects : undefined; + config.cliArgs = params.locations || []; + config.cliGrep = params.grep; + config.cliProjectFilter = params.projects?.length ? params.projects : undefined; const reporter = new InternalReporter(new Multiplexer(await createReporters(config, 'run'))); const taskRunner = createTaskRunnerForTestServer(config, reporter); @@ -182,18 +198,42 @@ class Dispatcher { await run; } + async findRelatedTestFiles(params: { + configFile: string; + files: string[]; + }): Promise { + const config = await this._loadConfig(params.configFile); + const runner = new Runner(config); + return runner.findRelatedTestFiles('out-of-process', params.files); + } + + async stop(params: { + configFile: string; + }) { + await this._stopTests(); + } + + async closeGracefully() { + gracefullyProcessExitDoNotHang(0); + } + private async _stopTests() { this._testRun?.stop?.resolve(); await this._testRun?.run; } - private async _loadConfig(overrides: ConfigCLIOverrides) { - return await loadConfig(this._configLocation, overrides); - } - private _dispatchEvent(method: string, params: any) { this._ws.send(JSON.stringify({ method, params })); } + + private async _loadConfig(configFile: string, overrides?: ConfigCLIOverrides): Promise { + return loadConfig({ resolvedConfigFile: configFile, configDir: path.dirname(configFile) }, overrides); + } + + private _syncEnv(env: NodeJS.ProcessEnv) { + for (const name in env) + process.env[name] = env[name]; + } } function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string) {