chore: follow up to the api review for test server (#30097)

This commit is contained in:
Pavel Feldman 2024-03-25 15:42:58 -07:00 committed by GitHub
parent a849ea9741
commit 7039ad0d11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 82 deletions

View File

@ -110,6 +110,10 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
this._onLoadTraceRequestedEmitter.fire(params); this._onLoadTraceRequestedEmitter.fire(params);
} }
async setSerializer(params: { serializer: string; }): Promise<void> {
await this._sendMessage('setSerializer', params);
}
async ping(params: Parameters<TestServerInterface['ping']>[0]): ReturnType<TestServerInterface['ping']> { async ping(params: Parameters<TestServerInterface['ping']>[0]): ReturnType<TestServerInterface['ping']> {
await this._sendMessage('ping'); await this._sendMessage('ping');
} }
@ -126,6 +130,10 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
this._sendMessageNoReply('watch', params); this._sendMessageNoReply('watch', params);
} }
async watchTestDir(params: Parameters<TestServerInterface['watchTestDir']>[0]): ReturnType<TestServerInterface['watchTestDir']> {
await this._sendMessage('watchTestDir', params);
}
async open(params: Parameters<TestServerInterface['open']>[0]): ReturnType<TestServerInterface['open']> { async open(params: Parameters<TestServerInterface['open']>[0]): ReturnType<TestServerInterface['open']> {
await this._sendMessage('open', params); await this._sendMessage('open', params);
} }

View File

@ -16,14 +16,21 @@
import type * as reporterTypes from '../../types/testReporter'; import type * as reporterTypes from '../../types/testReporter';
import type { Event } from './events'; import type { Event } from './events';
import type { JsonEvent } from './teleReceiver';
export type ReportEntry = JsonEvent;
export interface TestServerInterface { export interface TestServerInterface {
setSerializer(params: { serializer: string }): Promise<void>;
ping(params: {}): Promise<void>; ping(params: {}): Promise<void>;
watch(params: { watch(params: {
fileNames: string[]; fileNames: string[];
}): Promise<void>; }): Promise<void>;
watchTestDir(params: {}): Promise<void>;
open(params: { location: reporterTypes.Location }): Promise<void>; open(params: { location: reporterTypes.Location }): Promise<void>;
resizeTerminal(params: { cols: number, rows: number }): Promise<void>; resizeTerminal(params: { cols: number, rows: number }): Promise<void>;
@ -32,34 +39,35 @@ export interface TestServerInterface {
installBrowsers(params: {}): Promise<void>; installBrowsers(params: {}): Promise<void>;
runGlobalSetup(params: {}): Promise<reporterTypes.FullResult['status']>; runGlobalSetup(params: {}): Promise<{
report: ReportEntry[],
status: reporterTypes.FullResult['status']
}>;
runGlobalTeardown(params: {}): Promise<reporterTypes.FullResult['status']>; runGlobalTeardown(params: {}): Promise<{
report: ReportEntry[],
status: reporterTypes.FullResult['status']
}>;
listFiles(params: { listFiles(params: {
projects?: string[]; projects?: string[];
}): Promise<{ }): Promise<{
projects: { report: ReportEntry[];
name: string; status: reporterTypes.FullResult['status']
testDir: string;
use: { testIdAttribute?: string };
files: string[];
}[];
cliEntryPoint?: string;
error?: reporterTypes.TestError;
}>; }>;
/** /**
* Returns list of teleReporter events. * Returns list of teleReporter events.
*/ */
listTests(params: { listTests(params: {
serializer?: string;
projects?: string[]; projects?: string[];
locations?: string[]; locations?: string[];
}): Promise<{ report: any[] }>; }): Promise<{
report: ReportEntry[],
status: reporterTypes.FullResult['status']
}>;
runTests(params: { runTests(params: {
serializer?: string;
locations?: string[]; locations?: string[];
grep?: string; grep?: string;
grepInvert?: string; grepInvert?: string;
@ -72,7 +80,9 @@ export interface TestServerInterface {
projects?: string[]; projects?: string[];
reuseContext?: boolean; reuseContext?: boolean;
connectWsEndpoint?: string; connectWsEndpoint?: string;
}): Promise<{ status: reporterTypes.FullResult['status'] }>; }): Promise<{
status: reporterTypes.FullResult['status'];
}>;
findRelatedTestFiles(params: { findRelatedTestFiles(params: {
files: string[]; files: string[];
@ -84,7 +94,6 @@ export interface TestServerInterface {
} }
export interface TestServerInterfaceEvents { export interface TestServerInterfaceEvents {
onClose: Event<void>;
onReport: Event<any>; onReport: Event<any>;
onStdio: Event<{ type: 'stdout' | 'stderr', text?: string, buffer?: string }>; onStdio: Event<{ type: 'stdout' | 'stderr', text?: string, buffer?: string }>;
onListChanged: Event<void>; onListChanged: Event<void>;
@ -93,8 +102,7 @@ export interface TestServerInterfaceEvents {
} }
export interface TestServerInterfaceEventEmitters { export interface TestServerInterfaceEventEmitters {
dispatchEvent(event: 'close', params: {}): void; dispatchEvent(event: 'report', params: ReportEntry): void;
dispatchEvent(event: 'report', params: any): void;
dispatchEvent(event: 'stdio', params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }): void; dispatchEvent(event: 'stdio', params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }): void;
dispatchEvent(event: 'listChanged', params: {}): void; dispatchEvent(event: 'listChanged', params: {}): void;
dispatchEvent(event: 'testFilesChanged', params: { testFiles: string[] }): void; dispatchEvent(event: 'testFilesChanged', params: { testFiles: string[] }): void;

View File

@ -32,7 +32,7 @@ import { dependenciesForTestFile } from '../transform/compilationCache';
import { sourceMapSupport } from '../utilsBundle'; import { sourceMapSupport } from '../utilsBundle';
import type { RawSourceMap } from 'source-map'; import type { RawSourceMap } from 'source-map';
export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean, additionalFileMatcher: Matcher | undefined) { export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean, additionalFileMatcher?: Matcher) {
const config = testRun.config; const config = testRun.config;
const fsCache = new Map(); const fsCache = new Map();
const sourceMapCache = new Map(); const sourceMapCache = new Map();
@ -60,7 +60,6 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest
return hasMatchingSources; return hasMatchingSources;
}); });
const filteredFiles = matchedFiles.filter(Boolean) as string[]; const filteredFiles = matchedFiles.filter(Boolean) as string[];
if (filteredFiles.length)
filesToRunByProject.set(project, filteredFiles); filesToRunByProject.set(project, filteredFiles);
} }
@ -316,7 +315,7 @@ export function loadGlobalHook(config: FullConfigInternal, file: string): Promis
return requireOrImportDefaultFunction(path.resolve(config.config.rootDir, file), false); return requireOrImportDefaultFunction(path.resolve(config.config.rootDir, file), false);
} }
export function loadReporter(config: FullConfigInternal, file: string): Promise<new (arg?: any) => Reporter> { export function loadReporter(config: FullConfigInternal | null, file: string): Promise<new (arg?: any) => Reporter> {
return requireOrImportDefaultFunction(config ? path.resolve(config.config.rootDir, file) : file, true); return requireOrImportDefaultFunction(config ? path.resolve(config.config.rootDir, file) : file, true);
} }

View File

