diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index 97a2bfa709646..e675ce336bade 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -135,7 +135,6 @@ export class TeleReporterReceiver { private _reporter: Partial; private _tests = new Map(); private _rootDir!: string; - private _listOnly = false; private _config!: reporterTypes.FullConfig; constructor(reporter: Partial, options: TeleReporterReceiverOptions) { @@ -144,11 +143,16 @@ export class TeleReporterReceiver { this._reporter = reporter; } - dispatch(mode: 'list' | 'test', message: JsonEvent): Promise | void { + reset() { + this._rootSuite.suites = []; + this._rootSuite.tests = []; + this._tests.clear(); + } + + dispatch(message: JsonEvent): Promise | void { const { method, params } = message; if (method === 'onConfigure') { this._onConfigure(params.config); - this._listOnly = mode === 'list'; return; } if (method === 'onProject') { @@ -205,23 +209,6 @@ export class TeleReporterReceiver { // Always update project in watch mode. projectSuite._project = this._parseProject(project); 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(); - 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() { diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts index e29e448675e7f..4c8b2ffb8d39b 100644 --- a/packages/playwright/src/isomorphic/testServerConnection.ts +++ b/packages/playwright/src/isomorphic/testServerConnection.ts @@ -20,16 +20,14 @@ import * as events from './events'; export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents { readonly onClose: events.Event; - readonly onListReport: events.Event; - readonly onTestReport: events.Event; + readonly onReport: events.Event; readonly onStdio: events.Event<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>; readonly onListChanged: events.Event; readonly onTestFilesChanged: events.Event<{ testFiles: string[] }>; readonly onLoadTraceRequested: events.Event<{ traceUrl: string }>; private _onCloseEmitter = new events.EventEmitter(); - private _onListReportEmitter = new events.EventEmitter(); - private _onTestReportEmitter = new events.EventEmitter(); + private _onReportEmitter = new events.EventEmitter(); private _onStdioEmitter = new events.EventEmitter<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>(); private _onListChangedEmitter = new events.EventEmitter(); private _onTestFilesChangedEmitter = new events.EventEmitter<{ testFiles: string[] }>(); @@ -42,8 +40,7 @@ export class TestServerConnection implements TestServerInterface, TestServerInte constructor(wsURL: string) { this.onClose = this._onCloseEmitter.event; - this.onListReport = this._onListReportEmitter.event; - this.onTestReport = this._onTestReportEmitter.event; + this.onReport = this._onReportEmitter.event; this.onStdio = this._onStdioEmitter.event; this.onListChanged = this._onListChangedEmitter.event; this.onTestFilesChanged = this._onTestFilesChangedEmitter.event; @@ -94,10 +91,8 @@ export class TestServerConnection implements TestServerInterface, TestServerInte } private _dispatchEvent(method: string, params?: any) { - if (method === 'listReport') - this._onListReportEmitter.fire(params); - else if (method === 'testReport') - this._onTestReportEmitter.fire(params); + if (method === 'report') + this._onReportEmitter.fire(params); else if (method === 'stdio') this._onStdioEmitter.fire(params); else if (method === 'listChanged') @@ -142,9 +137,10 @@ export class TestServerConnection implements TestServerInterface, TestServerInte return await this._sendMessage('listFiles'); } - async listTests(params: { reporter?: string | undefined; fileNames?: string[] | undefined; }): Promise { - await this._sendMessage('listTests', params); + async listTests(params: { reporter?: string | undefined; fileNames?: string[] | undefined; }): Promise<{ report: any[] }> { + 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 { await this._sendMessage('runTests', params); } @@ -153,8 +149,8 @@ export class TestServerConnection implements TestServerInterface, TestServerInte return await this._sendMessage('findRelatedTestFiles', params); } - async stop(): Promise { - await this._sendMessage('stop'); + async stopTests(): Promise { + await this._sendMessage('stopTests'); } async closeGracefully(): Promise { diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 17324a4117d56..c10a273ef64fc 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -47,10 +47,13 @@ export interface TestServerInterface { error?: reporterTypes.TestError; }>; + /** + * Returns list of teleReporter events. + */ listTests(params: { reporter?: string; fileNames?: string[]; - }): Promise; + }): Promise<{ report: any[] }>; runTests(params: { reporter?: string; @@ -69,15 +72,14 @@ export interface TestServerInterface { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[]; }>; - stop(): Promise; + stopTests(): Promise; closeGracefully(): Promise; } export interface TestServerInterfaceEvents { onClose: Event; - onListReport: Event; - onTestReport: Event; + onReport: Event; onStdio: Event<{ type: 'stdout' | 'stderr', text?: string, buffer?: string }>; onListChanged: Event; onTestFilesChanged: Event<{ testFiles: string[] }>; @@ -86,8 +88,7 @@ export interface TestServerInterfaceEvents { export interface TestServerInterfaceEventEmitters { dispatchEvent(event: 'close', params: {}): void; - dispatchEvent(event: 'listReport', params: any): void; - dispatchEvent(event: 'testReport', params: any): void; + dispatchEvent(event: 'report', params: any): void; dispatchEvent(event: 'stdio', params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }): void; dispatchEvent(event: 'listChanged', params: {}): void; dispatchEvent(event: 'testFilesChanged', params: { testFiles: string[] }): void; diff --git a/packages/playwright/src/reporters/merge.ts b/packages/playwright/src/reporters/merge.ts index 0b21182bba97c..ef34c409cc980 100644 --- a/packages/playwright/src/reporters/merge.ts +++ b/packages/playwright/src/reporters/merge.ts @@ -65,7 +65,7 @@ export async function createMergedReport(config: FullConfigInternal, dir: string for (const event of events) { if (event.method === 'onEnd') printStatus(`building final report`); - await receiver.dispatch('test', event); + await receiver.dispatch(event); if (event.method === 'onEnd') printStatus(`finished building report`); } diff --git a/packages/playwright/src/runner/reporters.ts b/packages/playwright/src/runner/reporters.ts index 3ab0ca18c60a1..7e846c493ffdd 100644 --- a/packages/playwright/src/runner/reporters.ts +++ b/packages/playwright/src/runner/reporters.ts @@ -89,7 +89,6 @@ function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'ui return { configDir: config.configDir, _send: send, - _mode: mode, }; } diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index c17435402c02f..828c3fbdc052a 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; +import path from 'path'; import { registry, startTraceViewerServer } from 'playwright-core/lib/server'; import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils'; import type { Transport, HttpServer } from 'playwright-core/lib/utils'; @@ -172,12 +174,17 @@ class TestServerDispatcher implements TestServerInterface { } 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; + return { report }; } 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); this._config.cliArgs = params.fileNames || []; this._config.cliListOnly = true; @@ -195,7 +202,15 @@ class TestServerDispatcher implements TestServerInterface { projectDirs.add(p.project.testDir); 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); + 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; }) { @@ -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; }) { - await this.stop(); + await this.stopTests(); const { testIds, projects, locations, grep } = params; const testIdSet = testIds ? new Set(testIds) : null; @@ -215,7 +230,7 @@ class TestServerDispatcher implements TestServerInterface { this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); 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 taskRunner = createTaskRunnerForWatch(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); } - async stop() { + async stopTests() { this._testRun?.stop?.resolve(); await this._testRun?.run; } @@ -306,3 +321,17 @@ function printInternalError(e: Error) { // eslint-disable-next-line no-console 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 + }; +} diff --git a/packages/trace-viewer/src/ui/teleSuiteUpdater.ts b/packages/trace-viewer/src/ui/teleSuiteUpdater.ts index 82e7ab2913cb2..582567e11a82d 100644 --- a/packages/trace-viewer/src/ui/teleSuiteUpdater.ts +++ b/packages/trace-viewer/src/ui/teleSuiteUpdater.ts @@ -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` // before we use it. - if (mode === 'test') - this._lastRunReceiver?.dispatch('test', message)?.catch(() => {}); - this._receiver.dispatch(mode, message)?.catch(() => {}); + this._lastRunReceiver?.dispatch(message)?.catch(() => {}); + this._receiver.dispatch(message)?.catch(() => {}); } } diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index a894920dfb359..e406747db7ad5 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -184,7 +184,7 @@ export const UIModeView: React.FC<{}> = ({ const onShortcutEvent = (e: KeyboardEvent) => { if (e.code === 'F6') { e.preventDefault(); - testServerConnection?.stop().catch(() => {}); + testServerConnection?.stopTests().catch(() => {}); } else if (e.code === 'F5') { e.preventDefault(); reloadTests(); @@ -278,7 +278,7 @@ export const UIModeView: React.FC<{}> = ({
Running {progress.passed}/{runningState.testIds.size} passed ({(progress.passed / runningState.testIds.size) * 100 | 0}%)
} runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}> - testServerConnection?.stop()} disabled={!isRunningTest || isLoading}> + testServerConnection?.stopTests()} disabled={!isRunningTest || isLoading}> { setWatchedTreeIds({ value: new Set() }); setWatchAll(!watchAll); @@ -648,12 +648,14 @@ const refreshRootSuite = async (testServerConnection: TestServerConnection): Pro }, pathSeparator, }); - return testServerConnection.listTests({}); + const { report } = await testServerConnection.listTests({}); + teleSuiteUpdater?.processListReport(report); }; const wireConnectionListeners = (testServerConnection: TestServerConnection) => { - testServerConnection.onListChanged(() => { - testServerConnection.listTests({}).catch(() => {}); + testServerConnection.onListChanged(async () => { + const { report } = await testServerConnection.listTests({}); + teleSuiteUpdater?.processListReport(report); }); testServerConnection.onTestFilesChanged(params => { @@ -669,12 +671,8 @@ const wireConnectionListeners = (testServerConnection: TestServerConnection) => } }); - testServerConnection.onListReport(params => { - teleSuiteUpdater?.dispatch('list', params); - }); - - testServerConnection.onTestReport(params => { - teleSuiteUpdater?.dispatch('test', params); + testServerConnection.onReport(params => { + teleSuiteUpdater?.processTestReport(params); }); xtermDataSource.resize = (cols, rows) => {