mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: send test params over the wire in ui mode (#30046)
This commit is contained in:
parent
c8e8d8f8bb
commit
ee9432b9da
@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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') {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user