@ -78,17 +78,16 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
return reporters; return reporters;
} }
export async function createReporterForTestServer(config: FullConfigInternal, mode: 'list' | 'test', file: string, messageSink: (message: any) => void): Promise<ReporterV2> { export async function createReporterForTestServer(file: string, messageSink: (message: any) => void): Promise<ReporterV2> {
const reporterConstructor = await loadReporter(config, file); const reporterConstructor = await loadReporter(null, file);
const runOptions = reporterOptions(config, mode, true, messageSink); return wrapReporterAsV2(new reporterConstructor({
const instance = new reporterConstructor(runOptions); _send: messageSink,
return wrapReporterAsV2(instance); }));
} }
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean, send?: (message: any) => void) { function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean) {
return { return {
configDir: config.configDir, configDir: config.configDir,
_send: send,
_mode: mode, _mode: mode,
_isTestServer: isTestServer, _isTestServer: isTestServer,
}; };

View File

@ -40,7 +40,6 @@ type ProjectConfigWithFiles = {
type ConfigListFilesReport = { type ConfigListFilesReport = {
projects: ProjectConfigWithFiles[]; projects: ProjectConfigWithFiles[];
cliEntryPoint?: string;
error?: TestError; error?: TestError;
}; };
@ -57,11 +56,9 @@ export class Runner {
} }
async listTestFiles(projectNames?: string[]): Promise<ConfigListFilesReport> { async listTestFiles(projectNames?: string[]): Promise<ConfigListFilesReport> {
const frameworkPackage = (this._config.config as any)['@playwright/test']?.['packageJSON'];
const projects = filterProjects(this._config.projects, projectNames); 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,
}; };
for (const project of projects) { for (const project of projects) {
report.projects.push({ report.projects.push({

View File

@ -28,7 +28,7 @@ import { TaskRunner } from './taskRunner';
import type { FullConfigInternal, FullProjectInternal } from '../common/config'; import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils'; import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
import type { Matcher } from '../util'; import type { Matcher } from '../util';
import type { Suite } from '../common/test'; import { Suite } from '../common/test';
import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils'; import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils';
import { FailureTracker } from './failureTracker'; import { FailureTracker } from './failureTracker';
@ -116,6 +116,13 @@ export function createTaskRunnerForList(config: FullConfigInternal, reporter: Re
return taskRunner; return taskRunner;
} }
export function createTaskRunnerForListFiles(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
taskRunner.addTask('load tests', createListFilesTask());
taskRunner.addTask('report begin', createReportBeginTask());
return taskRunner;
}
function createReportBeginTask(): Task<TestRun> { function createReportBeginTask(): Task<TestRun> {
return { return {
setup: async ({ reporter, rootSuite }) => { setup: async ({ reporter, rootSuite }) => {
@ -195,6 +202,29 @@ function createRemoveOutputDirsTask(): Task<TestRun> {
}; };
} }
function createListFilesTask(): Task<TestRun> {
return {
setup: async (testRun, errors) => {
testRun.rootSuite = await createRootSuite(testRun, errors, false);
testRun.failureTracker.onRootSuite(testRun.rootSuite);
await collectProjectsAndTestFiles(testRun, false);
for (const [project, files] of testRun.projectFiles) {
const projectSuite = new Suite(project.project.name, 'project');
projectSuite._fullProject = project;
testRun.rootSuite._addSuite(projectSuite);
const suites = files.map(file => {
const title = path.relative(testRun.config.config.rootDir, file);
const suite = new Suite(title, 'file');
suite.location = { file, line: 0, column: 0 };
projectSuite._addSuite(suite);
return suite;
});
testRun.projectSuites.set(project, suites);
}
},
};
}
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> { function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
return { return {
setup: async (testRun, errors, softErrors) => { setup: async (testRun, errors, softErrors) => {

View File

@ -24,21 +24,20 @@ import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/
import type { FullConfigInternal } from '../common/config'; import type { FullConfigInternal } from '../common/config';
import { InternalReporter } from '../reporters/internalReporter'; import { InternalReporter } from '../reporters/internalReporter';
import { createReporterForTestServer, createReporters } from './reporters'; import { createReporterForTestServer, createReporters } from './reporters';
import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer, createTaskRunnerForWatchSetup } from './tasks'; import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer, createTaskRunnerForWatchSetup, createTaskRunnerForListFiles } from './tasks';
import { open } from 'playwright-core/lib/utilsBundle'; import { open } from 'playwright-core/lib/utilsBundle';
import ListReporter from '../reporters/list'; import ListReporter from '../reporters/list';
import { Multiplexer } from '../reporters/multiplexer'; import { Multiplexer } from '../reporters/multiplexer';
import { SigIntWatcher } from './sigIntWatcher'; import { SigIntWatcher } from './sigIntWatcher';
import { Watcher } from '../fsWatcher'; import { Watcher } from '../fsWatcher';
import type { TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface'; import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface';
import { Runner } from './runner'; import { Runner } from './runner';
import { serializeError } from '../util';
import { prepareErrorStack } from '../reporters/base';
import type { ConfigCLIOverrides } from '../common/ipc'; import type { ConfigCLIOverrides } from '../common/ipc';
import { loadConfig, resolveConfigFile, restartWithExperimentalTsEsm } from '../common/configLoader'; import { loadConfig, resolveConfigFile, restartWithExperimentalTsEsm } from '../common/configLoader';
import { webServerPluginsForConfig } from '../plugins/webServerPlugin'; import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playwright-core/lib/server/trace/viewer/traceViewer'; import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playwright-core/lib/server/trace/viewer/traceViewer';
import type { TestRunnerPluginRegistration } from '../plugins'; import type { TestRunnerPluginRegistration } from '../plugins';
import { serializeError } from '../util';
class TestServer { class TestServer {
private _configFile: string | undefined; private _configFile: string | undefined;
@ -89,9 +88,11 @@ class TestServerDispatcher implements TestServerInterface {
private _testRun: { run: Promise<reporterTypes.FullResult['status']>, stop: ManualPromise<void> } | undefined; private _testRun: { run: Promise<reporterTypes.FullResult['status']>, stop: ManualPromise<void> } | undefined;
readonly transport: Transport; readonly transport: Transport;
private _queue = Promise.resolve(); private _queue = Promise.resolve();
private _globalCleanup: (() => Promise<reporterTypes.FullResult['status']>) | undefined; private _globalSetup: { cleanup: () => Promise<any>, report: ReportEntry[] } | undefined;
readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent']; readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent'];
private _plugins: TestRunnerPluginRegistration[] | undefined; private _plugins: TestRunnerPluginRegistration[] | undefined;
private _serializer = require.resolve('./uiModeReporter');
private _watchTestDir = false;
constructor(configFile: string | undefined) { constructor(configFile: string | undefined) {
this._configFile = configFile; this._configFile = configFile;
@ -108,6 +109,21 @@ class TestServerDispatcher implements TestServerInterface {
this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params); this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params);
} }
async setSerializer(params: { serializer: string; }): Promise<void> {
this._serializer = params.serializer;
}
private async _wireReporter(messageSink: (message: any) => void) {
return await createReporterForTestServer(this._serializer, messageSink);
}
private async _collectingReporter() {
const report: ReportEntry[] = [];
const wireReporter = await createReporterForTestServer(this._serializer, e => report.push(e));
const reporter = new InternalReporter(wireReporter);
return { reporter, report };
}
async ready() {} async ready() {}
async ping() {} async ping() {}
@ -134,14 +150,19 @@ class TestServerDispatcher implements TestServerInterface {
await installBrowsers(); await installBrowsers();
} }
async runGlobalSetup(): Promise<reporterTypes.FullResult['status']> { async runGlobalSetup(params: Parameters<TestServerInterface['runGlobalSetup']>[0]): ReturnType<TestServerInterface['runGlobalSetup']> {
await this.runGlobalTeardown(); await this.runGlobalTeardown();
const config = await this._loadConfig(this._configFile); const { reporter, report } = await this._collectingReporter();
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); const { config, error } = await this._loadConfig(this._configFile);
if (!config) {
reporter.onError(error!);
return { status: 'failed', report };
}
const reporter = new InternalReporter(new ListReporter()); webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
const taskRunner = createTaskRunnerForWatchSetup(config, reporter); const listReporter = new InternalReporter(new ListReporter());
const taskRunner = createTaskRunnerForWatchSetup(config, new Multiplexer([reporter, listReporter]));
reporter.onConfigure(config.config); reporter.onConfigure(config.config);
const testRun = new TestRun(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);
@ -149,28 +170,35 @@ class TestServerDispatcher implements TestServerInterface {
await reporter.onExit(); await reporter.onExit();
if (status !== 'passed') { if (status !== 'passed') {
await globalCleanup(); await globalCleanup();
return status; return { report, status };
} }
this._globalCleanup = globalCleanup; this._globalSetup = { cleanup: globalCleanup, report };
return status; return { report, status };
} }
async runGlobalTeardown() { async runGlobalTeardown() {
const result = (await this._globalCleanup?.()) || 'passed'; const globalSetup = this._globalSetup;
this._globalCleanup = undefined; const status = await globalSetup?.cleanup();
return result; this._globalSetup = undefined;
return { status, report: globalSetup?.report || [] };
} }
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> { async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
try { const { reporter, report } = await this._collectingReporter();
const config = await this._loadConfig(this._configFile); const { config, error } = await this._loadConfig(this._configFile);
const runner = new Runner(config); if (!config) {
return runner.listTestFiles(params.projects); reporter.onError(error!);
} catch (e) { return { status: 'failed', report };
const error: reporterTypes.TestError = serializeError(e);
error.location = prepareErrorStack(e.stack).location;
return { projects: [], error };
} }
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
const taskRunner = createTaskRunnerForListFiles(config, reporter);
reporter.onConfigure(config.config);
const testRun = new TestRun(config, reporter);
const status = await taskRunner.run(testRun, 0);
await reporter.onEnd({ status });
await reporter.onExit();
return { report, status };
} }
async listTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> { async listTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> {
@ -187,15 +215,17 @@ class TestServerDispatcher implements TestServerInterface {
repeatEach: 1, repeatEach: 1,
retries: 0, retries: 0,
}; };
const config = await this._loadConfig(this._configFile, overrides); const { reporter, report } = await this._collectingReporter();
const { config, error } = await this._loadConfig(this._configFile, overrides);
if (!config) {
reporter.onError(error!);
return { report: [], status: 'failed' };
}
config.cliArgs = params.locations || []; config.cliArgs = params.locations || [];
config.cliProjectFilter = params.projects?.length ? params.projects : undefined; config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
config.cliListOnly = true; config.cliListOnly = true;
const wireReporter = await createReporterForTestServer(config, 'list', params.serializer || require.resolve('./uiModeReporter'), e => report.push(e));
const report: any[] = [];
const reporter = new InternalReporter(wireReporter);
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false }); const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false });
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);
reporter.onConfigure(config.config); reporter.onConfigure(config.config);
@ -216,17 +246,18 @@ class TestServerDispatcher implements TestServerInterface {
projectOutputs.add(result.outDir); projectOutputs.add(result.outDir);
} }
if (this._watchTestDir)
this._globalWatcher.update([...projectDirs], [...projectOutputs], false); this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
return { report }; return { report, status };
} }
async runTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> { async runTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
let result: Awaited<ReturnType<TestServerInterface['runTests']>>; let result: Awaited<ReturnType<TestServerInterface['runTests']>> = { status: 'passed' };
this._queue = this._queue.then(async () => { this._queue = this._queue.then(async () => {
result = await this._innerRunTests(params).catch(printInternalError) || { status: 'failed' }; result = await this._innerRunTests(params).catch(e => { printInternalError(e); return { status: 'failed' }; });
}); });
await this._queue; await this._queue;
return result!; return result;
} }
private async _innerRunTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> { private async _innerRunTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
@ -250,8 +281,14 @@ class TestServerDispatcher implements TestServerInterface {
else else
process.env.PW_LIVE_TRACE_STACKS = undefined; process.env.PW_LIVE_TRACE_STACKS = undefined;
const { config, error } = await this._loadConfig(this._configFile, overrides);
if (!config) {
const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e));
wireReporter.onError(error!);
return { status: 'failed' };
}
const testIdSet = params.testIds ? new Set<string>(params.testIds) : null; const testIdSet = params.testIds ? new Set<string>(params.testIds) : null;
const config = await this._loadConfig(this._configFile, overrides);
config.cliListOnly = false; config.cliListOnly = false;
config.cliPassWithNoTests = true; config.cliPassWithNoTests = true;
config.cliArgs = params.locations || []; config.cliArgs = params.locations || [];
@ -261,7 +298,8 @@ class TestServerDispatcher implements TestServerInterface {
config.testIdMatcher = testIdSet ? id => testIdSet.has(id) : undefined; config.testIdMatcher = testIdSet ? id => testIdSet.has(id) : undefined;
const reporters = await createReporters(config, 'test', true); const reporters = await createReporters(config, 'test', true);
reporters.push(await createReporterForTestServer(config, 'test', params.serializer || require.resolve('./uiModeReporter'), e => this._dispatchEvent('report', e))); const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e));
reporters.push(wireReporter);
const reporter = new InternalReporter(new Multiplexer(reporters)); const reporter = new InternalReporter(new Multiplexer(reporters));
const taskRunner = createTaskRunnerForTestServer(config, reporter); const taskRunner = createTaskRunnerForTestServer(config, reporter);
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);
@ -274,8 +312,11 @@ class TestServerDispatcher implements TestServerInterface {
return status; return status;
}); });
this._testRun = { run, stop }; this._testRun = { run, stop };
const status = await run; return { status: await run };
return { status }; }
async watchTestDir() {
this._watchTestDir = true;
} }
async watch(params: { fileNames: string[]; }) { async watch(params: { fileNames: string[]; }) {
@ -288,8 +329,10 @@ class TestServerDispatcher implements TestServerInterface {
} }
async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> { async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> {
const config = await this._loadConfig(this._configFile); const { config, error } = await this._loadConfig(this._configFile);
const runner = new Runner(config); if (error)
return { testFiles: [], errors: [error] };
const runner = new Runner(config!);
return runner.findRelatedTestFiles('out-of-process', params.files); return runner.findRelatedTestFiles('out-of-process', params.files);
} }
@ -302,17 +345,20 @@ class TestServerDispatcher implements TestServerInterface {
gracefullyProcessExitDoNotHang(0); gracefullyProcessExitDoNotHang(0);
} }
private async _loadConfig(configFile: string | undefined, overrides?: ConfigCLIOverrides): Promise<FullConfigInternal> { private async _loadConfig(configFile: string | undefined, overrides?: ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> {
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd(); const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
try {
const config = await loadConfig({ resolvedConfigFile, configDir: resolvedConfigFile === configFileOrDirectory ? path.dirname(resolvedConfigFile) : configFileOrDirectory }, overrides); const config = await loadConfig({ resolvedConfigFile, configDir: resolvedConfigFile === configFileOrDirectory ? path.dirname(resolvedConfigFile) : configFileOrDirectory }, overrides);
// Preserve plugin instances between setup and build. // Preserve plugin instances between setup and build.
if (!this._plugins) if (!this._plugins)
this._plugins = config.plugins || []; this._plugins = config.plugins || [];
else else
config.plugins.splice(0, config.plugins.length, ...this._plugins); config.plugins.splice(0, config.plugins.length, ...this._plugins);
return config; return { config };
} catch (e) {
return { config: null, error: serializeError(e) };
}
} }
} }

View File

@ -172,7 +172,8 @@ export const UIModeView: React.FC<{}> = ({
setIsLoading(true); setIsLoading(true);
setWatchedTreeIds({ value: new Set() }); setWatchedTreeIds({ value: new Set() });
(async () => { (async () => {
const status = await testServerConnection.runGlobalSetup({}); await testServerConnection.watchTestDir({});
const { status } = await testServerConnection.runGlobalSetup({});
if (status !== 'passed') if (status !== 'passed')
return; return;
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args }); const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });