mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: use test server as a singleton (#29630)
This commit is contained in:
parent
ee93136132
commit
2ca45ff948
@ -93,7 +93,7 @@ export async function deserializeConfig(data: SerializedConfig): Promise<FullCon
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function loadUserConfig(location: ConfigLocation): Promise<Config> {
|
||||
async function loadUserConfig(location: ConfigLocation): Promise<Config> {
|
||||
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,8 +348,9 @@ export function restartWithExperimentalTsEsm(configFile: string | undefined): bo
|
||||
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
||||
return false;
|
||||
}
|
||||
if (!fileIsModule(configFile))
|
||||
if (!force && !fileIsModule(configFile!))
|
||||
return false;
|
||||
|
||||
// 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), {
|
||||
|
||||
@ -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 <file>', `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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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<void>;
|
||||
|
||||
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<void>;
|
||||
|
||||
findRelatedTestFiles(params: {
|
||||
configFile: string;
|
||||
files: string[];
|
||||
}): Promise<{ testFiles: string[]; errors?: TestError[]; }>;
|
||||
|
||||
stop(params: {
|
||||
configFile: string;
|
||||
}): Promise<void>;
|
||||
|
||||
closeGracefully(): Promise<void>;
|
||||
}
|
||||
|
||||
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<FullResult['status']>, stop: ManualPromise<void> } | 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<FindRelatedTestFilesReport> {
|
||||
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<FindRelatedTestFilesReport> {
|
||||
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<FullConfigInternal> {
|
||||
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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user