chore: prepare to reuse test server from ui mode (6) (#30008)

This commit is contained in:
Pavel Feldman 2024-03-20 13:43:26 -07:00 committed by GitHub
parent 2bd3e104ab
commit 48ccc9cbcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 77 additions and 62 deletions

View file

@ -135,7 +135,6 @@ export class TeleReporterReceiver {
private _reporter: Partial<ReporterV2>; private _reporter: Partial<ReporterV2>;
private _tests = new Map<string, TeleTestCase>(); private _tests = new Map<string, TeleTestCase>();
private _rootDir!: string; private _rootDir!: string;
private _listOnly = false;
private _config!: reporterTypes.FullConfig; private _config!: reporterTypes.FullConfig;
constructor(reporter: Partial<ReporterV2>, options: TeleReporterReceiverOptions) { constructor(reporter: Partial<ReporterV2>, options: TeleReporterReceiverOptions) {
@ -144,11 +143,16 @@ export class TeleReporterReceiver {
this._reporter = reporter; this._reporter = reporter;
} }
dispatch(mode: 'list' | 'test', message: JsonEvent): Promise<void> | void { reset() {
this._rootSuite.suites = [];
this._rootSuite.tests = [];
this._tests.clear();
}
dispatch(message: JsonEvent): Promise<void> | void {
const { method, params } = message; const { method, params } = message;
if (method === 'onConfigure') { if (method === 'onConfigure') {
this._onConfigure(params.config); this._onConfigure(params.config);
this._listOnly = mode === 'list';
return; return;
} }
if (method === 'onProject') { if (method === 'onProject') {
@ -205,23 +209,6 @@ export class TeleReporterReceiver {
// Always update project in watch mode. // Always update project in watch mode.
projectSuite._project = this._parseProject(project); projectSuite._project = this._parseProject(project);
this._mergeSuitesInto(project.suites, projectSuite); this._mergeSuitesInto(project.suites, projectSuite);
// Remove deleted tests when listing. Empty suites will be auto-filtered
// in the UI layer.
if (this._listOnly) {
const testIds = new Set<string>();
const collectIds = (suite: JsonSuite) => {
suite.tests.map(t => t.testId).forEach(testId => testIds.add(testId));
suite.suites.forEach(collectIds);
};
project.suites.forEach(collectIds);
const filterTests = (suite: TeleSuite) => {
suite.tests = suite.tests.filter(t => testIds.has(t.id));
suite.suites.forEach(filterTests);
};
filterTests(projectSuite);
}
} }
private _onBegin() { private _onBegin() {

View file

@ -20,16 +20,14 @@ import * as events from './events';
export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents { export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents {
readonly onClose: events.Event<void>; readonly onClose: events.Event<void>;
readonly onListReport: events.Event<any>; readonly onReport: events.Event<any>;
readonly onTestReport: events.Event<any>;
readonly onStdio: events.Event<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>; readonly onStdio: events.Event<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>;
readonly onListChanged: events.Event<void>; readonly onListChanged: events.Event<void>;
readonly onTestFilesChanged: events.Event<{ testFiles: string[] }>; readonly onTestFilesChanged: events.Event<{ testFiles: string[] }>;
readonly onLoadTraceRequested: events.Event<{ traceUrl: string }>; readonly onLoadTraceRequested: events.Event<{ traceUrl: string }>;
private _onCloseEmitter = new events.EventEmitter<void>(); private _onCloseEmitter = new events.EventEmitter<void>();
private _onListReportEmitter = new events.EventEmitter<any>(); private _onReportEmitter = new events.EventEmitter<any>();
private _onTestReportEmitter = new events.EventEmitter<any>();
private _onStdioEmitter = new events.EventEmitter<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>(); private _onStdioEmitter = new events.EventEmitter<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>();
private _onListChangedEmitter = new events.EventEmitter<void>(); private _onListChangedEmitter = new events.EventEmitter<void>();
private _onTestFilesChangedEmitter = new events.EventEmitter<{ testFiles: string[] }>(); private _onTestFilesChangedEmitter = new events.EventEmitter<{ testFiles: string[] }>();
@ -42,8 +40,7 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
constructor(wsURL: string) { constructor(wsURL: string) {
this.onClose = this._onCloseEmitter.event; this.onClose = this._onCloseEmitter.event;
this.onListReport = this._onListReportEmitter.event; this.onReport = this._onReportEmitter.event;
this.onTestReport = this._onTestReportEmitter.event;
this.onStdio = this._onStdioEmitter.event; this.onStdio = this._onStdioEmitter.event;
this.onListChanged = this._onListChangedEmitter.event; this.onListChanged = this._onListChangedEmitter.event;
this.onTestFilesChanged = this._onTestFilesChangedEmitter.event; this.onTestFilesChanged = this._onTestFilesChangedEmitter.event;
@ -94,10 +91,8 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
} }
private _dispatchEvent(method: string, params?: any) { private _dispatchEvent(method: string, params?: any) {
if (method === 'listReport') if (method === 'report')
this._onListReportEmitter.fire(params); this._onReportEmitter.fire(params);
else if (method === 'testReport')
this._onTestReportEmitter.fire(params);
else if (method === 'stdio') else if (method === 'stdio')
this._onStdioEmitter.fire(params); this._onStdioEmitter.fire(params);
else if (method === 'listChanged') else if (method === 'listChanged')
@ -142,9 +137,10 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
return await this._sendMessage('listFiles'); return await this._sendMessage('listFiles');
} }
async listTests(params: { reporter?: string | undefined; fileNames?: string[] | undefined; }): Promise<void> { async listTests(params: { reporter?: string | undefined; fileNames?: string[] | undefined; }): Promise<{ report: any[] }> {
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<void> { 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<void> {
await this._sendMessage('runTests', params); await this._sendMessage('runTests', params);
} }
@ -153,8 +149,8 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
return await this._sendMessage('findRelatedTestFiles', params); return await this._sendMessage('findRelatedTestFiles', params);
} }
async stop(): Promise<void> { async stopTests(): Promise<void> {
await this._sendMessage('stop'); await this._sendMessage('stopTests');
} }
async closeGracefully(): Promise<void> { async closeGracefully(): Promise<void> {

View file

@ -47,10 +47,13 @@ export interface TestServerInterface {
error?: reporterTypes.TestError; error?: reporterTypes.TestError;
}>; }>;
/**
* Returns list of teleReporter events.
*/
listTests(params: { listTests(params: {
reporter?: string; reporter?: string;
fileNames?: string[]; fileNames?: string[];
}): Promise<void>; }): Promise<{ report: any[] }>;
runTests(params: { runTests(params: {
reporter?: string; reporter?: string;
@ -69,15 +72,14 @@ export interface TestServerInterface {
files: string[]; files: string[];
}): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[]; }>; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[]; }>;
stop(): Promise<void>; stopTests(): Promise<void>;
closeGracefully(): Promise<void>; closeGracefully(): Promise<void>;
} }
export interface TestServerInterfaceEvents { export interface TestServerInterfaceEvents {
onClose: Event<void>; onClose: Event<void>;
onListReport: Event<any>; onReport: Event<any>;
onTestReport: 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>;
onTestFilesChanged: Event<{ testFiles: string[] }>; onTestFilesChanged: Event<{ testFiles: string[] }>;
@ -86,8 +88,7 @@ export interface TestServerInterfaceEvents {
export interface TestServerInterfaceEventEmitters { export interface TestServerInterfaceEventEmitters {
dispatchEvent(event: 'close', params: {}): void; dispatchEvent(event: 'close', params: {}): void;
dispatchEvent(event: 'listReport', params: any): void; dispatchEvent(event: 'report', params: any): void;
dispatchEvent(event: 'testReport', 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

@ -65,7 +65,7 @@ export async function createMergedReport(config: FullConfigInternal, dir: string
for (const event of events) { for (const event of events) {
if (event.method === 'onEnd') if (event.method === 'onEnd')
printStatus(`building final report`); printStatus(`building final report`);
await receiver.dispatch('test', event); await receiver.dispatch(event);
if (event.method === 'onEnd') if (event.method === 'onEnd')
printStatus(`finished building report`); printStatus(`finished building report`);
} }

View file

@ -89,7 +89,6 @@ function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'ui
return { return {
configDir: config.configDir, configDir: config.configDir,
_send: send, _send: send,
_mode: mode,
}; };
} }

View file

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import fs from 'fs';
import path from 'path';
import { registry, startTraceViewerServer } from 'playwright-core/lib/server'; import { 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';
@ -172,12 +174,17 @@ class TestServerDispatcher implements TestServerInterface {
} }
async listTests(params: { reporter?: string; fileNames: string[]; }) { async listTests(params: { reporter?: string; fileNames: string[]; }) {
this._queue = this._queue.then(() => this._innerListTests(params)).catch(printInternalError); let report: any[] = [];
this._queue = this._queue.then(async () => {
report = await this._innerListTests(params);
}).catch(printInternalError);
await this._queue; await this._queue;
return { report };
} }
private async _innerListTests(params: { reporter?: string; fileNames?: string[]; }) { private async _innerListTests(params: { reporter?: string; fileNames?: string[]; }) {
const wireReporter = await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => this._dispatchEvent('listReport', 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); const reporter = new InternalReporter(wireReporter);
this._config.cliArgs = params.fileNames || []; this._config.cliArgs = params.fileNames || [];
this._config.cliListOnly = true; this._config.cliListOnly = true;
@ -195,7 +202,15 @@ class TestServerDispatcher implements TestServerInterface {
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);
if (result) {
projectDirs.add(result.templateDir);
projectOutputs.add(result.outDir);
}
this._globalWatcher.update([...projectDirs], [...projectOutputs], false); this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
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: { 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; }) {
@ -204,7 +219,7 @@ class TestServerDispatcher implements TestServerInterface {
} }
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; }) { 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; }) {
await this.stop(); await this.stopTests();
const { testIds, projects, locations, grep } = params; const { testIds, projects, locations, grep } = params;
const testIdSet = testIds ? new Set<string>(testIds) : null; const testIdSet = testIds ? new Set<string>(testIds) : null;
@ -215,7 +230,7 @@ class TestServerDispatcher implements TestServerInterface {
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
const reporters = await createReporters(this._config, 'ui'); const reporters = await createReporters(this._config, 'ui');
reporters.push(await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => this._dispatchEvent('testReport', e))); reporters.push(await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', 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(this._config, reporter);
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(this._config, reporter);
@ -246,7 +261,7 @@ class TestServerDispatcher implements TestServerInterface {
return runner.findRelatedTestFiles('out-of-process', params.files); return runner.findRelatedTestFiles('out-of-process', params.files);
} }
async stop() { async stopTests() {
this._testRun?.stop?.resolve(); this._testRun?.stop?.resolve();
await this._testRun?.run; await this._testRun?.run;
} }
@ -306,3 +321,17 @@ function printInternalError(e: Error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Internal error:', e); console.error('Internal error:', e);
} }
// TODO: remove CT dependency.
export async function resolveCtDirs(config: FullConfigInternal) {
const use = config.config.projects[0].use as any;
const relativeTemplateDir = use.ctTemplateDir || 'playwright';
const templateDir = await fs.promises.realpath(path.normalize(path.join(config.configDir, relativeTemplateDir))).catch(() => undefined);
if (!templateDir)
return null;
const outDir = use.ctCacheDir ? path.resolve(config.configDir, use.ctCacheDir) : path.resolve(templateDir, '.cache');
return {
outDir,
templateDir
};
}

View file

@ -128,11 +128,16 @@ export class TeleSuiteUpdater {
}; };
} }
dispatch(mode: 'test' | 'list', message: any) { processListReport(report: any[]) {
this._receiver.reset();
for (const message of report)
this._receiver.dispatch(message);
}
processTestReport(message: any) {
// The order of receiver dispatches matters here, we want to assign `lastRunTestCount` // The order of receiver dispatches matters here, we want to assign `lastRunTestCount`
// before we use it. // before we use it.
if (mode === 'test') this._lastRunReceiver?.dispatch(message)?.catch(() => {});
this._lastRunReceiver?.dispatch('test', message)?.catch(() => {}); this._receiver.dispatch(message)?.catch(() => {});
this._receiver.dispatch(mode, message)?.catch(() => {});
} }
} }

View file

@ -184,7 +184,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?.stop().catch(() => {}); testServerConnection?.stopTests().catch(() => {});
} else if (e.code === 'F5') { } else if (e.code === 'F5') {
e.preventDefault(); e.preventDefault();
reloadTests(); reloadTests();
@ -278,7 +278,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?.stop()} 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);
@ -648,12 +648,14 @@ const refreshRootSuite = async (testServerConnection: TestServerConnection): Pro
}, },
pathSeparator, pathSeparator,
}); });
return testServerConnection.listTests({}); const { report } = await testServerConnection.listTests({});
teleSuiteUpdater?.processListReport(report);
}; };
const wireConnectionListeners = (testServerConnection: TestServerConnection) => { const wireConnectionListeners = (testServerConnection: TestServerConnection) => {
testServerConnection.onListChanged(() => { testServerConnection.onListChanged(async () => {
testServerConnection.listTests({}).catch(() => {}); const { report } = await testServerConnection.listTests({});
teleSuiteUpdater?.processListReport(report);
}); });
testServerConnection.onTestFilesChanged(params => { testServerConnection.onTestFilesChanged(params => {
@ -669,12 +671,8 @@ const wireConnectionListeners = (testServerConnection: TestServerConnection) =>
} }
}); });
testServerConnection.onListReport(params => { testServerConnection.onReport(params => {
teleSuiteUpdater?.dispatch('list', params); teleSuiteUpdater?.processTestReport(params);
});
testServerConnection.onTestReport(params => {
teleSuiteUpdater?.dispatch('test', params);
}); });
xtermDataSource.resize = (cols, rows) => { xtermDataSource.resize = (cols, rows) => {