From d573c515a315362072d1c5f8298c45d3164de69a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 20 Feb 2024 09:56:33 -0800 Subject: [PATCH] chore: add test server stub (#29568) --- packages/playwright-core/src/utils/index.ts | 1 + packages/playwright-ct-core/src/mount.ts | 4 +- packages/playwright/package.json | 1 + packages/playwright/src/common/config.ts | 2 + packages/playwright/src/common/ipc.ts | 1 + packages/playwright/src/index.ts | 26 ++-- packages/playwright/src/program.ts | 11 ++ packages/playwright/src/runner/tasks.ts | 5 +- packages/playwright/src/runner/testServer.ts | 125 +++++++++++++++++++ packages/playwright/src/runner/watchMode.ts | 13 +- 10 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 packages/playwright/src/runner/testServer.ts diff --git a/packages/playwright-core/src/utils/index.ts b/packages/playwright-core/src/utils/index.ts index 16642ae042..a5219cda6c 100644 --- a/packages/playwright-core/src/utils/index.ts +++ b/packages/playwright-core/src/utils/index.ts @@ -40,6 +40,7 @@ export * from './time'; export * from './timeoutRunner'; export * from './traceUtils'; export * from './userAgent'; +export * from './wsServer'; export * from './zipFile'; export * from './zones'; export * from './isomorphic/locatorGenerators'; diff --git a/packages/playwright-ct-core/src/mount.ts b/packages/playwright-ct-core/src/mount.ts index e8ee9272ac..7220916a4b 100644 --- a/packages/playwright-ct-core/src/mount.ts +++ b/packages/playwright-ct-core/src/mount.ts @@ -33,12 +33,12 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & { type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _ctWorker: { context: BrowserContext | undefined, hash: string } }; type BaseTestFixtures = { _contextFactory: (options?: BrowserContextOptions) => Promise, - _contextReuseMode: ContextReuseMode + _optionContextReuseMode: ContextReuseMode }; export const fixtures: Fixtures = { - _contextReuseMode: 'when-possible', + _optionContextReuseMode: 'when-possible', serviceWorkers: 'block', diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 02e509b036..39e4cf23ec 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -25,6 +25,7 @@ "./lib/transform/babelBundle": "./lib/transform/babelBundle.js", "./lib/transform/compilationCache": "./lib/transform/compilationCache.js", "./lib/runner/runner": "./lib/runner/runner.js", + "./lib/runner/testServer": "./lib/runner/testServer.js", "./lib/transform/esmLoader": "./lib/transform/esmLoader.js", "./lib/transform/transform": "./lib/transform/transform.js", "./lib/internalsForTest": "./lib/internalsForTest.js", diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 5aa4a9abe0..32ad9e586f 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -40,6 +40,7 @@ export class FullConfigInternal { readonly configDir: string; readonly configCLIOverrides: ConfigCLIOverrides; readonly ignoreSnapshots: boolean; + readonly preserveOutputDir: boolean; readonly webServers: Exclude[]; readonly plugins: TestRunnerPluginRegistration[]; readonly projects: FullProjectInternal[] = []; @@ -68,6 +69,7 @@ export class FullConfigInternal { this.configDir = configDir; this.configCLIOverrides = configCLIOverrides; this.globalOutputDir = takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, config.outputDir), throwawayArtifactsPath, path.resolve(process.cwd())); + this.preserveOutputDir = configCLIOverrides.preserveOutputDir || false; this.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots, false); const privateConfiguration = (config as any)['@playwright/test']; this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index 5656878abf..0ea9158571 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -25,6 +25,7 @@ export type ConfigCLIOverrides = { globalTimeout?: number; maxFailures?: number; outputDir?: string; + preserveOutputDir?: boolean; quiet?: boolean; repeatEach?: number; retries?: number; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 04d95491ef..78314547f6 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -44,14 +44,16 @@ if ((process as any)['__pw_initiator__']) { type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & { _combinedContextOptions: BrowserContextOptions, - _contextReuseMode: ContextReuseMode, - _reuseContext: boolean, _setupContextOptions: void; _setupArtifacts: void; _contextFactory: (options?: BrowserContextOptions) => Promise; }; + type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _browserOptions: LaunchOptions; + _optionContextReuseMode: ContextReuseMode, + _optionConnectOptions: PlaywrightWorkerOptions['connectOptions'], + _reuseContext: boolean, }; const playwrightFixtures: Fixtures = ({ @@ -63,8 +65,8 @@ const playwrightFixtures: Fixtures = ({ headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }], channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }], launchOptions: [{}, { scope: 'worker', option: true }], - connectOptions: [async ({}, use) => { - await use(connectOptionsFromEnv()); + connectOptions: [async ({ _optionConnectOptions }, use) => { + await use(connectOptionsFromEnv() || _optionConnectOptions); }, { scope: 'worker', option: true }], screenshot: ['off', { scope: 'worker', option: true }], video: ['off', { scope: 'worker', option: true }], @@ -88,7 +90,7 @@ const playwrightFixtures: Fixtures = ({ (browserType as any)._defaultLaunchOptions = undefined; }, { scope: 'worker', auto: true }], - browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use, testInfo) => { + browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => { if (!['chromium', 'firefox', 'webkit'].includes(browserName)) throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); @@ -97,7 +99,7 @@ const playwrightFixtures: Fixtures = ({ ...connectOptions, exposeNetwork: connectOptions.exposeNetwork ?? (connectOptions as any)._exposeNetwork, headers: { - ...(process.env.PW_TEST_REUSE_CONTEXT ? { 'x-playwright-reuse-context': '1' } : {}), + ...(_reuseContext ? { 'x-playwright-reuse-context': '1' } : {}), // HTTP headers are ASCII only (not UTF-8). 'x-playwright-launch-options': jsonStringifyForceASCII(_browserOptions), ...connectOptions.headers, @@ -348,12 +350,16 @@ const playwrightFixtures: Fixtures = ({ }, { scope: 'test', _title: 'context' } as any], - _contextReuseMode: process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none'), + _optionContextReuseMode: ['none', { scope: 'worker' }], + _optionConnectOptions: [undefined, { scope: 'worker' }], - _reuseContext: [async ({ video, _contextReuseMode }, use, testInfo) => { - const reuse = _contextReuseMode === 'force' || (_contextReuseMode === 'when-possible' && !shouldCaptureVideo(normalizeVideoMode(video), testInfo)); + _reuseContext: [async ({ video, _optionContextReuseMode }, use) => { + let mode = _optionContextReuseMode; + if (process.env.PW_TEST_REUSE_CONTEXT) + mode = process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none'); + const reuse = mode === 'force' || (mode === 'when-possible' && normalizeVideoMode(video) === 'off'); await use(reuse); - }, { scope: 'test', _title: 'context' } as any], + }, { scope: 'worker', _title: 'context' } as any], context: async ({ playwright, browser, _reuseContext, _contextFactory }, use, testInfo) => { attachConnectedHeaderIfNeeded(testInfo, browser); diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 705ee4091f..cfe2d471ff 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -35,6 +35,7 @@ export { program } from 'playwright-core/lib/cli/program'; import type { ReporterDescription } from '../types/test'; import { prepareErrorStack } from './reporters/base'; import { affectedTestFiles, cacheDir } from './transform/compilationCache'; +import { runTestServer } from './runner/testServer'; function addTestCommand(program: Command) { const command = program.command('test [test-filter...]'); @@ -115,6 +116,15 @@ 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); + }); +} + function addShowReportCommand(program: Command) { const command = program.command('show-report [report]'); command.description('show HTML report'); @@ -339,3 +349,4 @@ addListFilesCommand(program); addMergeReportsCommand(program); addClearCacheCommand(program); addFindRelatedTestFilesCommand(program); +addTestServerCommand(program); diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 28f363d58f..08f030dc0c 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -88,7 +88,8 @@ function addGlobalSetupTasks(taskRunner: TaskRunner, config: FullConfig taskRunner.addTask('plugin setup', createPluginSetupTask(plugin)); if (config.config.globalSetup || config.config.globalTeardown) taskRunner.addTask('global setup', createGlobalSetupTask()); - taskRunner.addTask('clear output', createRemoveOutputDirsTask()); + if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) + taskRunner.addTask('clear output', createRemoveOutputDirsTask()); } function addRunTasks(taskRunner: TaskRunner, config: FullConfigInternal) { @@ -165,8 +166,6 @@ function createGlobalSetupTask(): Task { function createRemoveOutputDirsTask(): Task { return { setup: async ({ config }) => { - if (process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) - return; const outputDirs = new Set(); const projects = filterProjects(config.projects, config.cliProjectFilter); projects.forEach(p => outputDirs.add(p.project.outputDir)); diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts new file mode 100644 index 0000000000..6fcdd18f6d --- /dev/null +++ b/packages/playwright/src/runner/testServer.ts @@ -0,0 +1,125 @@ +/** + * 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 http from 'http'; +import { ManualPromise, createGuid } 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 type { FullConfigInternal } from '../common/config'; +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'; + +type PlaywrightTestOptions = { + headed?: boolean, + oneWorker?: boolean, + trace?: 'on' | 'off', + projects?: string[]; + grep?: string; + reuseContext?: boolean, + connectWsEndpoint?: string; +}; + +export async function runTestServer(configFile: string) { + process.env.PW_TEST_HTML_REPORT_OPEN = 'never'; + process.env.FORCE_COLOR = '1'; + + const config = await loadConfigFromFile(configFile); + if (!config) + return; + + const dispatcher = new Dispatcher(config); + const wss = new WSServer({ + onConnection(request: http.IncomingMessage, url: URL, ws: WebSocket, id: string) { + 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 })); + }); + return { + async close() {} + }; + }, + }); + const url = await wss.listen(0, 'localhost', '/' + createGuid()); + // eslint-disable-next-line no-console + console.log(`Listening on ${url}`); +} + +class Dispatcher { + private _config: FullConfigInternal; + + constructor(config: FullConfigInternal) { + this._config = config; + } + + async test(params: { mode: 'list' | 'run', locations: string[], options: PlaywrightTestOptions, 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); + } +} + + +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(); +} diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index ec165a2a69..5758bf673e 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -417,15 +417,20 @@ async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: nu config.config.workers = 1; showBrowserServer = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: 1 }); const wsEndpoint = await showBrowserServer.listen(); - process.env.PW_TEST_REUSE_CONTEXT = '1'; - process.env.PW_TEST_CONNECT_WS_ENDPOINT = wsEndpoint; + config.configCLIOverrides.use = { + ...config.configCLIOverrides.use, + _optionContextReuseMode: 'when-possible', + _optionConnectOptions: { wsEndpoint }, + }; process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('on')}\n`); } else { config.config.workers = originalWorkers; + if (config.configCLIOverrides.use) { + delete config.configCLIOverrides.use._optionContextReuseMode; + delete config.configCLIOverrides.use._optionConnectOptions; + } await showBrowserServer?.close(); showBrowserServer = undefined; - delete process.env.PW_TEST_REUSE_CONTEXT; - delete process.env.PW_TEST_CONNECT_WS_ENDPOINT; process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('off')}\n`); } }