diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index e2923d0a1a..bc0c4b5c78 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -38,7 +38,6 @@ class UIMode { private _testWatcher: FSWatcher | undefined; private _watchTestFile: string | undefined; private _originalStderr: (buffer: string | Uint8Array) => void; - private _globalWatcher: FSWatcher; constructor(config: FullConfigInternal) { this._config = config; @@ -58,7 +57,7 @@ class UIMode { return true; }; - this._globalWatcher = this._installGlobalWatcher(); + this._installGlobalWatcher(); } private _installGlobalWatcher(): FSWatcher { diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index 51da4491a6..c9bb002419 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -324,6 +324,8 @@ const refreshRootSuite = (eraseResults: boolean) => { else ++progress.passed; updateRootSuite(rootSuite, progress); + // This will update selected trace viewer. + updateStepsProgress(); }, onStepBegin: () => { @@ -373,22 +375,13 @@ const sendMessageNoReply = (method: string, params?: any) => { }; const fileName = (treeItem?: TreeItem): string | undefined => { - if (!treeItem) - return; - if (treeItem.kind === 'file') - return treeItem.file; - return fileName(treeItem.parent || undefined); + return treeItem?.location.file; }; const locationToOpen = (treeItem?: TreeItem) => { if (!treeItem) return; - if (treeItem.kind === 'test') - return treeItem.test.location.file + ':' + treeItem.test.location.line; - if (treeItem.kind === 'case') - return treeItem.location.file + ':' + treeItem.location.line; - if (treeItem.kind === 'file') - return treeItem.file; + return treeItem.location.file + ':' + treeItem.location.line; }; const collectTestIds = (treeItem?: TreeItem): string[] => { @@ -414,29 +407,22 @@ type Progress = { }; type TreeItemBase = { - kind: 'root' | 'file' | 'case' | 'test', + kind: 'root' | 'group' | 'case' | 'test', id: string; title: string; - parent: TreeItem | null; + location: Location, children: TreeItem[]; status: 'none' | 'running' | 'passed' | 'failed'; }; -type RootItem = TreeItemBase & { - kind: 'root', - children: FileItem[]; -}; - -type FileItem = TreeItemBase & { - kind: 'file', - file: string; - children: TestCaseItem[]; +type GroupItem = TreeItemBase & { + kind: 'group', + children: (TestCaseItem | GroupItem)[]; }; type TestCaseItem = TreeItemBase & { kind: 'case', tests: TestCase[]; - location: Location, }; type TestItem = TreeItemBase & { @@ -444,77 +430,77 @@ type TestItem = TreeItemBase & { test: TestCase; }; -type TreeItem = RootItem | FileItem | TestCaseItem | TestItem; +type TreeItem = GroupItem | TestCaseItem | TestItem; -function createTree(rootSuite: Suite | undefined, projects: Map): RootItem { - const rootItem: RootItem = { - kind: 'root', +function createTree(rootSuite: Suite | undefined, projects: Map): GroupItem { + const rootItem: GroupItem = { + kind: 'group', id: 'root', title: '', - parent: null, + location: { file: '', line: 0, column: 0 }, children: [], status: 'none', }; - const fileItems = new Map(); - for (const projectSuite of rootSuite?.suites || []) { - if (!projects.get(projectSuite.title)) - continue; - for (const fileSuite of projectSuite.suites) { - const file = fileSuite.location!.file; - let fileItem = fileItems.get(file); - if (!fileItem) { - fileItem = { - kind: 'file', - id: fileSuite.title, - title: fileSuite.title, - file, - parent: null, + const visitSuite = (projectName: string, parentSuite: Suite, parentGroup: GroupItem) => { + for (const suite of parentSuite.suites) { + const title = suite.title; + let group = parentGroup.children.find(item => item.title === title) as GroupItem | undefined; + if (!group) { + group = { + kind: 'group', + id: parentGroup.id + '\x1e' + title, + title, + location: suite.location!, children: [], status: 'none', }; - fileItems.set(fileSuite.location!.file, fileItem); - rootItem.children.push(fileItem); + parentGroup.children.push(group); } - - for (const test of fileSuite.allTests()) { - const title = test.titlePath().slice(3).join(' › '); - let testCaseItem = fileItem.children.find(t => t.title === title) as TestCaseItem; - if (!testCaseItem) { - testCaseItem = { - kind: 'case', - id: fileItem.id + ' / ' + title, - title, - parent: fileItem, - children: [], - tests: [], - location: test.location, - status: 'none', - }; - fileItem.children.push(testCaseItem); - } - - let status: 'none' | 'running' | 'passed' | 'failed' = 'none'; - if (test.results.some(r => r.duration === -1)) - status = 'running'; - else if (test.results.length && test.outcome() !== 'expected') - status = 'failed'; - else if (test.outcome() === 'expected') - status = 'passed'; - - testCaseItem.tests.push(test); - testCaseItem.children.push({ - kind: 'test', - id: test.id, - title: projectSuite.title, - parent: testCaseItem, - test, - children: [], - status, - }); - } - (fileItem.children as TestCaseItem[]).sort((a, b) => a.location.line - b.location.line); + visitSuite(projectName, suite, group); } + + for (const test of parentSuite.tests) { + const title = test.title; + let testCaseItem = parentGroup.children.find(t => t.title === title) as TestCaseItem; + if (!testCaseItem) { + testCaseItem = { + kind: 'case', + id: parentGroup.id + '\x1e' + title, + title, + children: [], + tests: [], + location: test.location, + status: 'none', + }; + parentGroup.children.push(testCaseItem); + } + + let status: 'none' | 'running' | 'passed' | 'failed' = 'none'; + if (test.results.some(r => r.duration === -1)) + status = 'running'; + else if (test.results.length && test.outcome() !== 'expected') + status = 'failed'; + else if (test.outcome() === 'expected') + status = 'passed'; + + testCaseItem.tests.push(test); + testCaseItem.children.push({ + kind: 'test', + id: test.id, + title: projectName, + location: test.location!, + test, + children: [], + status, + }); + } + }; + + for (const projectSuite of rootSuite?.suites || []) { + if (!projects.get(projectSuite.title)) + continue; + visitSuite(projectSuite.title, projectSuite, rootItem); } const propagateStatus = (treeItem: TreeItem) => { @@ -542,27 +528,29 @@ function createTree(rootSuite: Suite | undefined, projects: Map return rootItem; } -function filterTree(rootItem: RootItem, filterText: string) { +function filterTree(rootItem: GroupItem, filterText: string) { const trimmedFilterText = filterText.trim(); const filterTokens = trimmedFilterText.toLowerCase().split(' '); - const result: FileItem[] = []; - for (const fileItem of rootItem.children) { - if (trimmedFilterText) { - const filteredCases: TestCaseItem[] = []; - for (const testCaseItem of fileItem.children) { - const fullTitle = (fileItem.title + ' ' + testCaseItem.title).toLowerCase(); - if (filterTokens.every(token => fullTitle.includes(token))) - filteredCases.push(testCaseItem); + + const visit = (treeItem: GroupItem) => { + const newChildren: (GroupItem | TestCaseItem)[] = []; + for (const child of treeItem.children) { + if (child.kind === 'case') { + const title = child.tests[0].titlePath().join(' ').toLowerCase(); + if (filterTokens.every(token => title.includes(token))) + newChildren.push(child); + } else { + visit(child); + if (child.children.length) + newChildren.push(child); } - fileItem.children = filteredCases; } - if (fileItem.children.length) - result.push(fileItem); - } - rootItem.children = result; + treeItem.children = newChildren; + }; + visit(rootItem); } -function hideOnlyTests(rootItem: RootItem) { +function hideOnlyTests(rootItem: GroupItem) { const visit = (treeItem: TreeItem) => { if (treeItem.kind === 'case' && treeItem.children.length === 1) treeItem.children = []; diff --git a/packages/web/src/components/xtermWrapper.tsx b/packages/web/src/components/xtermWrapper.tsx index 7447c64e18..1a1522199d 100644 --- a/packages/web/src/components/xtermWrapper.tsx +++ b/packages/web/src/components/xtermWrapper.tsx @@ -16,7 +16,7 @@ import * as React from 'react'; import './xtermWrapper.css'; -import type { Terminal } from 'xterm'; +import type { ITheme, Terminal } from 'xterm'; import type { XtermModule } from './xtermModule'; import { isDarkTheme } from '@web/theme'; @@ -77,7 +77,7 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({ ; }; -const lightTheme = { +const lightTheme: ITheme = { foreground: '#383a42', background: '#fafafa', cursor: '#383a42', @@ -96,10 +96,12 @@ const lightTheme = { brightBlue: '#4078f2', brightMagenta: '#a626a4', brightCyan: '#0184bc', - brightWhite: '#383a42' + brightWhite: '#383a42', + selectionBackground: '#d7d7d7', + selectionForeground: '#383a42', }; -const darkTheme = { +const darkTheme: ITheme = { foreground: '#f8f8f2', background: '#1e1e1e', cursor: '#f8f8f0', @@ -119,4 +121,6 @@ const darkTheme = { brightMagenta: '#ff92df', brightCyan: '#a4ffff', brightWhite: '#e6e6e6', + selectionBackground: '#44475a', + selectionForeground: '#f8f8f2', };