diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index 4bf6986341..29d5dbd425 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -639,7 +639,8 @@ type TreeItemBase = { }; type GroupItem = TreeItemBase & { - kind: 'group', + kind: 'group'; + subKind: 'folder' | 'file' | 'describe'; children: (TestCaseItem | GroupItem)[]; }; @@ -657,10 +658,34 @@ type TestItem = TreeItemBase & { type TreeItem = GroupItem | TestCaseItem | TestItem; +function getFileItem(rootItem: GroupItem, filePath: string[], isFile: boolean, fileItems: Map): GroupItem { + if (filePath.length === 0) + return rootItem; + const fileName = filePath.join('/'); + const existingFileItem = fileItems.get(fileName); + if (existingFileItem) + return existingFileItem; + const parentFileItem = getFileItem(rootItem, filePath.slice(0, filePath.length - 1), false, fileItems); + const fileItem: GroupItem = { + kind: 'group', + subKind: isFile ? 'file' : 'folder', + id: fileName, + title: filePath[filePath.length - 1], + location: { file: fileName, line: 0, column: 0 }, + parent: parentFileItem, + children: [], + status: 'none', + }; + parentFileItem.children.push(fileItem); + fileItems.set(fileName, fileItem); + return fileItem; +} + function createTree(rootSuite: Suite | undefined, projectFilters: Map): GroupItem { const filterProjects = [...projectFilters.values()].some(Boolean); const rootItem: GroupItem = { kind: 'group', + subKind: 'folder', id: 'root', title: '', location: { file: '', line: 0, column: 0 }, @@ -676,6 +701,7 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map(); for (const projectSuite of rootSuite?.suites || []) { if (filterProjects && !projectFilters.get(projectSuite.title)) continue; - visitSuite(projectSuite.title, projectSuite, rootItem); + for (const fileSuite of projectSuite.suites) { + const fileItem = getFileItem(rootItem, fileSuite.location!.file.split(/[\\\/]/), true, fileMap); + visitSuite(projectSuite.title, fileSuite, fileItem); + } } const sortAndPropagateStatus = (treeItem: TreeItem) => { for (const child of treeItem.children) sortAndPropagateStatus(child); - if (treeItem.kind === 'group' && treeItem.parent) - treeItem.children.sort((a, b) => a.location.line - b.location.line); + 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; @@ -767,7 +801,11 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map) { diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index aab0fddc38..1419db8e65 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -254,3 +254,31 @@ test('should stop', async ({ runUITest }) => { ◯ test 3 `); }); + +test('should run folder', async ({ runUITest }) => { + const page = await runUITest({ + 'a/folder-b/folder-c/inC.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + 'a/folder-b/in-b.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + 'a/in-a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + }); + + await page.getByText('folder-b').hover(); + await page.getByRole('listitem').filter({ hasText: 'folder-b' }).getByTitle('Run').click(); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` + ▼ ✅ folder-b <= + ► ✅ folder-c + ► ✅ in-b.test.ts + ▼ ◯ in-a.test.ts + ◯ passes + `); +}); diff --git a/tests/playwright-test/ui-mode-test-tree.spec.ts b/tests/playwright-test/ui-mode-test-tree.spec.ts index a57c8d035d..9dcfef62ef 100644 --- a/tests/playwright-test/ui-mode-test-tree.spec.ts +++ b/tests/playwright-test/ui-mode-test-tree.spec.ts @@ -117,3 +117,28 @@ test('should expand / collapse groups', async ({ runUITest }) => { ► ◯ a.test.ts <= `); }); + +test('should merge folder trees', async ({ runUITest }) => { + const page = await runUITest({ + 'a/b/c/inC.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + 'a/b/in-b.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + 'a/in-a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + }); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` + ▼ ◯ b + ► ◯ c + ► ◯ in-b.test.ts + ▼ ◯ in-a.test.ts + ◯ passes + `); +});