chore: prepare to reuse test server from ui mode (#29965)

This commit is contained in:
Pavel Feldman 2024-03-18 09:50:11 -07:00 committed by GitHub
parent 0db1d40abc
commit 6faadf5160
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 165 additions and 328 deletions

View file

@ -23,8 +23,8 @@ import type { Command } from '../utilsBundle';
import { program } from '../utilsBundle';
export { program } from '../utilsBundle';
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
import type { OpenTraceViewerOptions } from '../server/trace/viewer/traceViewer';
import { openTraceInBrowser, openTraceViewerApp } from '../server/trace/viewer/traceViewer';
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
import type { TraceViewerServerOptions } from '../server/trace/viewer/traceViewer';
import * as playwright from '../..';
import type { BrowserContext } from '../client/browserContext';
import type { Browser } from '../client/browser';
@ -305,19 +305,16 @@ program
if (options.browser === 'wk')
options.browser = 'webkit';
const openOptions: OpenTraceViewerOptions = {
headless: false,
const openOptions: TraceViewerServerOptions = {
host: options.host,
port: +options.port,
isServer: !!options.stdin,
};
if (options.port !== undefined || options.host !== undefined) {
openTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
} else {
openTraceViewerApp(traces, options.browser, openOptions).then(page => {
page.on('close', () => gracefullyProcessExitDoNotHang(0));
}).catch(logErrorAndExit);
}
if (options.port !== undefined || options.host !== undefined)
runTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
else
runTraceViewerApp(traces, options.browser, openOptions, true).catch(logErrorAndExit);
}).addHelpText('afterAll', `
Examples:

View file

@ -29,6 +29,6 @@ export { createPlaywright } from './playwright';
export type { DispatcherScope } from './dispatchers/dispatcher';
export type { Playwright } from './playwright';
export { openTraceInBrowser, openTraceViewerApp } from './trace/viewer/traceViewer';
export { openTraceInBrowser, openTraceViewerApp, runTraceViewerApp, startTraceViewerServer, installRootRedirect } from './trace/viewer/traceViewer';
export { serverSideCallMetadata } from './instrumentation';
export { SocksProxy } from '../common/socksProxy';

View file

@ -17,34 +17,35 @@
import path from 'path';
import fs from 'fs';
import { HttpServer } from '../../../utils/httpServer';
import { createGuid, gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
import type { Transport } from '../../../utils/httpServer';
import { gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
import { syncLocalStorageWithSettings } from '../../launchApp';
import { serverSideCallMetadata } from '../../instrumentation';
import { createPlaywright } from '../../playwright';
import { ProgressController } from '../../progress';
import { open, wsServer } from '../../../utilsBundle';
import { open } from '../../../utilsBundle';
import type { Page } from '../../page';
import type { BrowserType } from '../../browserType';
import { launchApp } from '../../launchApp';
export type Transport = {
sendEvent?: (method: string, params: any) => void;
dispatch: (method: string, params: any) => Promise<any>;
close?: () => void;
onclose: () => void;
};
export type OpenTraceViewerOptions = {
app?: string;
headless?: boolean;
export type TraceViewerServerOptions = {
host?: string;
port?: number;
isServer?: boolean;
transport?: Transport;
};
export type TraceViewerRedirectOptions = {
webApp?: string;
isServer?: boolean;
};
export type TraceViewerAppOptions = {
headless?: boolean;
persistentContextOptions?: Parameters<BrowserType['launchPersistentContext']>[2];
};
async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceViewerOptions): Promise<{ server: HttpServer, url: string }> {
async function validateTraceUrls(traceUrls: string[]) {
for (const traceUrl of traceUrls) {
let traceFile = traceUrl;
// If .json is requested, we'll synthesize it.
@ -54,10 +55,13 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceFile) && !fs.existsSync(traceFile + '.trace')) {
// eslint-disable-next-line no-console
console.error(`Trace file ${traceUrl} does not exist!`);
gracefullyProcessExitDoNotHang(1);
return false;
}
}
return true;
}
export async function startTraceViewerServer(options?: TraceViewerServerOptions): Promise<HttpServer> {
const server = new HttpServer();
server.routePrefix('/trace', (request, response) => {
const url = new URL('http://localhost' + request.url!);
@ -88,36 +92,25 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
return server.serveFile(request, response, absolutePath);
});
const params = traceUrls.map(t => `trace=${encodeURIComponent(t)}`);
const transport = options?.transport || (options?.isServer ? new StdinServer() : undefined);
if (transport)
server.createWebSocket(transport);
if (transport) {
const guid = createGuid();
params.push('ws=' + guid);
const wss = new wsServer({ server: server.server(), path: '/' + guid });
wss.on('connection', ws => {
transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
transport.close = () => ws.close();
ws.on('message', async (message: string) => {
const { id, method, params } = JSON.parse(message);
const result = await transport.dispatch(method, params);
ws.send(JSON.stringify({ id, result }));
});
ws.on('close', () => transport.onclose());
ws.on('error', () => transport.onclose());
});
}
const { host, port } = options || {};
await server.start({ preferredPort: port, host });
return server;
}
export async function installRootRedirect(server: HttpServer, traceUrls: string[], options: TraceViewerRedirectOptions) {
const params = (traceUrls || []).map(t => `trace=${encodeURIComponent(t)}`);
if (server.wsGuid())
params.push('ws=' + server.wsGuid());
if (options?.isServer)
params.push('isServer');
if (isUnderTest())
params.push('isUnderTest=true');
const { host, port } = options || {};
const url = await server.start({ preferredPort: port, host });
const { app } = options || {};
const searchQuery = params.length ? '?' + params.join('&') : '';
const urlPath = `/trace/${app || 'index.html'}${searchQuery}`;
const urlPath = `/trace/${options.webApp || 'index.html'}${searchQuery}`;
server.routePath('/', (request, response) => {
response.statusCode = 302;
@ -125,12 +118,28 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
response.end();
return true;
});
return { server, url };
}
export async function openTraceViewerApp(traceUrls: string[], browserName: string, options?: OpenTraceViewerOptions): Promise<Page> {
const { url } = await startTraceViewerServer(traceUrls, options);
export async function runTraceViewerApp(traceUrls: string[], browserName: string, options: TraceViewerServerOptions, exitOnClose?: boolean) {
if (!validateTraceUrls(traceUrls))
return;
const server = await startTraceViewerServer(options);
await installRootRedirect(server, traceUrls, options);
const page = await openTraceViewerApp(server.urlPrefix(), browserName);
if (exitOnClose)
page.on('close', () => gracefullyProcessExitDoNotHang(0));
return page;
}
export async function runTraceInBrowser(traceUrls: string[], options: TraceViewerServerOptions) {
if (!validateTraceUrls(traceUrls))
return;
const server = await startTraceViewerServer(options);
await installRootRedirect(server, traceUrls, options);
await openTraceInBrowser(server.urlPrefix());
}
export async function openTraceViewerApp(url: string, browserName: string, options?: TraceViewerAppOptions): Promise<Page> {
const traceViewerPlaywright = createPlaywright({ sdkLanguage: 'javascript', isInternalPlaywright: true });
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
@ -163,8 +172,7 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin
return page;
}
export async function openTraceInBrowser(traceUrls: string[], options?: OpenTraceViewerOptions) {
const { url } = await startTraceViewerServer(traceUrls, options);
export async function openTraceInBrowser(url: string) {
// eslint-disable-next-line no-console
console.log('\nListening on ' + url);
if (!isUnderTest())

View file

@ -17,19 +17,28 @@
import type http from 'http';
import fs from 'fs';
import path from 'path';
import { mime } from '../utilsBundle';
import { mime, wsServer } from '../utilsBundle';
import { assert } from './debug';
import { createHttpServer } from './network';
import { ManualPromise } from './manualPromise';
import { createGuid } from './crypto';
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
export type Transport = {
sendEvent?: (method: string, params: any) => void;
dispatch: (method: string, params: any) => Promise<any>;
close?: () => void;
onclose: () => void;
};
export class HttpServer {
private _server: http.Server;
private _urlPrefix: string;
private _port: number = 0;
private _started = false;
private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = [];
private _wsGuid: string | undefined;
constructor(address: string = '') {
this._urlPrefix = address;
@ -68,6 +77,27 @@ export class HttpServer {
}
}
createWebSocket(transport: Transport, guid?: string) {
assert(!this._wsGuid, 'can only create one main websocket transport per server');
this._wsGuid = guid || createGuid();
const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid });
wss.on('connection', ws => {
transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
transport.close = () => ws.close();
ws.on('message', async (message: string) => {
const { id, method, params } = JSON.parse(message);
const result = await transport.dispatch(method, params);
ws.send(JSON.stringify({ id, result }));
});
ws.on('close', () => transport.onclose());
ws.on('error', () => transport.onclose());
});
}
wsGuid(): string | undefined {
return this._wsGuid;
}
async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise<string> {
assert(!this._started, 'server already started');
this._started = true;

View file

@ -34,7 +34,6 @@ export { program } from 'playwright-core/lib/cli/program';
import type { ReporterDescription } from '../types/test';
import { prepareErrorStack } from './reporters/base';
import { cacheDir } from './transform/compilationCache';
import { runTestServer } from './runner/testServer';
function addTestCommand(program: Command) {
const command = program.command('test [test-filter...]');
@ -108,9 +107,9 @@ function addFindRelatedTestFilesCommand(program: Command) {
function addTestServerCommand(program: Command) {
const command = program.command('test-server', { hidden: true });
command.description('start test server');
command.action(() => {
void runTestServer();
});
command.option('--host <host>', 'Host to start the server on', 'localhost');
command.option('--port <port>', 'Port to start the server on', '0');
command.action(opts => runTestServer(opts));
}
function addShowReportCommand(program: Command) {
@ -166,7 +165,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
const runner = new Runner(config);
let status: FullResult['status'];
if (opts.ui || opts.uiHost || opts.uiPort)
status = await runner.uiAllTests({ host: opts.uiHost, port: opts.uiPort ? +opts.uiPort : undefined });
status = await runner.runUIMode({ host: opts.uiHost, port: opts.uiPort ? +opts.uiPort : undefined });
else if (process.env.PWTEST_WATCH)
status = await runner.watchAllTests();
else
@ -176,6 +175,19 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
gracefullyProcessExitDoNotHang(exitCode);
}
async function runTestServer(opts: { [key: string]: any }) {
const config = await loadConfigFromFileRestartIfNeeded(opts.config, overridesFromOptions(opts), opts.deps === false);
if (!config)
return;
config.cliPassWithNoTests = true;
const runner = new Runner(config);
const host = opts.host || 'localhost';
const port = opts.port ? +opts.port : 0;
const status = await runner.runTestServer({ host, port });
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
gracefullyProcessExitDoNotHang(exitCode);
}
export async function withRunnerAndMutedWrite(configFile: string | undefined, callback: (runner: Runner) => Promise<any>) {
// Redefine process.stdout.write in case config decides to pollute stdio.
const stdoutWrite = process.stdout.write.bind(process.stdout);

View file

@ -16,7 +16,8 @@
*/
import path from 'path';
import { monotonicTime } from 'playwright-core/lib/utils';
import type { HttpServer, ManualPromise } from 'playwright-core/lib/utils';
import { isUnderTest, monotonicTime } from 'playwright-core/lib/utils';
import type { FullResult, TestError } from '../../types/testReporter';
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import { collectFilesForProject, filterProjects } from './projectUtils';
@ -25,12 +26,13 @@ import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks';
import type { FullConfigInternal } from '../common/config';
import { colors } from 'playwright-core/lib/utilsBundle';
import { runWatchModeLoop } from './watchMode';
import { runUIMode } from './uiMode';
import { runTestServer } from './uiMode';
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';
import { installRootRedirect, openTraceInBrowser, openTraceViewerApp } from 'playwright-core/lib/server';
type ProjectConfigWithFiles = {
name: string;
@ -146,10 +148,32 @@ export class Runner {
return await runWatchModeLoop(config);
}
async uiAllTests(options: { host?: string, port?: number }): Promise<FullResult['status']> {
async runUIMode(options: { host?: string, port?: number }): Promise<FullResult['status']> {
const config = this._config;
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
return await runUIMode(config, options);
return await runTestServer(config, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => {
await installRootRedirect(server, [], { webApp: 'uiMode.html' });
if (options.host !== undefined || options.port !== undefined) {
await openTraceInBrowser(server.urlPrefix());
} else {
const page = await openTraceViewerApp(server.urlPrefix(), 'chromium', {
headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
persistentContextOptions: {
handleSIGINT: false,
},
});
page.on('close', () => cancelPromise.resolve());
}
});
}
async runTestServer(options: { host?: string, port?: number }): Promise<FullResult['status']> {
const config = this._config;
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
return await runTestServer(config, options, async server => {
// eslint-disable-next-line no-console
console.log('Listening on ' + server.urlPrefix().replace('http:', 'ws:') + '/' + server.wsGuid());
});
}
async findRelatedTestFiles(mode: 'in-process' | 'out-of-process', files: string[]): Promise<FindRelatedTestFilesReport> {

View file

@ -78,7 +78,7 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: ReporterV2, additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunTestsOutsideProjectFilter: true, additionalFileMatcher }));
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher }));
addRunTasks(taskRunner, config);
return taskRunner;
}
@ -86,7 +86,7 @@ export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: R
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
addGlobalSetupTasks(taskRunner, config);
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunTestsOutsideProjectFilter: true }));
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }));
addRunTasks(taskRunner, config);
return taskRunner;
}
@ -195,10 +195,10 @@ function createRemoveOutputDirsTask(): Task<TestRun> {
};
}
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunTestsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
return {
setup: async (testRun, errors, softErrors) => {
await collectProjectsAndTestFiles(testRun, !!options.doNotRunTestsOutsideProjectFilter, options.additionalFileMatcher);
await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher);
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
testRun.failureTracker.onRootSuite(testRun.rootSuite);

View file

@ -1,226 +0,0 @@
/**
* 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 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, TestError } from 'playwright/types/testReporter';
import { loadConfig, restartWithExperimentalTsEsm } from '../common/configLoader';
import { InternalReporter } from '../reporters/internalReporter';
import { Multiplexer } from '../reporters/multiplexer';
import { createReporterForTestServer, createReporters } from './reporters';
import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer } from './tasks';
import type { ConfigCLIOverrides } from '../common/ipc';
import { Runner } from './runner';
import type { FindRelatedTestFilesReport } from './runner';
import type { FullConfigInternal } from '../common/config';
import type { TestServerInterface } from './testServerInterface';
import { serializeError } from '../util';
import { prepareErrorStack } from '../reporters/base';
export async function runTestServer() {
if (restartWithExperimentalTsEsm(undefined, true))
return null;
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(ws);
ws.on('message', async message => {
const { id, method, params } = JSON.parse(String(message));
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() {}
};
},
});
const url = await wss.listen(0, 'localhost', '/' + createGuid());
// eslint-disable-next-line no-console
process.on('exit', () => wss.close().catch(console.error));
// eslint-disable-next-line no-console
console.log(`Listening on ${url}`);
process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0));
}
class Dispatcher implements TestServerInterface {
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
private _ws: WebSocket;
constructor(ws: WebSocket) {
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 listFiles(params: {
configFile: string;
}): Promise<{
projects: {
name: string;
testDir: string;
use: { testIdAttribute?: string };
files: string[];
}[];
cliEntryPoint?: string;
error?: TestError;
}> {
try {
const config = await this._loadConfig(params.configFile);
const runner = new Runner(config);
return runner.listTestFiles();
} catch (e) {
const error: TestError = serializeError(e);
error.location = prepareErrorStack(e.stack).location;
return { projects: [], error };
}
}
async listTests(params: {
configFile: string;
locations: string[];
reporter: string;
env: NodeJS.ProcessEnv;
}) {
const config = await this._loadConfig(params.configFile);
config.cliArgs = params.locations || [];
const wireReporter = await createReporterForTestServer(config, params.reporter, 'list', message => this._dispatchEvent('report', message));
const reporter = new InternalReporter(new Multiplexer([wireReporter]));
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: true });
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 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;
}) {
await this._stopTests();
const overrides: ConfigCLIOverrides = {
repeatEach: 1,
retries: 0,
preserveOutputDir: true,
use: {
trace: params.trace,
headless: params.headed ? false : undefined,
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
},
workers: params.oneWorker ? 1 : undefined,
};
const config = await this._loadConfig(params.configFile, overrides);
config.cliListOnly = false;
config.cliArgs = params.locations || [];
config.cliGrep = params.grep;
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
const wireReporter = await createReporterForTestServer(config, params.reporter, 'test', message => this._dispatchEvent('report', message));
const configReporters = await createReporters(config, 'test');
const reporter = new InternalReporter(new Multiplexer([...configReporters, wireReporter]));
const taskRunner = createTaskRunnerForTestServer(config, reporter);
const testRun = new TestRun(config, reporter);
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();
this._testRun = undefined;
return status;
});
this._testRun = { run, stop };
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 _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);
}
}
function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string) {
if (chunk instanceof Buffer)
return { type, buffer: chunk.toString('base64') };
return { type, text: chunk };
}

View file

@ -14,8 +14,9 @@
* limitations under the License.
*/
import { openTraceViewerApp, openTraceInBrowser, registry } from 'playwright-core/lib/server';
import { isUnderTest, ManualPromise } from 'playwright-core/lib/utils';
import { registry, startTraceViewerServer } from 'playwright-core/lib/server';
import { ManualPromise } from 'playwright-core/lib/utils';
import type { Transport, HttpServer } from 'playwright-core/lib/utils';
import type { FullResult } from '../../types/testReporter';
import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
import type { FullConfigInternal } from '../common/config';
@ -25,14 +26,13 @@ import { createReporters } from './reporters';
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
import { open } from 'playwright-core/lib/utilsBundle';
import ListReporter from '../reporters/list';
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
import { Multiplexer } from '../reporters/multiplexer';
import { SigIntWatcher } from './sigIntWatcher';
import { Watcher } from '../fsWatcher';
class UIMode {
class TestServer {
private _config: FullConfigInternal;
private _transport!: Transport;
private _transport: Transport | undefined;
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
globalCleanup: (() => Promise<FullResult['status']>) | undefined;
private _globalWatcher: Watcher;
@ -80,10 +80,9 @@ class UIMode {
return status;
}
async showUI(options: { host?: string, port?: number }, cancelPromise: ManualPromise<void>) {
async start(options: { host?: string, port?: number }): Promise<HttpServer> {
let queue = Promise.resolve();
this._transport = {
const transport: Transport = {
dispatch: async (method, params) => {
if (method === 'ping')
return;
@ -118,25 +117,13 @@ class UIMode {
await queue;
},
onclose: () => { },
onclose: () => {},
};
const openOptions: OpenTraceViewerOptions = {
app: 'uiMode.html',
headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
transport: this._transport,
host: options.host,
port: options.port,
persistentContextOptions: {
handleSIGINT: false,
},
};
if (options.host !== undefined || options.port !== undefined) {
await openTraceInBrowser([], openOptions);
} else {
const page = await openTraceViewerApp([], 'chromium', openOptions);
page.on('close', () => cancelPromise.resolve());
}
this._transport = transport;
return await startTraceViewerServer({ ...options, transport });
}
wireStdIO() {
if (!process.env.PWTEST_DEBUG) {
process.stdout.write = (chunk: string | Buffer) => {
this._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
@ -147,8 +134,9 @@ class UIMode {
return true;
};
}
await cancelPromise;
}
unwireStdIO() {
if (!process.env.PWTEST_DEBUG) {
process.stdout.write = this._originalStdoutWrite;
process.stderr.write = this._originalStderrWrite;
@ -163,7 +151,7 @@ class UIMode {
}
private _dispatchEvent(method: string, params?: any) {
this._transport.sendEvent?.(method, params);
this._transport?.sendEvent?.(method, params);
}
private async _listTests() {
@ -227,20 +215,24 @@ class UIMode {
}
}
export async function runUIMode(config: FullConfigInternal, options: { host?: string, port?: number }): Promise<FullResult['status']> {
const uiMode = new UIMode(config);
const globalSetupStatus = await uiMode.runGlobalSetup();
export async function runTestServer(config: FullConfigInternal, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<FullResult['status']> {
const testServer = new TestServer(config);
const globalSetupStatus = await testServer.runGlobalSetup();
if (globalSetupStatus !== 'passed')
return globalSetupStatus;
const cancelPromise = new ManualPromise<void>();
const sigintWatcher = new SigIntWatcher();
void sigintWatcher.promise().then(() => cancelPromise.resolve());
try {
await uiMode.showUI(options, cancelPromise);
const server = await testServer.start(options);
await openUI(server, cancelPromise);
testServer.wireStdIO();
await cancelPromise;
} finally {
testServer.unwireStdIO();
sigintWatcher.disarm();
}
return await uiMode.globalCleanup?.() || (sigintWatcher.hadSignal() ? 'interrupted' : 'passed');
return await testServer.globalCleanup?.() || (sigintWatcher.hadSignal() ? 'interrupted' : 'passed');
}
type StdioPayload = {

View file

@ -16,7 +16,7 @@
import type { Fixtures, FrameLocator, Locator, Page, Browser, BrowserContext } from '@playwright/test';
import { step } from './baseTest';
import { openTraceViewerApp } from '../../packages/playwright-core/lib/server';
import { runTraceViewerApp } from '../../packages/playwright-core/lib/server';
type BaseTestFixtures = {
context: BrowserContext;
@ -107,7 +107,7 @@ export const traceViewerFixtures: Fixtures<TraceViewerFixtures, {}, BaseTestFixt
const browsers: Browser[] = [];
const contextImpls: any[] = [];
await use(async (traces: string[], { host, port } = {}) => {
const pageImpl = await openTraceViewerApp(traces, browserName, { headless, host, port });
const pageImpl = await runTraceViewerApp(traces, browserName, { headless, host, port });
const contextImpl = pageImpl.context();
const browser = await playwright.chromium.connectOverCDP(contextImpl._browser.options.wsEndpoint);
browsers.push(browser);