diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index aa6ef60271..f3ed7fa06c 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -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; }); diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts index 3a07fbcb31..b33d56ebde 100644 --- a/packages/playwright/src/isomorphic/testServerConnection.ts +++ b/packages/playwright/src/isomorphic/testServerConnection.ts @@ -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((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 { 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 { + async ping(params: Parameters[0]): ReturnType { await this._sendMessage('ping'); } - async pingNoReply() { + async pingNoReply(params: Parameters[0]) { this._sendMessageNoReply('ping'); } - async watch(params: { fileNames: string[]; }): Promise { + async watch(params: Parameters[0]): ReturnType { await this._sendMessage('watch', params); } - watchNoReply(params: { fileNames: string[]; }) { + watchNoReply(params: Parameters[0]) { this._sendMessageNoReply('watch', params); } - async open(params: { location: reporterTypes.Location; }): Promise { + async open(params: Parameters[0]): ReturnType { await this._sendMessage('open', params); } - openNoReply(params: { location: reporterTypes.Location; }) { + openNoReply(params: Parameters[0]) { this._sendMessageNoReply('open', params); } - async resizeTerminal(params: { cols: number; rows: number; }): Promise { + async resizeTerminal(params: Parameters[0]): ReturnType { await this._sendMessage('resizeTerminal', params); } - resizeTerminalNoReply(params: { cols: number; rows: number; }) { + resizeTerminalNoReply(params: Parameters[0]) { this._sendMessageNoReply('resizeTerminal', params); } - async checkBrowsers(): Promise<{ hasBrowsers: boolean; }> { + async checkBrowsers(params: Parameters[0]): ReturnType { return await this._sendMessage('checkBrowsers'); } - async installBrowsers(): Promise { + async installBrowsers(params: Parameters[0]): ReturnType { await this._sendMessage('installBrowsers'); } - async runGlobalSetup(): Promise<'passed' | 'failed' | 'timedout' | 'interrupted'> { + async runGlobalSetup(params: Parameters[0]): ReturnType { return await this._sendMessage('runGlobalSetup'); } - async runGlobalTeardown(): Promise<'passed' | 'failed' | 'timedout' | 'interrupted'> { + async runGlobalTeardown(params: Parameters[0]): ReturnType { 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[0]): ReturnType { + return await this._sendMessage('listFiles', params); } - async listTests(params: { reporter?: string | undefined; fileNames?: string[] | undefined; }): Promise<{ report: any[] }> { + async listTests(params: Parameters[0]): ReturnType { 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[0]): ReturnType { return await this._sendMessage('runTests', params); } - async findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[] | undefined; }> { + async findRelatedTestFiles(params: Parameters[0]): ReturnType { return await this._sendMessage('findRelatedTestFiles', params); } - async stopTests(): Promise { + async stopTests(params: Parameters[0]): ReturnType { await this._sendMessage('stopTests'); } - stopTestsNoReply() { + stopTestsNoReply(params: Parameters[0]) { this._sendMessageNoReply('stopTests'); } - async closeGracefully(): Promise { + async closeGracefully(params: Parameters[0]): ReturnType { await this._sendMessage('closeGracefully'); } } diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 77fdd6049c..d86d37d40e 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -18,7 +18,7 @@ import type * as reporterTypes from '../../types/testReporter'; import type { Event } from './events'; export interface TestServerInterface { - ping(): Promise; + ping(params: {}): Promise; watch(params: { fileNames: string[]; @@ -28,15 +28,17 @@ export interface TestServerInterface { resizeTerminal(params: { cols: number, rows: number }): Promise; - checkBrowsers(): Promise<{ hasBrowsers: boolean }>; + checkBrowsers(params: {}): Promise<{ hasBrowsers: boolean }>; - installBrowsers(): Promise; + installBrowsers(params: {}): Promise; - runGlobalSetup(): Promise; + runGlobalSetup(params: {}): Promise; - runGlobalTeardown(): Promise; + runGlobalTeardown(params: {}): Promise; - 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; + stopTests(params: {}): Promise; - closeGracefully(): Promise; + closeGracefully(params: {}): Promise; } export interface TestServerInterfaceEvents { diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index bb65d1982a..d16a47d4e6 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -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); } diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index d32e581c61..8f38346c83 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -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') { diff --git a/packages/playwright/src/reporters/merge.ts b/packages/playwright/src/reporters/merge.ts index ef34c409cc..57398ebc4e 100644 --- a/packages/playwright/src/reporters/merge.ts +++ b/packages/playwright/src/reporters/merge.ts @@ -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(); diff --git a/packages/playwright/src/runner/reporters.ts b/packages/playwright/src/runner/reporters.ts index 7e846c493f..3fc633f6e6 100644 --- a/packages/playwright/src/runner/reporters.ts +++ b/packages/playwright/src/runner/reporters.ts @@ -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 { +export async function createReporters(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean, descriptions?: ReporterDescription[]): Promise { 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 { +export async function createReporterForTestServer(config: FullConfigInternal, mode: 'list' | 'test', file: string, messageSink: (message: any) => void): Promise { 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, }; } diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts index 4a2d329159..e6ecf84d00 100644 --- a/packages/playwright/src/runner/runner.ts +++ b/packages/playwright/src/runner/runner.ts @@ -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 { + async listTestFiles(projectNames?: string[]): Promise { 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 { - const config = this._config; - webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); - return await runTestServer(config, options, async (server: HttpServer, cancelPromise: ManualPromise) => { - 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 { - 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 { const result = await this.loadAllTests(mode); if (result.status !== 'passed' || !result.suite) diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index b915834d24..33cc6e4f9e 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -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 { - 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, stop: ManualPromise } | undefined; @@ -98,9 +91,10 @@ class TestServerDispatcher implements TestServerInterface { private _queue = Promise.resolve(); private _globalCleanup: (() => Promise) | 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[0]): ReturnType { 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[0]): ReturnType { 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 { 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[0]): ReturnType { 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[0]): ReturnType { + let result: Awaited>; 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[0]): ReturnType { + 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(); const projectOutputs = new Set(); - 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[0]): ReturnType { + let result: Awaited>; 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 { + private async _innerRunTests(params: Parameters[0]): ReturnType { 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(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(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[0]): ReturnType { + 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 { + 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) => Promise): Promise { - const testServer = new TestServer(config); +export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise { + return await innerRunTestServer(configFile, options, async (server: HttpServer, cancelPromise: ManualPromise) => { + 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 { + 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) => Promise): Promise { + if (restartWithExperimentalTsEsm(undefined, true)) + return 'passed'; + const testServer = new TestServer(configFile); const cancelPromise = new ManualPromise(); const sigintWatcher = new SigIntWatcher(); void sigintWatcher.promise().then(() => cancelPromise.resolve()); diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 06bb83bc45..fe9d7005cb 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -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(''); @@ -76,9 +91,6 @@ export const UIModeView: React.FC<{}> = ({ const inputRef = React.useRef(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<{}> = ({
Running {progress.passed}/{runningState.testIds.size} passed ({(progress.passed / runningState.testIds.size) * 100 | 0}%)
} runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}> - testServerConnection?.stopTests()} disabled={!isRunningTest || isLoading}> + testServerConnection?.stopTests({})} disabled={!isRunningTest || isLoading}> { setWatchedTreeIds({ value: new Set() }); setWatchAll(!watchAll);