diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index 4ea983ea02..3a9a34e391 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -146,6 +146,11 @@ export class TeleReporterReceiver { this._reporter = reporter; } + reset() { + this._rootSuite._entries = []; + this._tests.clear(); + } + dispatch(message: JsonEvent): Promise | void { const { method, params } = message; if (method === 'onConfigure') { @@ -206,35 +211,6 @@ export class TeleReporterReceiver { projectSuite._project = this._parseProject(project); for (const suite of project.suites) this._mergeSuiteInto(suite, projectSuite); - - // Remove deleted tests when listing. Empty suites will be auto-filtered - // in the UI layer. - if (this.isListing) { - const testIds = new Set(); - const collectIds = (suite: JsonSuite) => { - suite.entries.forEach(entry => { - if ('testId' in entry) - testIds.add(entry.testId); - else - collectIds(entry); - }); - }; - project.suites.forEach(collectIds); - - const filterTests = (suite: TeleSuite) => { - suite._entries = suite._entries.filter(entry => { - if (entry.type === 'test') { - if (testIds.has(entry.id)) - return true; - this._tests.delete(entry.id); - return false; - } - filterTests(entry); - return true; - }); - }; - filterTests(projectSuite); - } } private _onBegin() { @@ -545,6 +521,11 @@ export class TeleTestCase implements reporterTypes.TestCase { this._resultsMap.clear(); } + _restoreResults(snapshot: Map) { + this.results = [...snapshot.values()]; + this._resultsMap = snapshot; + } + _createTestResult(id: string): TeleTestResult { const result = new TeleTestResult(this.results.length); this.results.push(result); @@ -585,7 +566,7 @@ class TeleTestStep implements reporterTypes.TestStep { } } -class TeleTestResult implements reporterTypes.TestResult { +export class TeleTestResult implements reporterTypes.TestResult { retry: reporterTypes.TestResult['retry']; parallelIndex: reporterTypes.TestResult['parallelIndex'] = -1; workerIndex: reporterTypes.TestResult['workerIndex'] = -1; diff --git a/packages/trace-viewer/src/ui/teleSuiteUpdater.ts b/packages/trace-viewer/src/ui/teleSuiteUpdater.ts index 600722db73..a766627af3 100644 --- a/packages/trace-viewer/src/ui/teleSuiteUpdater.ts +++ b/packages/trace-viewer/src/ui/teleSuiteUpdater.ts @@ -15,6 +15,7 @@ */ import { TeleReporterReceiver, TeleSuite } from '@testIsomorphic/teleReceiver'; +import type { TeleTestCase, TeleTestResult } from '@testIsomorphic/teleReceiver'; import { statusEx } from '@testIsomorphic/testTree'; import type { ReporterV2 } from 'playwright/src/reporters/reporterV2'; import type * as reporterTypes from 'playwright/types/testReporter'; @@ -27,7 +28,7 @@ export type TeleSuiteUpdaterOptions = { }; export class TeleSuiteUpdater { - rootSuite: reporterTypes.Suite | undefined; + rootSuite: TeleSuite | undefined; config: reporterTypes.FullConfig | undefined; readonly loadErrors: reporterTypes.TestError[] = []; readonly progress: Progress = { @@ -41,6 +42,7 @@ export class TeleSuiteUpdater { private _lastRunReceiver: TeleReporterReceiver | undefined; private _lastRunTestCount = 0; private _options: TeleSuiteUpdaterOptions; + private _testResultsSnapshot: Map> | undefined; constructor(options: TeleSuiteUpdaterOptions) { this._receiver = new TeleReporterReceiver(this._createReporter(), { @@ -76,7 +78,16 @@ export class TeleSuiteUpdater { onBegin: (suite: reporterTypes.Suite) => { if (!this.rootSuite) - this.rootSuite = suite; + this.rootSuite = suite as TeleSuite; + // As soon as new test tree is built add previous results. + if (this._testResultsSnapshot) { + (this.rootSuite.allTests() as TeleTestCase[]).forEach(test => { + const results = this._testResultsSnapshot!.get(test.id); + if (results) + test._restoreResults(results); + }); + this._testResultsSnapshot = undefined; + } this.progress.total = this._lastRunTestCount; this.progress.passed = 0; this.progress.failed = 0; @@ -123,10 +134,13 @@ export class TeleSuiteUpdater { } processListReport(report: any[]) { - this._receiver.isListing = true; + // Save test results and reset all projects, the results will be restored after + // new project structure is built. + if (this.rootSuite) + this._testResultsSnapshot = new Map((this.rootSuite.allTests() as TeleTestCase[]).map(test => [test.id, test._resultsMap])); + this._receiver.reset(); for (const message of report) this._receiver.dispatch(message); - this._receiver.isListing = false; } processTestReportEvent(message: any) { diff --git a/tests/playwright-test/ui-mode-test-tree.spec.ts b/tests/playwright-test/ui-mode-test-tree.spec.ts index 0e0bc01f28..7346c204e4 100644 --- a/tests/playwright-test/ui-mode-test-tree.spec.ts +++ b/tests/playwright-test/ui-mode-test-tree.spec.ts @@ -48,6 +48,60 @@ test('should list tests', async ({ runUITest }) => { `); }); +test('should list all tests from projects with clashing names', async ({ runUITest }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30396' }); + const { page } = await runUITest({ + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + + export default defineConfig({ + projects: [ + { + name: 'proj-uno', + testDir: './foo', + }, + { + name: 'proj-dos', + testDir: './foo', + }, + { + name: 'proj-uno', + testDir: './bar', + }, + { + name: 'proj-dos', + testDir: './bar', + }, + ] + }); + `, + 'foo/a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('one', () => {}); + test('two', () => {}); + `, + 'bar/b.test.ts': ` + import { test, expect } from '@playwright/test'; + test('three', () => {}); + test('four', () => {}); + `, + }); + await page.getByTestId('test-tree').getByText('b.test.ts').click(); + await page.keyboard.press('ArrowRight'); + await page.getByTestId('test-tree').getByText('a.test.ts').click(); + await page.keyboard.press('ArrowRight'); + await expect.poll(dumpTestTree(page)).toBe(` + ▼ ◯ bar + ▼ ◯ b.test.ts + ◯ three + ◯ four + ▼ ◯ foo + ▼ ◯ a.test.ts <= + ◯ one + ◯ two + `); +}); + test('should traverse up/down', async ({ runUITest }) => { const { page } = await runUITest(basicTestTree); await page.getByText('a.test.ts').click();