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';
|
import { program } from '../utilsBundle';
|
||||||
export { program } from '../utilsBundle';
|
export { program } from '../utilsBundle';
|
||||||
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
|
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
|
||||||
import type { OpenTraceViewerOptions } from '../server/trace/viewer/traceViewer';
|
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
|
||||||
import { openTraceInBrowser, openTraceViewerApp } from '../server/trace/viewer/traceViewer';
|
import type { TraceViewerServerOptions } from '../server/trace/viewer/traceViewer';
|
||||||
import * as playwright from '../..';
|
import * as playwright from '../..';
|
||||||
import type { BrowserContext } from '../client/browserContext';
|
import type { BrowserContext } from '../client/browserContext';
|
||||||
import type { Browser } from '../client/browser';
|
import type { Browser } from '../client/browser';
|
||||||
|
|
@ -305,19 +305,16 @@ program
|
||||||
if (options.browser === 'wk')
|
if (options.browser === 'wk')
|
||||||
options.browser = 'webkit';
|
options.browser = 'webkit';
|
||||||
|
|
||||||
const openOptions: OpenTraceViewerOptions = {
|
const openOptions: TraceViewerServerOptions = {
|
||||||
headless: false,
|
|
||||||
host: options.host,
|
host: options.host,
|
||||||
port: +options.port,
|
port: +options.port,
|
||||||
isServer: !!options.stdin,
|
isServer: !!options.stdin,
|
||||||
};
|
};
|
||||||
if (options.port !== undefined || options.host !== undefined) {
|
|
||||||
openTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
|
if (options.port !== undefined || options.host !== undefined)
|
||||||
} else {
|
runTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
|
||||||
openTraceViewerApp(traces, options.browser, openOptions).then(page => {
|
else
|
||||||
page.on('close', () => gracefullyProcessExitDoNotHang(0));
|
runTraceViewerApp(traces, options.browser, openOptions, true).catch(logErrorAndExit);
|
||||||
}).catch(logErrorAndExit);
|
|
||||||
}
|
|
||||||
}).addHelpText('afterAll', `
|
}).addHelpText('afterAll', `
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,6 @@ export { createPlaywright } from './playwright';
|
||||||
|
|
||||||
export type { DispatcherScope } from './dispatchers/dispatcher';
|
export type { DispatcherScope } from './dispatchers/dispatcher';
|
||||||
export type { Playwright } from './playwright';
|
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 { serverSideCallMetadata } from './instrumentation';
|
||||||
export { SocksProxy } from '../common/socksProxy';
|
export { SocksProxy } from '../common/socksProxy';
|
||||||
|
|
|
||||||
|
|
@ -17,34 +17,35 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { HttpServer } from '../../../utils/httpServer';
|
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 { syncLocalStorageWithSettings } from '../../launchApp';
|
||||||
import { serverSideCallMetadata } from '../../instrumentation';
|
import { serverSideCallMetadata } from '../../instrumentation';
|
||||||
import { createPlaywright } from '../../playwright';
|
import { createPlaywright } from '../../playwright';
|
||||||
import { ProgressController } from '../../progress';
|
import { ProgressController } from '../../progress';
|
||||||
import { open, wsServer } from '../../../utilsBundle';
|
import { open } from '../../../utilsBundle';
|
||||||
import type { Page } from '../../page';
|
import type { Page } from '../../page';
|
||||||
import type { BrowserType } from '../../browserType';
|
import type { BrowserType } from '../../browserType';
|
||||||
import { launchApp } from '../../launchApp';
|
import { launchApp } from '../../launchApp';
|
||||||
|
|
||||||
export type Transport = {
|
export type TraceViewerServerOptions = {
|
||||||
sendEvent?: (method: string, params: any) => void;
|
|
||||||
dispatch: (method: string, params: any) => Promise<any>;
|
|
||||||
close?: () => void;
|
|
||||||
onclose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenTraceViewerOptions = {
|
|
||||||
app?: string;
|
|
||||||
headless?: boolean;
|
|
||||||
host?: string;
|
host?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
isServer?: boolean;
|
isServer?: boolean;
|
||||||
transport?: Transport;
|
transport?: Transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TraceViewerRedirectOptions = {
|
||||||
|
webApp?: string;
|
||||||
|
isServer?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TraceViewerAppOptions = {
|
||||||
|
headless?: boolean;
|
||||||
persistentContextOptions?: Parameters<BrowserType['launchPersistentContext']>[2];
|
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) {
|
for (const traceUrl of traceUrls) {
|
||||||
let traceFile = traceUrl;
|
let traceFile = traceUrl;
|
||||||
// If .json is requested, we'll synthesize it.
|
// 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')) {
|
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceFile) && !fs.existsSync(traceFile + '.trace')) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(`Trace file ${traceUrl} does not exist!`);
|
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();
|
const server = new HttpServer();
|
||||||
server.routePrefix('/trace', (request, response) => {
|
server.routePrefix('/trace', (request, response) => {
|
||||||
const url = new URL('http://localhost' + request.url!);
|
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);
|
return server.serveFile(request, response, absolutePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
const params = traceUrls.map(t => `trace=${encodeURIComponent(t)}`);
|
|
||||||
const transport = options?.transport || (options?.isServer ? new StdinServer() : undefined);
|
const transport = options?.transport || (options?.isServer ? new StdinServer() : undefined);
|
||||||
|
if (transport)
|
||||||
|
server.createWebSocket(transport);
|
||||||
|
|
||||||
if (transport) {
|
const { host, port } = options || {};
|
||||||
const guid = createGuid();
|
await server.start({ preferredPort: port, host });
|
||||||
params.push('ws=' + guid);
|
return server;
|
||||||
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());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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)
|
if (options?.isServer)
|
||||||
params.push('isServer');
|
params.push('isServer');
|
||||||
if (isUnderTest())
|
if (isUnderTest())
|
||||||
params.push('isUnderTest=true');
|
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 searchQuery = params.length ? '?' + params.join('&') : '';
|
||||||
const urlPath = `/trace/${app || 'index.html'}${searchQuery}`;
|
const urlPath = `/trace/${options.webApp || 'index.html'}${searchQuery}`;
|
||||||
|
|
||||||
server.routePath('/', (request, response) => {
|
server.routePath('/', (request, response) => {
|
||||||
response.statusCode = 302;
|
response.statusCode = 302;
|
||||||
|
|
@ -125,12 +118,28 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
|
||||||
response.end();
|
response.end();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { server, url };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openTraceViewerApp(traceUrls: string[], browserName: string, options?: OpenTraceViewerOptions): Promise<Page> {
|
export async function runTraceViewerApp(traceUrls: string[], browserName: string, options: TraceViewerServerOptions, exitOnClose?: boolean) {
|
||||||
const { url } = await startTraceViewerServer(traceUrls, options);
|
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 traceViewerPlaywright = createPlaywright({ sdkLanguage: 'javascript', isInternalPlaywright: true });
|
||||||
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
|
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
|
||||||
|
|
||||||
|
|
@ -163,8 +172,7 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openTraceInBrowser(traceUrls: string[], options?: OpenTraceViewerOptions) {
|
export async function openTraceInBrowser(url: string) {
|
||||||
const { url } = await startTraceViewerServer(traceUrls, options);
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('\nListening on ' + url);
|
console.log('\nListening on ' + url);
|
||||||
if (!isUnderTest())
|
if (!isUnderTest())
|
||||||
|
|
|
||||||
|
|
@ -17,19 +17,28 @@
|
||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { mime } from '../utilsBundle';
|
import { mime, wsServer } from '../utilsBundle';
|
||||||
import { assert } from './debug';
|
import { assert } from './debug';
|
||||||
import { createHttpServer } from './network';
|
import { createHttpServer } from './network';
|
||||||
import { ManualPromise } from './manualPromise';
|
import { ManualPromise } from './manualPromise';
|
||||||
|
import { createGuid } from './crypto';
|
||||||
|
|
||||||
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
|
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 {
|
export class HttpServer {
|
||||||
private _server: http.Server;
|
private _server: http.Server;
|
||||||
private _urlPrefix: string;
|
private _urlPrefix: string;
|
||||||
private _port: number = 0;
|
private _port: number = 0;
|
||||||
private _started = false;
|
private _started = false;
|
||||||
private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = [];
|
private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = [];
|
||||||
|
private _wsGuid: string | undefined;
|
||||||
|
|
||||||
constructor(address: string = '') {
|
constructor(address: string = '') {
|
||||||
this._urlPrefix = address;
|
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> {
|
async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise<string> {
|
||||||
assert(!this._started, 'server already started');
|
assert(!this._started, 'server already started');
|
||||||
this._started = true;
|
this._started = true;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ export { program } from 'playwright-core/lib/cli/program';
|
||||||
import type { ReporterDescription } from '../types/test';
|
import type { ReporterDescription } from '../types/test';
|
||||||
import { prepareErrorStack } from './reporters/base';
|
import { prepareErrorStack } from './reporters/base';
|
||||||
import { cacheDir } from './transform/compilationCache';
|
import { cacheDir } from './transform/compilationCache';
|
||||||
import { runTestServer } from './runner/testServer';
|
|
||||||
|
|
||||||
function addTestCommand(program: Command) {
|
function addTestCommand(program: Command) {
|
||||||
const command = program.command('test [test-filter...]');
|
const command = program.command('test [test-filter...]');
|
||||||
|
|
@ -108,9 +107,9 @@ function addFindRelatedTestFilesCommand(program: Command) {
|
||||||
function addTestServerCommand(program: Command) {
|
function addTestServerCommand(program: Command) {
|
||||||
const command = program.command('test-server', { hidden: true });
|
const command = program.command('test-server', { hidden: true });
|
||||||
command.description('start test server');
|
command.description('start test server');
|
||||||
command.action(() => {
|
command.option('--host <host>', 'Host to start the server on', 'localhost');
|
||||||
void runTestServer();
|
command.option('--port <port>', 'Port to start the server on', '0');
|
||||||
});
|
command.action(opts => runTestServer(opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
function addShowReportCommand(program: Command) {
|
function addShowReportCommand(program: Command) {
|
||||||
|
|
@ -166,7 +165,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||||
const runner = new Runner(config);
|
const runner = new Runner(config);
|
||||||
let status: FullResult['status'];
|
let status: FullResult['status'];
|
||||||
if (opts.ui || opts.uiHost || opts.uiPort)
|
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)
|
else if (process.env.PWTEST_WATCH)
|
||||||
status = await runner.watchAllTests();
|
status = await runner.watchAllTests();
|
||||||
else
|
else
|
||||||
|
|
@ -176,6 +175,19 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||||
gracefullyProcessExitDoNotHang(exitCode);
|
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>) {
|
export async function withRunnerAndMutedWrite(configFile: string | undefined, callback: (runner: Runner) => Promise<any>) {
|
||||||
// Redefine process.stdout.write in case config decides to pollute stdio.
|
// Redefine process.stdout.write in case config decides to pollute stdio.
|
||||||
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
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 type { FullResult, TestError } from '../../types/testReporter';
|
||||||
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
||||||
import { collectFilesForProject, filterProjects } from './projectUtils';
|
import { collectFilesForProject, filterProjects } from './projectUtils';
|
||||||
|
|
@ -25,12 +26,13 @@ import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks';
|
||||||
import type { FullConfigInternal } from '../common/config';
|
import type { FullConfigInternal } from '../common/config';
|
||||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import { runWatchModeLoop } from './watchMode';
|
import { runWatchModeLoop } from './watchMode';
|
||||||
import { runUIMode } from './uiMode';
|
import { runTestServer } from './uiMode';
|
||||||
import { InternalReporter } from '../reporters/internalReporter';
|
import { InternalReporter } from '../reporters/internalReporter';
|
||||||
import { Multiplexer } from '../reporters/multiplexer';
|
import { Multiplexer } from '../reporters/multiplexer';
|
||||||
import type { Suite } from '../common/test';
|
import type { Suite } from '../common/test';
|
||||||
import { wrapReporterAsV2 } from '../reporters/reporterV2';
|
import { wrapReporterAsV2 } from '../reporters/reporterV2';
|
||||||
import { affectedTestFiles } from '../transform/compilationCache';
|
import { affectedTestFiles } from '../transform/compilationCache';
|
||||||
|
import { installRootRedirect, openTraceInBrowser, openTraceViewerApp } from 'playwright-core/lib/server';
|
||||||
|
|
||||||
type ProjectConfigWithFiles = {
|
type ProjectConfigWithFiles = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -146,10 +148,32 @@ export class Runner {
|
||||||
return await runWatchModeLoop(config);
|
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;
|
const config = this._config;
|
||||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
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> {
|
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> {
|
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: ReporterV2, additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
|
||||||
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
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);
|
addRunTasks(taskRunner, config);
|
||||||
return taskRunner;
|
return taskRunner;
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: R
|
||||||
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
|
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
|
||||||
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
||||||
addGlobalSetupTasks(taskRunner, config);
|
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);
|
addRunTasks(taskRunner, config);
|
||||||
return taskRunner;
|
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 {
|
return {
|
||||||
setup: async (testRun, errors, softErrors) => {
|
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);
|
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
|
||||||
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
|
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
|
||||||
testRun.failureTracker.onRootSuite(testRun.rootSuite);
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { openTraceViewerApp, openTraceInBrowser, registry } from 'playwright-core/lib/server';
|
import { registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
||||||
import { isUnderTest, ManualPromise } from 'playwright-core/lib/utils';
|
import { ManualPromise } from 'playwright-core/lib/utils';
|
||||||
|
import type { Transport, HttpServer } from 'playwright-core/lib/utils';
|
||||||
import type { FullResult } from '../../types/testReporter';
|
import type { FullResult } from '../../types/testReporter';
|
||||||
import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
|
import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
|
||||||
import type { FullConfigInternal } from '../common/config';
|
import type { FullConfigInternal } from '../common/config';
|
||||||
|
|
@ -25,14 +26,13 @@ import { createReporters } from './reporters';
|
||||||
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||||
import { open } from 'playwright-core/lib/utilsBundle';
|
import { open } from 'playwright-core/lib/utilsBundle';
|
||||||
import ListReporter from '../reporters/list';
|
import ListReporter from '../reporters/list';
|
||||||
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
|
|
||||||
import { Multiplexer } from '../reporters/multiplexer';
|
import { Multiplexer } from '../reporters/multiplexer';
|
||||||
import { SigIntWatcher } from './sigIntWatcher';
|
import { SigIntWatcher } from './sigIntWatcher';
|
||||||
import { Watcher } from '../fsWatcher';
|
import { Watcher } from '../fsWatcher';
|
||||||
|
|
||||||
class UIMode {
|
class TestServer {
|
||||||
private _config: FullConfigInternal;
|
private _config: FullConfigInternal;
|
||||||
private _transport!: Transport;
|
private _transport: Transport | undefined;
|
||||||
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
|
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
|
||||||
globalCleanup: (() => Promise<FullResult['status']>) | undefined;
|
globalCleanup: (() => Promise<FullResult['status']>) | undefined;
|
||||||
private _globalWatcher: Watcher;
|
private _globalWatcher: Watcher;
|
||||||
|
|
@ -80,10 +80,9 @@ class UIMode {
|
||||||
return status;
|
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();
|
let queue = Promise.resolve();
|
||||||
|
const transport: Transport = {
|
||||||
this._transport = {
|
|
||||||
dispatch: async (method, params) => {
|
dispatch: async (method, params) => {
|
||||||
if (method === 'ping')
|
if (method === 'ping')
|
||||||
return;
|
return;
|
||||||
|
|
@ -118,25 +117,13 @@ class UIMode {
|
||||||
await queue;
|
await queue;
|
||||||
},
|
},
|
||||||
|
|
||||||
onclose: () => { },
|
onclose: () => {},
|
||||||
};
|
};
|
||||||
const openOptions: OpenTraceViewerOptions = {
|
this._transport = transport;
|
||||||
app: 'uiMode.html',
|
return await startTraceViewerServer({ ...options, transport });
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
wireStdIO() {
|
||||||
if (!process.env.PWTEST_DEBUG) {
|
if (!process.env.PWTEST_DEBUG) {
|
||||||
process.stdout.write = (chunk: string | Buffer) => {
|
process.stdout.write = (chunk: string | Buffer) => {
|
||||||
this._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
|
this._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
|
||||||
|
|
@ -147,8 +134,9 @@ class UIMode {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await cancelPromise;
|
}
|
||||||
|
|
||||||
|
unwireStdIO() {
|
||||||
if (!process.env.PWTEST_DEBUG) {
|
if (!process.env.PWTEST_DEBUG) {
|
||||||
process.stdout.write = this._originalStdoutWrite;
|
process.stdout.write = this._originalStdoutWrite;
|
||||||
process.stderr.write = this._originalStderrWrite;
|
process.stderr.write = this._originalStderrWrite;
|
||||||
|
|
@ -163,7 +151,7 @@ class UIMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dispatchEvent(method: string, params?: any) {
|
private _dispatchEvent(method: string, params?: any) {
|
||||||
this._transport.sendEvent?.(method, params);
|
this._transport?.sendEvent?.(method, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _listTests() {
|
private async _listTests() {
|
||||||
|
|
@ -227,20 +215,24 @@ class UIMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runUIMode(config: FullConfigInternal, options: { host?: string, port?: number }): Promise<FullResult['status']> {
|
export async function runTestServer(config: FullConfigInternal, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<FullResult['status']> {
|
||||||
const uiMode = new UIMode(config);
|
const testServer = new TestServer(config);
|
||||||
const globalSetupStatus = await uiMode.runGlobalSetup();
|
const globalSetupStatus = await testServer.runGlobalSetup();
|
||||||
if (globalSetupStatus !== 'passed')
|
if (globalSetupStatus !== 'passed')
|
||||||
return globalSetupStatus;
|
return globalSetupStatus;
|
||||||
const cancelPromise = new ManualPromise<void>();
|
const cancelPromise = new ManualPromise<void>();
|
||||||
const sigintWatcher = new SigIntWatcher();
|
const sigintWatcher = new SigIntWatcher();
|
||||||
void sigintWatcher.promise().then(() => cancelPromise.resolve());
|
void sigintWatcher.promise().then(() => cancelPromise.resolve());
|
||||||
try {
|
try {
|
||||||
await uiMode.showUI(options, cancelPromise);
|
const server = await testServer.start(options);
|
||||||
|
await openUI(server, cancelPromise);
|
||||||
|
testServer.wireStdIO();
|
||||||
|
await cancelPromise;
|
||||||
} finally {
|
} finally {
|
||||||
|
testServer.unwireStdIO();
|
||||||
sigintWatcher.disarm();
|
sigintWatcher.disarm();
|
||||||
}
|
}
|
||||||
return await uiMode.globalCleanup?.() || (sigintWatcher.hadSignal() ? 'interrupted' : 'passed');
|
return await testServer.globalCleanup?.() || (sigintWatcher.hadSignal() ? 'interrupted' : 'passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
type StdioPayload = {
|
type StdioPayload = {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import type { Fixtures, FrameLocator, Locator, Page, Browser, BrowserContext } from '@playwright/test';
|
import type { Fixtures, FrameLocator, Locator, Page, Browser, BrowserContext } from '@playwright/test';
|
||||||
import { step } from './baseTest';
|
import { step } from './baseTest';
|
||||||
import { openTraceViewerApp } from '../../packages/playwright-core/lib/server';
|
import { runTraceViewerApp } from '../../packages/playwright-core/lib/server';
|
||||||
|
|
||||||
type BaseTestFixtures = {
|
type BaseTestFixtures = {
|
||||||
context: BrowserContext;
|
context: BrowserContext;
|
||||||
|
|
@ -107,7 +107,7 @@ export const traceViewerFixtures: Fixtures<TraceViewerFixtures, {}, BaseTestFixt
|
||||||
const browsers: Browser[] = [];
|
const browsers: Browser[] = [];
|
||||||
const contextImpls: any[] = [];
|
const contextImpls: any[] = [];
|
||||||
await use(async (traces: string[], { host, port } = {}) => {
|
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 contextImpl = pageImpl.context();
|
||||||
const browser = await playwright.chromium.connectOverCDP(contextImpl._browser.options.wsEndpoint);
|
const browser = await playwright.chromium.connectOverCDP(contextImpl._browser.options.wsEndpoint);
|
||||||
browsers.push(browser);
|
browsers.push(browser);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue