diff --git a/packages/playwright-test/src/isomorphic/teleReceiver.ts b/packages/playwright-test/src/isomorphic/teleReceiver.ts index f3e86b9d7a..88315bd442 100644 --- a/packages/playwright-test/src/isomorphic/teleReceiver.ts +++ b/packages/playwright-test/src/isomorphic/teleReceiver.ts @@ -175,6 +175,7 @@ export class TeleReporterReceiver { testResult.workerIndex = payload.workerIndex; testResult.parallelIndex = payload.parallelIndex; testResult.startTime = new Date(payload.startTime); + testResult.statusEx = 'running'; this._reporter.onTestBegin?.(test, testResult); } @@ -183,6 +184,7 @@ export class TeleReporterReceiver { const result = test.resultsMap.get(payload.id)!; result.duration = payload.duration; result.status = payload.status; + result.statusEx = payload.status; result.errors = payload.errors; result.attachments = payload.attachments; this._reporter.onTestEnd?.(test, result); @@ -380,7 +382,7 @@ export class TeleSuite implements SuitePrivate { export class TeleTestCase implements reporterTypes.TestCase { title: string; fn = () => {}; - results: reporterTypes.TestResult[] = []; + results: TeleTestResult[] = []; location: Location; parent!: TeleSuite; @@ -426,7 +428,7 @@ export class TeleTestCase implements reporterTypes.TestCase { this.resultsMap.clear(); } - _createTestResult(id: string): reporterTypes.TestResult { + _createTestResult(id: string): TeleTestResult { this._clearResults(); const result: TeleTestResult = { retry: this.results.length, @@ -438,6 +440,7 @@ export class TeleTestCase implements reporterTypes.TestCase { stderr: [], attachments: [], status: 'skipped', + statusEx: 'scheduled', steps: [], errors: [], stepMap: new Map(), @@ -453,6 +456,7 @@ export class TeleTestCase implements reporterTypes.TestCase { export type TeleTestResult = reporterTypes.TestResult & { stepMap: Map; stepStack: (reporterTypes.TestStep | reporterTypes.TestResult)[]; + statusEx: reporterTypes.TestResult['status'] | 'scheduled' | 'running'; }; export type TeleFullProject = FullProject & { id: string }; diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index 21402377f2..541d7c7f47 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -307,8 +307,11 @@ const TestList: React.FC<{ // Build the test tree. const { rootItem, treeItemMap, fileNames } = React.useMemo(() => { - const rootItem = createTree(testModel.rootSuite, projectFilters); + let rootItem = createTree(testModel.rootSuite, projectFilters); filterTree(rootItem, filterText, statusFilters, runningState?.testIds); + sortAndPropagateStatus(rootItem); + rootItem = shortenRoot(rootItem); + hideOnlyTests(rootItem); const treeItemMap = new Map(); const visibleTestIds = new Set(); @@ -769,18 +772,19 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map r.workerIndex === -1)) + if (result?.statusEx === 'scheduled') status = 'scheduled'; - else if (test.results.some(r => r.duration === -1)) + else if (result?.statusEx === 'running') status = 'running'; - else if (test.results.length && test.results[0].status === 'skipped') + else if (result?.status === 'skipped') status = 'skipped'; - else if (test.results.length && test.results[0].status === 'interrupted') + else if (result?.status === 'interrupted') status = 'none'; - else if (test.results.length && test.outcome() !== 'expected') + else if (result && test.outcome() !== 'expected') status = 'failed'; - else if (test.results.length && test.outcome() === 'expected') + else if (result && test.outcome() === 'expected') status = 'passed'; testCaseItem.tests.push(test); @@ -809,50 +813,7 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map { - for (const child of treeItem.children) - sortAndPropagateStatus(child); - - if (treeItem.kind === 'group') { - treeItem.children.sort((a, b) => { - const fc = a.location.file.localeCompare(b.location.file); - return fc || a.location.line - b.location.line; - }); - } - - let allPassed = treeItem.children.length > 0; - let allSkipped = treeItem.children.length > 0; - let hasFailed = false; - let hasRunning = false; - let hasScheduled = false; - - for (const child of treeItem.children) { - allSkipped = allSkipped && child.status === 'skipped'; - allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped'); - hasFailed = hasFailed || child.status === 'failed'; - hasRunning = hasRunning || child.status === 'running'; - hasScheduled = hasScheduled || child.status === 'scheduled'; - } - - if (hasRunning) - treeItem.status = 'running'; - else if (hasScheduled) - treeItem.status = 'scheduled'; - else if (hasFailed) - treeItem.status = 'failed'; - else if (allSkipped) - treeItem.status = 'skipped'; - else if (allPassed) - treeItem.status = 'passed'; - }; - sortAndPropagateStatus(rootItem); - - let shortRoot = rootItem; - while (shortRoot.children.length === 1 && shortRoot.children[0].kind === 'group' && shortRoot.children[0].subKind === 'folder') - shortRoot = shortRoot.children[0]; - shortRoot.location = rootItem.location; - return shortRoot; + return rootItem; } function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map, runningTestIds: Set | undefined) { @@ -887,6 +848,51 @@ function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map< visit(rootItem); } +function sortAndPropagateStatus(treeItem: TreeItem) { + for (const child of treeItem.children) + sortAndPropagateStatus(child); + + if (treeItem.kind === 'group') { + treeItem.children.sort((a, b) => { + const fc = a.location.file.localeCompare(b.location.file); + return fc || a.location.line - b.location.line; + }); + } + + let allPassed = treeItem.children.length > 0; + let allSkipped = treeItem.children.length > 0; + let hasFailed = false; + let hasRunning = false; + let hasScheduled = false; + + for (const child of treeItem.children) { + allSkipped = allSkipped && child.status === 'skipped'; + allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped'); + hasFailed = hasFailed || child.status === 'failed'; + hasRunning = hasRunning || child.status === 'running'; + hasScheduled = hasScheduled || child.status === 'scheduled'; + } + + if (hasRunning) + treeItem.status = 'running'; + else if (hasScheduled) + treeItem.status = 'scheduled'; + else if (hasFailed) + treeItem.status = 'failed'; + else if (allSkipped) + treeItem.status = 'skipped'; + else if (allPassed) + treeItem.status = 'passed'; +} + +function shortenRoot(rootItem: GroupItem): GroupItem { + let shortRoot = rootItem; + while (shortRoot.children.length === 1 && shortRoot.children[0].kind === 'group' && shortRoot.children[0].subKind === 'folder') + shortRoot = shortRoot.children[0]; + shortRoot.location = rootItem.location; + return shortRoot; +} + function hideOnlyTests(rootItem: GroupItem) { const visit = (treeItem: TreeItem) => { if (treeItem.kind === 'case' && treeItem.children.length === 1) diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts index 4adc151c62..36e47663ac 100644 --- a/tests/playwright-test/ui-mode-test-filters.spec.ts +++ b/tests/playwright-test/ui-mode-test-filters.spec.ts @@ -178,3 +178,28 @@ test('should not hide filtered while running', async ({ runUITest, createLatch } ↻ fails <= `); }); + +test('should filter skipped', async ({ runUITest, createLatch }) => { + const page = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test.skip('fails', async () => { + expect(1).toBe(2); + }); + `, + }); + await page.getByTitle('Run all').click(); + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ✅ a.test.ts + ✅ passes + ⊘ fails + `); + + await page.getByText('Status:').click(); + await page.getByLabel('skipped').setChecked(true); + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ⊘ a.test.ts + ⊘ fails + `); +});