chore: send test params over the wire in ui mode (#30046)

This commit is contained in:
Pavel Feldman 2024-03-22 13:49:28 -07:00 committed by GitHub
parent c8e8d8f8bb
commit ee9432b9da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 289 additions and 174 deletions

View File

@ -36,6 +36,14 @@ export type TraceViewerServerOptions = {
};
export type TraceViewerRedirectOptions = {
args?: string[];
grep?: string;
grepInvert?: string;
project?: string[];
workers?: number | string;
headed?: boolean;
timeout?: number;
reporter?: string[];
webApp?: string;
isServer?: boolean;
};
@ -102,19 +110,36 @@ export async function startTraceViewerServer(options?: TraceViewerServerOptions)
}
export async function installRootRedirect(server: HttpServer, traceUrls: string[], options: TraceViewerRedirectOptions) {
const params = (traceUrls || []).map(t => `trace=${encodeURIComponent(t)}`);
const params = new URLSearchParams();
for (const traceUrl of traceUrls)
params.append('trace', traceUrl);
if (server.wsGuid())
params.push('ws=' + server.wsGuid());
params.append('ws', server.wsGuid()!);
if (options?.isServer)
params.push('isServer');
params.append('isServer', '');
if (isUnderTest())
params.push('isUnderTest=true');
const searchQuery = params.length ? '?' + params.join('&') : '';
const urlPath = `/trace/${options.webApp || 'index.html'}${searchQuery}`;
params.append('isUnderTest', 'true');
for (const arg of options.args || [])
params.append('arg', arg);
if (options.grep)
params.append('grep', options.grep);
if (options.grepInvert)
params.append('grepInvert', options.grepInvert);
for (const project of options.project || [])
params.append('project', project);
if (options.workers)
params.append('workers', String(options.workers));
if (options.timeout)
params.append('timeout', String(options.timeout));
if (options.headed)
params.append('headed', '');
for (const reporter of options.reporter || [])
params.append('reporter', reporter);
server.routePath('/', (request, response) => {
const urlPath = `/trace/${options.webApp || 'index.html'}?${params.toString()}`;
server.routePath('/', (_, response) => {
response.statusCode = 302;
response.setHeader('Location', urlPath + request.url!.substring(1));
response.setHeader('Location', urlPath);
response.end();
return true;
});

View File

@ -15,7 +15,6 @@
*/
import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface';
import type * as reporterTypes from 'playwright/types/testReporter';
import * as events from './events';
export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents {
@ -67,7 +66,7 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
this._connectedPromise = new Promise<void>((f, r) => {
this._ws.addEventListener('open', () => {
f();
this._ws.send(JSON.stringify({ method: 'ready' }));
this._ws.send(JSON.stringify({ id: -1, method: 'ready' }));
});
this._ws.addEventListener('error', r);
});
@ -77,6 +76,10 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
});
}
connect() {
return this._connectedPromise;
}
private async _sendMessage(method: string, params?: any): Promise<any> {
const logForTest = (globalThis as any).__logForTest;
logForTest?.({ method, params });
@ -103,81 +106,83 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
this._onListChangedEmitter.fire(params);
else if (method === 'testFilesChanged')
this._onTestFilesChangedEmitter.fire(params);
else if (method === 'loadTraceRequested')
this._onLoadTraceRequestedEmitter.fire(params);
}
async ping(): Promise<void> {
async ping(params: Parameters<TestServerInterface['ping']>[0]): ReturnType<TestServerInterface['ping']> {
await this._sendMessage('ping');
}
async pingNoReply() {
async pingNoReply(params: Parameters<TestServerInterface['ping']>[0]) {
this._sendMessageNoReply('ping');
}
async watch(params: { fileNames: string[]; }): Promise<void> {
async watch(params: Parameters<TestServerInterface['watch']>[0]): ReturnType<TestServerInterface['watch']> {
await this._sendMessage('watch', params);
}
watchNoReply(params: { fileNames: string[]; }) {
watchNoReply(params: Parameters<TestServerInterface['watch']>[0]) {
this._sendMessageNoReply('watch', params);
}
async open(params: { location: reporterTypes.Location; }): Promise<void> {
async open(params: Parameters<TestServerInterface['open']>[0]): ReturnType<TestServerInterface['open']> {
await this._sendMessage('open', params);
}
openNoReply(params: { location: reporterTypes.Location; }) {
openNoReply(params: Parameters<TestServerInterface['open']>[0]) {
this._sendMessageNoReply('open', params);
}
async resizeTerminal(params: { cols: number; rows: number; }): Promise<void> {
async resizeTerminal(params: Parameters<TestServerInterface['resizeTerminal']>[0]): ReturnType<TestServerInterface['resizeTerminal']> {
await this._sendMessage('resizeTerminal', params);
}
resizeTerminalNoReply(params: { cols: number; rows: number; }) {
resizeTerminalNoReply(params: Parameters<TestServerInterface['resizeTerminal']>[0]) {
this._sendMessageNoReply('resizeTerminal', params);
}
async checkBrowsers(): Promise<{ hasBrowsers: boolean; }> {
async checkBrowsers(params: Parameters<TestServerInterface['checkBrowsers']>[0]): ReturnType<TestServerInterface['checkBrowsers']> {
return await this._sendMessage('checkBrowsers');
}
async installBrowsers(): Promise<void> {
async installBrowsers(params: Parameters<TestServerInterface['installBrowsers']>[0]): ReturnType<TestServerInterface['installBrowsers']> {
await this._sendMessage('installBrowsers');
}
async runGlobalSetup(): Promise<'passed' | 'failed' | 'timedout' | 'interrupted'> {
async runGlobalSetup(params: Parameters<TestServerInterface['runGlobalSetup']>[0]): ReturnType<TestServerInterface['runGlobalSetup']> {
return await this._sendMessage('runGlobalSetup');
}
async runGlobalTeardown(): Promise<'passed' | 'failed' | 'timedout' | 'interrupted'> {
async runGlobalTeardown(params: Parameters<TestServerInterface['runGlobalTeardown']>[0]): ReturnType<TestServerInterface['runGlobalTeardown']> {
return await this._sendMessage('runGlobalTeardown');
}
async listFiles(): Promise<{ projects: { name: string; testDir: string; use: { testIdAttribute?: string | undefined; }; files: string[]; }[]; cliEntryPoint?: string | undefined; error?: reporterTypes.TestError | undefined; }> {
return await this._sendMessage('listFiles');
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
return await this._sendMessage('listFiles', params);
}
async listTests(params: { reporter?: string | undefined; fileNames?: string[] | undefined; }): Promise<{ report: any[] }> {
async listTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> {
return await this._sendMessage('listTests', params);
}
async runTests(params: { reporter?: string | undefined; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }): Promise<{ status: reporterTypes.FullResult['status'] }> {
async runTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
return await this._sendMessage('runTests', params);
}
async findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[] | undefined; }> {
async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> {
return await this._sendMessage('findRelatedTestFiles', params);
}
async stopTests(): Promise<void> {
async stopTests(params: Parameters<TestServerInterface['stopTests']>[0]): ReturnType<TestServerInterface['stopTests']> {
await this._sendMessage('stopTests');
}
stopTestsNoReply() {
stopTestsNoReply(params: Parameters<TestServerInterface['stopTests']>[0]) {
this._sendMessageNoReply('stopTests');
}
async closeGracefully(): Promise<void> {
async closeGracefully(params: Parameters<TestServerInterface['closeGracefully']>[0]): ReturnType<TestServerInterface['closeGracefully']> {
await this._sendMessage('closeGracefully');
}
}

View File

@ -18,7 +18,7 @@ import type * as reporterTypes from '../../types/testReporter';
import type { Event } from './events';
export interface TestServerInterface {
ping(): Promise<void>;
ping(params: {}): Promise<void>;
watch(params: {
fileNames: string[];
@ -28,15 +28,17 @@ export interface TestServerInterface {
resizeTerminal(params: { cols: number, rows: number }): Promise<void>;
checkBrowsers(): Promise<{ hasBrowsers: boolean }>;
checkBrowsers(params: {}): Promise<{ hasBrowsers: boolean }>;
installBrowsers(): Promise<void>;
installBrowsers(params: {}): Promise<void>;
runGlobalSetup(): Promise<reporterTypes.FullResult['status']>;
runGlobalSetup(params: {}): Promise<reporterTypes.FullResult['status']>;
runGlobalTeardown(): Promise<reporterTypes.FullResult['status']>;
runGlobalTeardown(params: {}): Promise<reporterTypes.FullResult['status']>;
listFiles(): Promise<{
listFiles(params: {
projects?: string[];
}): Promise<{
projects: {
name: string;
testDir: string;
@ -51,17 +53,21 @@ export interface TestServerInterface {
* Returns list of teleReporter events.
*/
listTests(params: {
reporter?: string;
fileNames?: string[];
serializer?: string;
projects?: string[];
locations?: string[];
}): Promise<{ report: any[] }>;
runTests(params: {
reporter?: string;
serializer?: string;
locations?: string[];
grep?: string;
grepInvert?: string;
testIds?: string[];
headed?: boolean;
oneWorker?: boolean;
workers?: number | string;
timeout?: number,
reporters?: string[],
trace?: 'on' | 'off';
projects?: string[];
reuseContext?: boolean;
@ -72,9 +78,9 @@ export interface TestServerInterface {
files: string[];
}): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[]; }>;
stopTests(): Promise<void>;
stopTests(params: {}): Promise<void>;
closeGracefully(): Promise<void>;
closeGracefully(params: {}): Promise<void>;
}
export interface TestServerInterfaceEvents {

View File

@ -34,6 +34,7 @@ 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 * as testServer from './runner/testServer';
function addTestCommand(program: Command) {
const command = program.command('test [test-filter...]');
@ -151,7 +152,28 @@ Examples:
async function runTests(args: string[], opts: { [key: string]: any }) {
await startProfiling();
const config = await loadConfigFromFileRestartIfNeeded(opts.config, overridesFromOptions(opts), opts.deps === false);
const cliOverrides = overridesFromOptions(opts);
if (opts.ui || opts.uiHost || opts.uiPort) {
const status = await testServer.runUIMode(opts.config, {
host: opts.uiHost,
port: opts.uiPort ? +opts.uiPort : undefined,
args,
grep: opts.grep as string | undefined,
grepInvert: opts.grepInvert as string | undefined,
project: opts.project || undefined,
headed: opts.headed,
reporter: Array.isArray(opts.reporter) ? opts.reporter : opts.reporter ? [opts.reporter] : undefined,
workers: cliOverrides.workers,
timeout: cliOverrides.timeout,
});
await stopProfiling('runner');
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
gracefullyProcessExitDoNotHang(exitCode);
return;
}
const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false);
if (!config)
return;
@ -164,9 +186,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.runUIMode({ host: opts.uiHost, port: opts.uiPort ? +opts.uiPort : undefined });
else if (process.env.PWTEST_WATCH)
if (process.env.PWTEST_WATCH)
status = await runner.watchAllTests();
else
status = await runner.runAllTests();
@ -176,14 +196,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
}
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 status = await testServer.runTestServer(opts.config, { host, port });
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
gracefullyProcessExitDoNotHang(exitCode);
}

View File

@ -51,7 +51,8 @@ type HtmlReporterOptions = {
host?: string,
port?: number,
attachmentsBaseURL?: string,
_mode?: string;
_mode?: 'test' | 'list';
_isTestServer?: boolean;
};
class HtmlReporter extends EmptyReporter {
@ -67,6 +68,8 @@ class HtmlReporter extends EmptyReporter {
constructor(options: HtmlReporterOptions) {
super();
this._options = options;
if (options._mode === 'test')
process.env.PW_HTML_REPORT = '1';
}
override printsToStdio() {
@ -125,7 +128,7 @@ class HtmlReporter extends EmptyReporter {
if (process.env.CI || !this._buildResult)
return;
const { ok, singleTestId } = this._buildResult;
const shouldOpen = this._open === 'always' || (!ok && this._open === 'on-failure');
const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure'));
if (shouldOpen) {
await showHTMLReport(this._outputFolder, this._options.host, this._options.port, singleTestId);
} else if (this._options._mode === 'test') {

View File

@ -37,7 +37,7 @@ type ReportData = {
};
export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[], rootDirOverride: string | undefined) {
const reporters = await createReporters(config, 'merge', reporterDescriptions);
const reporters = await createReporters(config, 'merge', false, reporterDescriptions);
const multiplexer = new Multiplexer(reporters);
const stringPool = new StringInternPool();

View File

@ -33,7 +33,7 @@ import { BlobReporter } from '../reporters/blob';
import type { ReporterDescription } from '../../types/test';
import { type ReporterV2, wrapReporterAsV2 } from '../reporters/reporterV2';
export async function createReporters(config: FullConfigInternal, mode: 'list' | 'test' | 'ui' | 'merge', descriptions?: ReporterDescription[]): Promise<ReporterV2[]> {
export async function createReporters(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean, descriptions?: ReporterDescription[]): Promise<ReporterV2[]> {
const defaultReporters: { [key in BuiltInReporter]: new(arg: any) => ReporterV2 } = {
blob: BlobReporter,
dot: mode === 'list' ? ListModeReporter : DotReporter,
@ -43,14 +43,14 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
json: JSONReporter,
junit: JUnitReporter,
null: EmptyReporter,
html: mode === 'ui' ? LineReporter : HtmlReporter,
html: HtmlReporter,
markdown: MarkdownReporter,
};
const reporters: ReporterV2[] = [];
descriptions ??= config.config.reporter;
if (config.configCLIOverrides.additionalReporters)
descriptions = [...descriptions, ...config.configCLIOverrides.additionalReporters];
const runOptions = reporterOptions(config, mode);
const runOptions = reporterOptions(config, mode, isTestServer);
for (const r of descriptions) {
const [name, arg] = r;
const options = { ...runOptions, ...arg };
@ -78,17 +78,19 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
return reporters;
}
export async function createReporterForTestServer(config: FullConfigInternal, file: string, mode: 'test' | 'list', messageSink: (message: any) => void): Promise<ReporterV2> {
export async function createReporterForTestServer(config: FullConfigInternal, mode: 'list' | 'test', file: string, messageSink: (message: any) => void): Promise<ReporterV2> {
const reporterConstructor = await loadReporter(config, file);
const runOptions = reporterOptions(config, mode, messageSink);
const runOptions = reporterOptions(config, mode, true, messageSink);
const instance = new reporterConstructor(runOptions);
return wrapReporterAsV2(instance);
}
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'ui' | 'merge', send?: (message: any) => void) {
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean, send?: (message: any) => void) {
return {
configDir: config.configDir,
_send: send,
_mode: mode,
_isTestServer: isTestServer,
};
}

View File

@ -16,8 +16,7 @@
*/
import path from 'path';
import type { HttpServer, ManualPromise } from 'playwright-core/lib/utils';
import { isUnderTest, monotonicTime } from 'playwright-core/lib/utils';
import { monotonicTime } from 'playwright-core/lib/utils';
import type { FullResult, TestError } from '../../types/testReporter';
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import { collectFilesForProject, filterProjects } from './projectUtils';
@ -26,13 +25,11 @@ import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks';
import type { FullConfigInternal } from '../common/config';
import { colors } from 'playwright-core/lib/utilsBundle';
import { runWatchModeLoop } from './watchMode';
import { runTestServer } from './testServer';
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;
@ -59,9 +56,9 @@ export class Runner {
this._config = config;
}
async listTestFiles(): Promise<ConfigListFilesReport> {
async listTestFiles(projectNames?: string[]): Promise<ConfigListFilesReport> {
const frameworkPackage = (this._config.config as any)['@playwright/test']?.['packageJSON'];
const projects = filterProjects(this._config.projects);
const projects = filterProjects(this._config.projects, projectNames);
const report: ConfigListFilesReport = {
projects: [],
cliEntryPoint: frameworkPackage ? path.join(path.dirname(frameworkPackage), 'cli.js') : undefined,
@ -85,7 +82,7 @@ export class Runner {
// Legacy webServer support.
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'test')));
const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'test', false)));
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true })
: createTaskRunner(config, reporter);
@ -148,34 +145,6 @@ export class Runner {
return await runWatchModeLoop(config);
}
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 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> {
const result = await this.loadAllTests(mode);
if (result.status !== 'passed' || !result.suite)

View File

@ -16,7 +16,7 @@
import fs from 'fs';
import path from 'path';
import { registry, startTraceViewerServer } from 'playwright-core/lib/server';
import { installRootRedirect, openTraceInBrowser, openTraceViewerApp, registry, startTraceViewerServer } from 'playwright-core/lib/server';
import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils';
import type { Transport, HttpServer } from 'playwright-core/lib/utils';
import type * as reporterTypes from '../../types/testReporter';
@ -34,33 +34,26 @@ import type { TestServerInterface, TestServerInterfaceEventEmitters } from '../i
import { Runner } from './runner';
import { serializeError } from '../util';
import { prepareErrorStack } from '../reporters/base';
import type { ConfigCLIOverrides } from '../common/ipc';
import { loadConfig, resolveConfigFile, restartWithExperimentalTsEsm } from '../common/configLoader';
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playwright-core/lib/server/trace/viewer/traceViewer';
import type { TestRunnerPluginRegistration } from '../plugins';
class TestServer {
private _config: FullConfigInternal;
private _configFile: string | undefined;
private _dispatcher: TestServerDispatcher | undefined;
private _originalStdoutWrite: NodeJS.WriteStream['write'];
private _originalStderrWrite: NodeJS.WriteStream['write'];
constructor(config: FullConfigInternal) {
this._config = config;
process.env.PW_LIVE_TRACE_STACKS = '1';
config.cliListOnly = false;
config.cliPassWithNoTests = true;
config.config.preserveOutput = 'always';
for (const p of config.projects) {
p.project.retries = 0;
p.project.repeatEach = 1;
}
config.configCLIOverrides.use = config.configCLIOverrides.use || {};
config.configCLIOverrides.use.trace = { mode: 'on', sources: false, _live: true };
constructor(configFile: string | undefined) {
this._configFile = configFile;
this._originalStdoutWrite = process.stdout.write;
this._originalStderrWrite = process.stderr.write;
}
async start(options: { host?: string, port?: number }): Promise<HttpServer> {
this._dispatcher = new TestServerDispatcher(this._config);
this._dispatcher = new TestServerDispatcher(this._configFile);
return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport });
}
@ -90,7 +83,7 @@ class TestServer {
}
class TestServerDispatcher implements TestServerInterface {
private _config: FullConfigInternal;
private _configFile: string | undefined;
private _globalWatcher: Watcher;
private _testWatcher: Watcher;
private _testRun: { run: Promise<reporterTypes.FullResult['status']>, stop: ManualPromise<void> } | undefined;
@ -98,9 +91,10 @@ class TestServerDispatcher implements TestServerInterface {
private _queue = Promise.resolve();
private _globalCleanup: (() => Promise<reporterTypes.FullResult['status']>) | undefined;
readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent'];
private _plugins: TestRunnerPluginRegistration[] | undefined;
constructor(config: FullConfigInternal) {
this._config = config;
constructor(configFile: string | undefined) {
this._configFile = configFile;
this.transport = {
dispatch: (method, params) => (this as any)[method](params),
onclose: () => {},
@ -114,16 +108,18 @@ class TestServerDispatcher implements TestServerInterface {
this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params);
}
async ready() {}
async ping() {}
async open(params: { location: reporterTypes.Location }) {
async open(params: Parameters<TestServerInterface['open']>[0]): ReturnType<TestServerInterface['open']> {
if (isUnderTest())
return;
// eslint-disable-next-line no-console
open('vscode://file/' + params.location.file + ':' + params.location.line).catch(e => console.error(e));
}
async resizeTerminal(params: { cols: number; rows: number; }) {
async resizeTerminal(params: Parameters<TestServerInterface['resizeTerminal']>[0]): ReturnType<TestServerInterface['resizeTerminal']> {
process.stdout.columns = params.cols;
process.stdout.rows = params.rows;
process.stderr.columns = params.cols;
@ -141,10 +137,13 @@ class TestServerDispatcher implements TestServerInterface {
async runGlobalSetup(): Promise<reporterTypes.FullResult['status']> {
await this.runGlobalTeardown();
const config = await this._loadConfig(this._configFile);
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
const reporter = new InternalReporter(new ListReporter());
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
reporter.onConfigure(this._config.config);
const testRun = new TestRun(this._config, reporter);
const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
reporter.onConfigure(config.config);
const testRun = new TestRun(config, reporter);
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
await reporter.onEnd({ status });
await reporter.onExit();
@ -162,10 +161,11 @@ class TestServerDispatcher implements TestServerInterface {
return result;
}
async listFiles() {
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
try {
const runner = new Runner(this._config);
return runner.listTestFiles();
const config = await this._loadConfig(this._configFile);
const runner = new Runner(config);
return runner.listTestFiles(params.projects);
} catch (e) {
const error: reporterTypes.TestError = serializeError(e);
error.location = prepareErrorStack(e.stack).location;
@ -173,82 +173,109 @@ class TestServerDispatcher implements TestServerInterface {
}
}
async listTests(params: { reporter?: string; fileNames: string[]; }) {
let report: any[] = [];
async listTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> {
let result: Awaited<ReturnType<TestServerInterface['listTests']>>;
this._queue = this._queue.then(async () => {
report = await this._innerListTests(params);
result = await this._innerListTests(params);
}).catch(printInternalError);
await this._queue;
return { report };
return result!;
}
private async _innerListTests(params: { reporter?: string; fileNames?: string[]; }) {
private async _innerListTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> {
const overrides: ConfigCLIOverrides = {
repeatEach: 1,
retries: 0,
};
const config = await this._loadConfig(this._configFile, overrides);
config.cliArgs = params.locations || [];
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
config.cliListOnly = true;
const wireReporter = await createReporterForTestServer(config, 'list', params.serializer || require.resolve('./uiModeReporter'), e => report.push(e));
const report: any[] = [];
const wireReporter = await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => report.push(e));
const reporter = new InternalReporter(wireReporter);
this._config.cliArgs = params.fileNames || [];
this._config.cliListOnly = true;
this._config.testIdMatcher = undefined;
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
const testRun = new TestRun(this._config, reporter);
reporter.onConfigure(this._config.config);
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false });
const testRun = new TestRun(config, reporter);
reporter.onConfigure(config.config);
const status = await taskRunner.run(testRun, 0);
await reporter.onEnd({ status });
await reporter.onExit();
const projectDirs = new Set<string>();
const projectOutputs = new Set<string>();
for (const p of this._config.projects) {
for (const p of config.projects) {
projectDirs.add(p.project.testDir);
projectOutputs.add(p.project.outputDir);
}
const result = await resolveCtDirs(this._config);
const result = await resolveCtDirs(config);
if (result) {
projectDirs.add(result.templateDir);
projectOutputs.add(result.outDir);
}
this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
return report;
return { report };
}
async runTests(params: { reporter?: string; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }) {
let status: reporterTypes.FullResult['status'];
async runTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
let result: Awaited<ReturnType<TestServerInterface['runTests']>>;
this._queue = this._queue.then(async () => {
status = await this._innerRunTests(params).catch(printInternalError) || 'failed';
result = await this._innerRunTests(params).catch(printInternalError) || { status: 'failed' };
});
await this._queue;
return { status: status! };
return result!;
}
private async _innerRunTests(params: { reporter?: string; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }): Promise<reporterTypes.FullResult['status']> {
private async _innerRunTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
await this.stopTests();
const { testIds, projects, locations, grep } = params;
const overrides: ConfigCLIOverrides = {
repeatEach: 1,
retries: 0,
preserveOutputDir: true,
timeout: params.timeout,
reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
use: {
trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : undefined,
headless: params.headed ? false : undefined,
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
},
workers: params.workers,
};
if (params.trace === 'on')
process.env.PW_LIVE_TRACE_STACKS = '1';
else
process.env.PW_LIVE_TRACE_STACKS = undefined;
const testIdSet = testIds ? new Set<string>(testIds) : null;
this._config.cliArgs = locations ? locations : [];
this._config.cliGrep = grep;
this._config.cliListOnly = false;
this._config.cliProjectFilter = projects?.length ? projects : undefined;
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
const testIdSet = params.testIds ? new Set<string>(params.testIds) : null;
const config = await this._loadConfig(this._configFile, overrides);
config.cliListOnly = false;
config.cliPassWithNoTests = true;
config.cliArgs = params.locations || [];
config.cliGrep = params.grep;
config.cliGrepInvert = params.grepInvert;
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
config.testIdMatcher = testIdSet ? id => testIdSet.has(id) : undefined;
const reporters = await createReporters(this._config, 'ui');
reporters.push(await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => this._dispatchEvent('report', e)));
const reporters = await createReporters(config, 'test', true);
reporters.push(await createReporterForTestServer(config, 'test', params.serializer || require.resolve('./uiModeReporter'), e => this._dispatchEvent('report', e)));
const reporter = new InternalReporter(new Multiplexer(reporters));
const taskRunner = createTaskRunnerForWatch(this._config, reporter);
const testRun = new TestRun(this._config, reporter);
reporter.onConfigure(this._config.config);
const taskRunner = createTaskRunnerForWatch(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;
this._config.testIdMatcher = undefined;
return status;
});
this._testRun = { run, stop };
return await run;
const status = await run;
return { status };
}
async watch(params: { fileNames: string[]; }) {
@ -260,8 +287,9 @@ class TestServerDispatcher implements TestServerInterface {
this._testWatcher.update([...files], [], true);
}
findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[] | undefined; }> {
const runner = new Runner(this._config);
async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> {
const config = await this._loadConfig(this._configFile);
const runner = new Runner(config);
return runner.findRelatedTestFiles('out-of-process', params.files);
}
@ -273,10 +301,49 @@ class TestServerDispatcher implements TestServerInterface {
async closeGracefully() {
gracefullyProcessExitDoNotHang(0);
}
private async _loadConfig(configFile: string | undefined, overrides?: ConfigCLIOverrides): Promise<FullConfigInternal> {
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
const config = await loadConfig({ resolvedConfigFile, configDir: resolvedConfigFile === configFileOrDirectory ? path.dirname(resolvedConfigFile) : configFileOrDirectory }, overrides);
// Preserve plugin instances between setup and build.
if (!this._plugins)
this._plugins = config.plugins || [];
else
config.plugins.splice(0, config.plugins.length, ...this._plugins);
return config;
}
}
export async function runTestServer(config: FullConfigInternal, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<reporterTypes.FullResult['status']> {
const testServer = new TestServer(config);
export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status']> {
return await innerRunTestServer(configFile, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => {
await installRootRedirect(server, [], { ...options, 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());
}
});
}
export async function runTestServer(configFile: string | undefined, options: { host?: string, port?: number }): Promise<reporterTypes.FullResult['status']> {
return await innerRunTestServer(configFile, options, async server => {
// eslint-disable-next-line no-console
console.log('Listening on ' + server.urlPrefix().replace('http:', 'ws:') + '/' + server.wsGuid());
});
}
async function innerRunTestServer(configFile: string | undefined, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<reporterTypes.FullResult['status']> {
if (restartWithExperimentalTsEsm(undefined, true))
return 'passed';
const testServer = new TestServer(configFile);
const cancelPromise = new ManualPromise<void>();
const sigintWatcher = new SigIntWatcher();
void sigintWatcher.promise().then(() => cancelPromise.resolve());

View File

@ -48,6 +48,21 @@ const xtermDataSource: XtermDataSource = {
resize: () => {},
};
const searchParams = new URLSearchParams(window.location.search);
const guid = searchParams.get('ws');
const wsURL = new URL(`../${guid}`, window.location.toString());
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
const queryParams = {
args: searchParams.getAll('arg'),
grep: searchParams.get('grep') || undefined,
grepInvert: searchParams.get('grepInvert') || undefined,
projects: searchParams.getAll('project'),
workers: searchParams.get('workers') || undefined,
timeout: searchParams.has('timeout') ? +searchParams.get('timeout')! : undefined,
headed: searchParams.has('headed'),
reporters: searchParams.has('reporter') ? searchParams.getAll('reporter') : undefined,
};
export const UIModeView: React.FC<{}> = ({
}) => {
const [filterText, setFilterText] = React.useState<string>('');
@ -76,9 +91,6 @@ export const UIModeView: React.FC<{}> = ({
const inputRef = React.useRef<HTMLInputElement>(null);
const reloadTests = React.useCallback(() => {
const guid = new URLSearchParams(window.location.search).get('ws');
const wsURL = new URL(`../${guid}`, window.location.toString());
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
setTestServerConnection(new TestServerConnection(wsURL.toString()));
}, []);
@ -143,7 +155,7 @@ export const UIModeView: React.FC<{}> = ({
commandQueue.current = commandQueue.current.then(async () => {
setIsLoading(true);
try {
const result = await testServerConnection.listTests({});
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
teleSuiteUpdater.processListReport(result.report);
} catch (e) {
// eslint-disable-next-line no-console
@ -158,10 +170,10 @@ export const UIModeView: React.FC<{}> = ({
setIsLoading(true);
setWatchedTreeIds({ value: new Set() });
(async () => {
const status = await testServerConnection.runGlobalSetup();
const status = await testServerConnection.runGlobalSetup({});
if (status !== 'passed')
return;
const result = await testServerConnection.listTests({});
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
teleSuiteUpdater.processListReport(result.report);
testServerConnection.onListChanged(updateList);
@ -170,7 +182,7 @@ export const UIModeView: React.FC<{}> = ({
});
setIsLoading(false);
const { hasBrowsers } = await testServerConnection.checkBrowsers();
const { hasBrowsers } = await testServerConnection.checkBrowsers({});
setHasBrowsers(hasBrowsers);
})();
return () => {
@ -251,7 +263,18 @@ export const UIModeView: React.FC<{}> = ({
setProgress({ total: 0, passed: 0, failed: 0, skipped: 0 });
setRunningState({ testIds });
await testServerConnection.runTests({ testIds: [...testIds], projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p) });
await testServerConnection.runTests({
locations: queryParams.args,
grep: queryParams.grep,
grepInvert: queryParams.grepInvert,
testIds: [...testIds],
projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p),
workers: queryParams.workers,
timeout: queryParams.timeout,
headed: queryParams.headed,
reporters: queryParams.reporters,
trace: 'on',
});
// Clear pending tests in case of interrupt.
for (const test of testModel.rootSuite?.allTests() || []) {
if (test.results[0]?.duration === -1)
@ -298,7 +321,7 @@ export const UIModeView: React.FC<{}> = ({
const onShortcutEvent = (e: KeyboardEvent) => {
if (e.code === 'F6') {
e.preventDefault();
testServerConnection?.stopTestsNoReply();
testServerConnection?.stopTestsNoReply({});
} else if (e.code === 'F5') {
e.preventDefault();
reloadTests();
@ -325,9 +348,9 @@ export const UIModeView: React.FC<{}> = ({
const installBrowsers = React.useCallback((e: React.MouseEvent) => {
closeInstallDialog(e);
setIsShowingOutput(true);
testServerConnection?.installBrowsers().then(async () => {
testServerConnection?.installBrowsers({}).then(async () => {
setIsShowingOutput(false);
const { hasBrowsers } = await testServerConnection?.checkBrowsers();
const { hasBrowsers } = await testServerConnection?.checkBrowsers({});
setHasBrowsers(hasBrowsers);
});
}, [closeInstallDialog, testServerConnection]);
@ -390,7 +413,7 @@ export const UIModeView: React.FC<{}> = ({
<div>Running {progress.passed}/{runningState.testIds.size} passed ({(progress.passed / runningState.testIds.size) * 100 | 0}%)</div>
</div>}
<ToolbarButton icon='play' title='Run all' onClick={() => runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton>
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => testServerConnection?.stopTests()} disabled={!isRunningTest || isLoading}></ToolbarButton>
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => testServerConnection?.stopTests({})} disabled={!isRunningTest || isLoading}></ToolbarButton>
<ToolbarButton icon='eye' title='Watch all' toggled={watchAll} onClick={() => {
setWatchedTreeIds({ value: new Set() });
setWatchAll(!watchAll);