chore: prepare to reuse test server from ui mode (#29965)
This commit is contained in:
parent
0db1d40abc
commit
6faadf5160
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue