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

View File

@ -15,7 +15,6 @@
*/ */
import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface'; import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface';
import type * as reporterTypes from 'playwright/types/testReporter';
import * as events from './events'; import * as events from './events';
export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents { export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents {
@ -67,7 +66,7 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
this._connectedPromise = new Promise<void>((f, r) => { this._connectedPromise = new Promise<void>((f, r) => {
this._ws.addEventListener('open', () => { this._ws.addEventListener('open', () => {
f(); f();
this._ws.send(JSON.stringify({ method: 'ready' })); this._ws.send(JSON.stringify({ id: -1, method: 'ready' }));
}); });
this._ws.addEventListener('error', r); 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> { private async _sendMessage(method: string, params?: any): Promise<any> {
const logForTest = (globalThis as any).__logForTest; const logForTest = (globalThis as any).__logForTest;
logForTest?.({ method, params }); logForTest?.({ method, params });
@ -103,81 +106,83 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
this._onListChangedEmitter.fire(params); this._onListChangedEmitter.fire(params);
else if (method === 'testFilesChanged') else if (method === 'testFilesChanged')
this._onTestFilesChangedEmitter.fire(params); 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'); await this._sendMessage('ping');
} }
async pingNoReply() { async pingNoReply(params: Parameters<TestServerInterface['ping']>[0]) {
this._sendMessageNoReply('ping'); 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); await this._sendMessage('watch', params);
} }
watchNoReply(params: { fileNames: string[]; }) { watchNoReply(params: Parameters<TestServerInterface['watch']>[0]) {
this._sendMessageNoReply('watch', params); 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); await this._sendMessage('open', params);
} }
openNoReply(params: { location: reporterTypes.Location; }) { openNoReply(params: Parameters<TestServerInterface['open']>[0]) {
this._sendMessageNoReply('open', params); 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); await this._sendMessage('resizeTerminal', params);
} }
resizeTerminalNoReply(params: { cols: number; rows: number; }) { resizeTerminalNoReply(params: Parameters<TestServerInterface['resizeTerminal']>[0]) {
this._sendMessageNoReply('resizeTerminal', params); this._sendMessageNoReply('resizeTerminal', params);
} }
async checkBrowsers(): Promise<{ hasBrowsers: boolean; }> { async checkBrowsers(params: Parameters<TestServerInterface['checkBrowsers']>[0]): ReturnType<TestServerInterface['checkBrowsers']> {
return await this._sendMessage('checkBrowsers'); return await this._sendMessage('checkBrowsers');
} }
async installBrowsers(): Promise<void> { async installBrowsers(params: Parameters<TestServerInterface['installBrowsers']>[0]): ReturnType<TestServerInterface['installBrowsers']> {
await this._sendMessage('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'); 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'); 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; }> { async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
return await this._sendMessage('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); 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); 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); return await this._sendMessage('findRelatedTestFiles', params);
} }
async stopTests(): Promise<void> { async stopTests(params: Parameters<TestServerInterface['stopTests']>[0]): ReturnType<TestServerInterface['stopTests']> {
await this._sendMessage('stopTests'); await this._sendMessage('stopTests');
} }
stopTestsNoReply() { stopTestsNoReply(params: Parameters<TestServerInterface['stopTests']>[0]) {
this._sendMessageNoReply('stopTests'); this._sendMessageNoReply('stopTests');
} }
async closeGracefully(): Promise<void> { async closeGracefully(params: Parameters<TestServerInterface['closeGracefully']>[0]): ReturnType<TestServerInterface['closeGracefully']> {
await this._sendMessage('closeGracefully'); await this._sendMessage('closeGracefully');
} }
} }

View File

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

View File

@ -34,6 +34,7 @@ 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 * as testServer 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...]');
@ -151,7 +152,28 @@ Examples:
async function runTests(args: string[], opts: { [key: string]: any }) { async function runTests(args: string[], opts: { [key: string]: any }) {
await startProfiling(); 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) if (!config)
return; return;
@ -164,9 +186,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 (process.env.PWTEST_WATCH)
status = await runner.runUIMode({ host: opts.uiHost, port: opts.uiPort ? +opts.uiPort : undefined });
else if (process.env.PWTEST_WATCH)
status = await runner.watchAllTests(); status = await runner.watchAllTests();
else else
status = await runner.runAllTests(); status = await runner.runAllTests();
@ -176,14 +196,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
} }
async function runTestServer(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 host = opts.host || 'localhost';
const port = opts.port ? +opts.port : 0; 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); const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
gracefullyProcessExitDoNotHang(exitCode); gracefullyProcessExitDoNotHang(exitCode);
} }

View File

@ -51,7 +51,8 @@ type HtmlReporterOptions = {
host?: string, host?: string,
port?: number, port?: number,
attachmentsBaseURL?: string, attachmentsBaseURL?: string,
_mode?: string; _mode?: 'test' | 'list';
_isTestServer?: boolean;
}; };
class HtmlReporter extends EmptyReporter { class HtmlReporter extends EmptyReporter {
@ -67,6 +68,8 @@ class HtmlReporter extends EmptyReporter {
constructor(options: HtmlReporterOptions) { constructor(options: HtmlReporterOptions) {
super(); super();
this._options = options; this._options = options;
if (options._mode === 'test')
process.env.PW_HTML_REPORT = '1';
} }
override printsToStdio() { override printsToStdio() {
@ -125,7 +128,7 @@ class HtmlReporter extends EmptyReporter {
if (process.env.CI || !this._buildResult) if (process.env.CI || !this._buildResult)
return; return;
const { ok, singleTestId } = this._buildResult; 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) { if (shouldOpen) {
await showHTMLReport(this._outputFolder, this._options.host, this._options.port, singleTestId); await showHTMLReport(this._outputFolder, this._options.host, this._options.port, singleTestId);
} else if (this._options._mode === 'test') { } 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) { 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 multiplexer = new Multiplexer(reporters);
const stringPool = new StringInternPool(); const stringPool = new StringInternPool();

View File

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

View File

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

View File

@ -16,7 +16,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; 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 { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils';
import type { Transport, HttpServer } from 'playwright-core/lib/utils'; import type { Transport, HttpServer } from 'playwright-core/lib/utils';
import type * as reporterTypes from '../../types/testReporter'; import type * as reporterTypes from '../../types/testReporter';
@ -34,33 +34,26 @@ import type { TestServerInterface, TestServerInterfaceEventEmitters } from '../i
import { Runner } from './runner'; import { Runner } from './runner';
import { serializeError } from '../util'; import { serializeError } from '../util';
import { prepareErrorStack } from '../reporters/base'; 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 { class TestServer {
private _config: FullConfigInternal; private _configFile: string | undefined;
private _dispatcher: TestServerDispatcher | undefined; private _dispatcher: TestServerDispatcher | undefined;
private _originalStdoutWrite: NodeJS.WriteStream['write']; private _originalStdoutWrite: NodeJS.WriteStream['write'];
private _originalStderrWrite: NodeJS.WriteStream['write']; private _originalStderrWrite: NodeJS.WriteStream['write'];
constructor(config: FullConfigInternal) { constructor(configFile: string | undefined) {
this._config = config; this._configFile = configFile;
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 };
this._originalStdoutWrite = process.stdout.write; this._originalStdoutWrite = process.stdout.write;
this._originalStderrWrite = process.stderr.write; this._originalStderrWrite = process.stderr.write;
} }
async start(options: { host?: string, port?: number }): Promise<HttpServer> { 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 }); return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport });
} }
@ -90,7 +83,7 @@ class TestServer {
} }
class TestServerDispatcher implements TestServerInterface { class TestServerDispatcher implements TestServerInterface {
private _config: FullConfigInternal; private _configFile: string | undefined;
private _globalWatcher: Watcher; private _globalWatcher: Watcher;
private _testWatcher: Watcher; private _testWatcher: Watcher;
private _testRun: { run: Promise<reporterTypes.FullResult['status']>, stop: ManualPromise<void> } | undefined; private _testRun: { run: Promise<reporterTypes.FullResult['status']>, stop: ManualPromise<void> } | undefined;
@ -98,9 +91,10 @@ class TestServerDispatcher implements TestServerInterface {
private _queue = Promise.resolve(); private _queue = Promise.resolve();
private _globalCleanup: (() => Promise<reporterTypes.FullResult['status']>) | undefined; private _globalCleanup: (() => Promise<reporterTypes.FullResult['status']>) | undefined;
readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent']; readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent'];
private _plugins: TestRunnerPluginRegistration[] | undefined;
constructor(config: FullConfigInternal) { constructor(configFile: string | undefined) {
this._config = config; this._configFile = configFile;
this.transport = { this.transport = {
dispatch: (method, params) => (this as any)[method](params), dispatch: (method, params) => (this as any)[method](params),
onclose: () => {}, onclose: () => {},
@ -114,16 +108,18 @@ class TestServerDispatcher implements TestServerInterface {
this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params); this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params);
} }
async ready() {}
async ping() {} async ping() {}
async open(params: { location: reporterTypes.Location }) { async open(params: Parameters<TestServerInterface['open']>[0]): ReturnType<TestServerInterface['open']> {
if (isUnderTest()) if (isUnderTest())
return; return;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
open('vscode://file/' + params.location.file + ':' + params.location.line).catch(e => console.error(e)); 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.columns = params.cols;
process.stdout.rows = params.rows; process.stdout.rows = params.rows;
process.stderr.columns = params.cols; process.stderr.columns = params.cols;
@ -141,10 +137,13 @@ class TestServerDispatcher implements TestServerInterface {
async runGlobalSetup(): Promise<reporterTypes.FullResult['status']> { async runGlobalSetup(): Promise<reporterTypes.FullResult['status']> {
await this.runGlobalTeardown(); 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 reporter = new InternalReporter(new ListReporter());
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter); const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
reporter.onConfigure(this._config.config); reporter.onConfigure(config.config);
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(config, reporter);
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0); const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
await reporter.onEnd({ status }); await reporter.onEnd({ status });
await reporter.onExit(); await reporter.onExit();
@ -162,10 +161,11 @@ class TestServerDispatcher implements TestServerInterface {
return result; return result;
} }
async listFiles() { async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
try { try {
const runner = new Runner(this._config); const config = await this._loadConfig(this._configFile);
return runner.listTestFiles(); const runner = new Runner(config);
return runner.listTestFiles(params.projects);
} catch (e) { } catch (e) {
const error: reporterTypes.TestError = serializeError(e); const error: reporterTypes.TestError = serializeError(e);
error.location = prepareErrorStack(e.stack).location; error.location = prepareErrorStack(e.stack).location;
@ -173,82 +173,109 @@ class TestServerDispatcher implements TestServerInterface {
} }
} }
async listTests(params: { reporter?: string; fileNames: string[]; }) { async listTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> {
let report: any[] = []; let result: Awaited<ReturnType<TestServerInterface['listTests']>>;
this._queue = this._queue.then(async () => { this._queue = this._queue.then(async () => {
report = await this._innerListTests(params); result = await this._innerListTests(params);
}).catch(printInternalError); }).catch(printInternalError);
await this._queue; 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 report: any[] = [];
const wireReporter = await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => report.push(e));
const reporter = new InternalReporter(wireReporter); const reporter = new InternalReporter(wireReporter);
this._config.cliArgs = params.fileNames || [];
this._config.cliListOnly = true; const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false });
this._config.testIdMatcher = undefined; const testRun = new TestRun(config, reporter);
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false }); reporter.onConfigure(config.config);
const testRun = new TestRun(this._config, reporter);
reporter.onConfigure(this._config.config);
const status = await taskRunner.run(testRun, 0); const status = await taskRunner.run(testRun, 0);
await reporter.onEnd({ status }); await reporter.onEnd({ status });
await reporter.onExit(); await reporter.onExit();
const projectDirs = new Set<string>(); const projectDirs = new Set<string>();
const projectOutputs = 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); projectDirs.add(p.project.testDir);
projectOutputs.add(p.project.outputDir); projectOutputs.add(p.project.outputDir);
} }
const result = await resolveCtDirs(this._config); const result = await resolveCtDirs(config);
if (result) { if (result) {
projectDirs.add(result.templateDir); projectDirs.add(result.templateDir);
projectOutputs.add(result.outDir); projectOutputs.add(result.outDir);
} }
this._globalWatcher.update([...projectDirs], [...projectOutputs], false); 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; }) { async runTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
let status: reporterTypes.FullResult['status']; let result: Awaited<ReturnType<TestServerInterface['runTests']>>;
this._queue = this._queue.then(async () => { 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; 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(); 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; const testIdSet = params.testIds ? new Set<string>(params.testIds) : null;
this._config.cliArgs = locations ? locations : []; const config = await this._loadConfig(this._configFile, overrides);
this._config.cliGrep = grep; config.cliListOnly = false;
this._config.cliListOnly = false; config.cliPassWithNoTests = true;
this._config.cliProjectFilter = projects?.length ? projects : undefined; config.cliArgs = params.locations || [];
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); 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'); const reporters = await createReporters(config, 'test', true);
reporters.push(await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => this._dispatchEvent('report', e))); reporters.push(await createReporterForTestServer(config, 'test', params.serializer || require.resolve('./uiModeReporter'), e => this._dispatchEvent('report', e)));
const reporter = new InternalReporter(new Multiplexer(reporters)); const reporter = new InternalReporter(new Multiplexer(reporters));
const taskRunner = createTaskRunnerForWatch(this._config, reporter); const taskRunner = createTaskRunnerForWatch(config, reporter);
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(config, reporter);
reporter.onConfigure(this._config.config); reporter.onConfigure(config.config);
const stop = new ManualPromise(); const stop = new ManualPromise();
const run = taskRunner.run(testRun, 0, stop).then(async status => { const run = taskRunner.run(testRun, 0, stop).then(async status => {
await reporter.onEnd({ status }); await reporter.onEnd({ status });
await reporter.onExit(); await reporter.onExit();
this._testRun = undefined; this._testRun = undefined;
this._config.testIdMatcher = undefined;
return status; return status;
}); });
this._testRun = { run, stop }; this._testRun = { run, stop };
return await run; const status = await run;
return { status };
} }
async watch(params: { fileNames: string[]; }) { async watch(params: { fileNames: string[]; }) {
@ -260,8 +287,9 @@ class TestServerDispatcher implements TestServerInterface {
this._testWatcher.update([...files], [], true); this._testWatcher.update([...files], [], true);
} }
findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[] | undefined; }> { async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> {
const runner = new Runner(this._config); const config = await this._loadConfig(this._configFile);
const runner = new Runner(config);
return runner.findRelatedTestFiles('out-of-process', params.files); return runner.findRelatedTestFiles('out-of-process', params.files);
} }
@ -273,10 +301,49 @@ class TestServerDispatcher implements TestServerInterface {
async closeGracefully() { async closeGracefully() {
gracefullyProcessExitDoNotHang(0); 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']> { export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status']> {
const testServer = new TestServer(config); 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 cancelPromise = new ManualPromise<void>();
const sigintWatcher = new SigIntWatcher(); const sigintWatcher = new SigIntWatcher();
void sigintWatcher.promise().then(() => cancelPromise.resolve()); void sigintWatcher.promise().then(() => cancelPromise.resolve());

View File

@ -48,6 +48,21 @@ const xtermDataSource: XtermDataSource = {
resize: () => {}, 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<{}> = ({ export const UIModeView: React.FC<{}> = ({
}) => { }) => {
const [filterText, setFilterText] = React.useState<string>(''); const [filterText, setFilterText] = React.useState<string>('');
@ -76,9 +91,6 @@ export const UIModeView: React.FC<{}> = ({
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const reloadTests = React.useCallback(() => { 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())); setTestServerConnection(new TestServerConnection(wsURL.toString()));
}, []); }, []);
@ -143,7 +155,7 @@ export const UIModeView: React.FC<{}> = ({
commandQueue.current = commandQueue.current.then(async () => { commandQueue.current = commandQueue.current.then(async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const result = await testServerConnection.listTests({}); const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
teleSuiteUpdater.processListReport(result.report); teleSuiteUpdater.processListReport(result.report);
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -158,10 +170,10 @@ export const UIModeView: React.FC<{}> = ({
setIsLoading(true); setIsLoading(true);
setWatchedTreeIds({ value: new Set() }); setWatchedTreeIds({ value: new Set() });
(async () => { (async () => {
const status = await testServerConnection.runGlobalSetup(); const status = await testServerConnection.runGlobalSetup({});
if (status !== 'passed') if (status !== 'passed')
return; return;
const result = await testServerConnection.listTests({}); const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
teleSuiteUpdater.processListReport(result.report); teleSuiteUpdater.processListReport(result.report);
testServerConnection.onListChanged(updateList); testServerConnection.onListChanged(updateList);
@ -170,7 +182,7 @@ export const UIModeView: React.FC<{}> = ({
}); });
setIsLoading(false); setIsLoading(false);
const { hasBrowsers } = await testServerConnection.checkBrowsers(); const { hasBrowsers } = await testServerConnection.checkBrowsers({});
setHasBrowsers(hasBrowsers); setHasBrowsers(hasBrowsers);
})(); })();
return () => { return () => {
@ -251,7 +263,18 @@ export const UIModeView: React.FC<{}> = ({
setProgress({ total: 0, passed: 0, failed: 0, skipped: 0 }); setProgress({ total: 0, passed: 0, failed: 0, skipped: 0 });
setRunningState({ testIds }); 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. // Clear pending tests in case of interrupt.
for (const test of testModel.rootSuite?.allTests() || []) { for (const test of testModel.rootSuite?.allTests() || []) {
if (test.results[0]?.duration === -1) if (test.results[0]?.duration === -1)
@ -298,7 +321,7 @@ export const UIModeView: React.FC<{}> = ({
const onShortcutEvent = (e: KeyboardEvent) => { const onShortcutEvent = (e: KeyboardEvent) => {
if (e.code === 'F6') { if (e.code === 'F6') {
e.preventDefault(); e.preventDefault();
testServerConnection?.stopTestsNoReply(); testServerConnection?.stopTestsNoReply({});
} else if (e.code === 'F5') { } else if (e.code === 'F5') {
e.preventDefault(); e.preventDefault();
reloadTests(); reloadTests();
@ -325,9 +348,9 @@ export const UIModeView: React.FC<{}> = ({
const installBrowsers = React.useCallback((e: React.MouseEvent) => { const installBrowsers = React.useCallback((e: React.MouseEvent) => {
closeInstallDialog(e); closeInstallDialog(e);
setIsShowingOutput(true); setIsShowingOutput(true);
testServerConnection?.installBrowsers().then(async () => { testServerConnection?.installBrowsers({}).then(async () => {
setIsShowingOutput(false); setIsShowingOutput(false);
const { hasBrowsers } = await testServerConnection?.checkBrowsers(); const { hasBrowsers } = await testServerConnection?.checkBrowsers({});
setHasBrowsers(hasBrowsers); setHasBrowsers(hasBrowsers);
}); });
}, [closeInstallDialog, testServerConnection]); }, [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>Running {progress.passed}/{runningState.testIds.size} passed ({(progress.passed / runningState.testIds.size) * 100 | 0}%)</div>
</div>} </div>}
<ToolbarButton icon='play' title='Run all' onClick={() => runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton> <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={() => { <ToolbarButton icon='eye' title='Watch all' toggled={watchAll} onClick={() => {
setWatchedTreeIds({ value: new Set() }); setWatchedTreeIds({ value: new Set() });
setWatchAll(!watchAll); setWatchAll(!watchAll);