diff --git a/packages/trace-viewer/src/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css index 8c293868ed..d361d52447 100644 --- a/packages/trace-viewer/src/ui/actionList.css +++ b/packages/trace-viewer/src/ui/actionList.css @@ -26,7 +26,7 @@ flex: none; align-items: center; margin: 0 4px; - color: var(--gray); + color: var(--vscode-editorCodeLens-foreground); } .action-icon { diff --git a/packages/trace-viewer/src/ui/timeline.css b/packages/trace-viewer/src/ui/timeline.css index bfcd66dbbd..179a63e1c3 100644 --- a/packages/trace-viewer/src/ui/timeline.css +++ b/packages/trace-viewer/src/ui/timeline.css @@ -127,7 +127,7 @@ .timeline-bar.frame_waitforeventinfo, .timeline-bar.page_waitforeventinfo { - --action-color: var(--gray); + --action-color: var(--vscode-editorCodeLens-foreground); } .timeline-label { diff --git a/packages/trace-viewer/src/ui/watchMode.css b/packages/trace-viewer/src/ui/watchMode.css index f94e490c5a..4e043dc894 100644 --- a/packages/trace-viewer/src/ui/watchMode.css +++ b/packages/trace-viewer/src/ui/watchMode.css @@ -37,7 +37,18 @@ .watch-mode-list-item-title { flex: auto; text-overflow: ellipsis; - overflow: hidden + overflow: hidden; +} + +.watch-mode-list-item-time { + flex: none; + color: var(--vscode-editorCodeLens-foreground); + margin: 0 4px; + user-select: none; +} + +.list-view-entry.highlighted .watch-mode-list-item-time { + display: none; } .watch-mode .section-title { @@ -56,7 +67,7 @@ .watch-mode-sidebar img { flex: none; - margin: 0 4px; + margin-left: 4px; width: 24px; height: 24px; } diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index 08176120ad..d615a7ec54 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -34,7 +34,7 @@ import { XtermWrapper } from '@web/components/xtermWrapper'; import { Expandable } from '@web/components/expandable'; import { toggleTheme } from '@web/theme'; import { artifactsFolderName } from '@testIsomorphic/folders'; -import { settings, useSetting } from '@web/uiUtils'; +import { msToString, settings, useSetting } from '@web/uiUtils'; let updateRootSuite: (config: FullConfig, rootSuite: Suite, progress: Progress | undefined) => void = () => {}; let runWatchedTests = (fileNames: string[]) => {}; @@ -73,12 +73,14 @@ export const WatchModeView: React.FC<{}> = ({ const [isLoading, setIsLoading] = React.useState(false); const [runningState, setRunningState] = React.useState<{ testIds: Set, itemSelectedByUser?: boolean } | undefined>(); const [watchAll, setWatchAll] = useSetting('watch-all', false); + const [watchedTreeIds, setWatchedTreeIds] = React.useState<{ value: Set }>({ value: new Set() }); const runTestPromiseChain = React.useRef(Promise.resolve()); const inputRef = React.useRef(null); const reloadTests = () => { setIsLoading(true); + setWatchedTreeIds({ value: new Set() }); updateRootSuite(baseFullConfig, new TeleSuite('', 'root'), undefined); refreshRootSuite(true).then(() => { setIsLoading(false); @@ -165,7 +167,6 @@ export const WatchModeView: React.FC<{}> = ({
Playwright
toggleTheme()} /> reloadTests()} disabled={isRunningTest || isLoading}> - setWatchAll(!watchAll)}> { setIsShowingOutput(!isShowingOutput); }} /> = ({ } runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}> sendMessageNoReply('stop')} disabled={!isRunningTest || isLoading}> + setWatchAll(!watchAll)}> = ({ onItemSelected={setSelectedItem} setVisibleTestIds={setVisibleTestIds} watchAll={watchAll} + watchedTreeIds={watchedTreeIds} + setWatchedTreeIds={setWatchedTreeIds} isLoading={isLoading} /> @@ -281,19 +285,20 @@ const TestList: React.FC<{ testModel: { rootSuite: Suite | undefined, config: FullConfig | undefined }, runTests: (mode: 'bounce-if-busy' | 'queue-if-busy', testIds: Set) => void, runningState?: { testIds: Set, itemSelectedByUser?: boolean }, - watchAll?: boolean, + watchAll: boolean, + watchedTreeIds: { value: Set }, + setWatchedTreeIds: (ids: { value: Set }) => void, isLoading?: boolean, setVisibleTestIds: (testIds: Set) => void, onItemSelected: (item: { testCase?: TestCase, location?: Location }) => void, -}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, isLoading, onItemSelected, setVisibleTestIds }) => { +}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, setVisibleTestIds }) => { const [treeState, setTreeState] = React.useState({ expandedItems: new Map() }); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState(); - const [watchedTreeIds, setWatchedTreeIds] = React.useState<{ value: Set }>({ value: new Set() }); // Build the test tree. const { rootItem, treeItemMap, fileNames } = React.useMemo(() => { const rootItem = createTree(testModel.rootSuite, projectFilters); - filterTree(rootItem, filterText, statusFilters); + filterTree(rootItem, filterText, statusFilters, runningState?.testIds); hideOnlyTests(rootItem); const treeItemMap = new Map(); const visibleTestIds = new Set(); @@ -309,7 +314,7 @@ const TestList: React.FC<{ visit(rootItem); setVisibleTestIds(visibleTestIds); return { rootItem, treeItemMap, fileNames }; - }, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds]); + }, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState]); // Look for a first failure within the run batch to select it. React.useEffect(() => { @@ -398,6 +403,7 @@ const TestList: React.FC<{ render={treeItem => { return
{treeItem.title}
+ {!!treeItem.duration &&
{msToString(treeItem.duration)}
} runTreeItem(treeItem)} disabled={!!runningState}> sendMessageNoReply('open', { location: locationToOpen(treeItem) })}> {!watchAll && { @@ -410,6 +416,8 @@ const TestList: React.FC<{
; }} icon={treeItem => { + if (treeItem.status === 'scheduled') + return 'codicon-clock'; if (treeItem.status === 'running') return 'codicon-loading'; if (treeItem.status === 'failed') @@ -638,9 +646,10 @@ type TreeItemBase = { id: string; title: string; location: Location, + duration: number; parent: TreeItem | undefined; children: TreeItem[]; - status: 'none' | 'running' | 'passed' | 'failed' | 'skipped'; + status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped'; }; type GroupItem = TreeItemBase & { @@ -677,6 +686,7 @@ function getFileItem(rootItem: GroupItem, filePath: string[], isFile: boolean, f id: fileName, title: filePath[filePath.length - 1], location: { file: fileName, line: 0, column: 0 }, + duration: 0, parent: parentFileItem, children: [], status: 'none', @@ -694,6 +704,7 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map r.duration === -1)) + let status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none'; + if (test.results.some(r => r.workerIndex === -1)) + status = 'scheduled'; + else if (test.results.some(r => r.duration === -1)) status = 'running'; else if (test.results.length && test.results[0].status === 'skipped') status = 'skipped'; @@ -758,8 +773,10 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map a + b.duration, 0); } }; @@ -788,16 +805,20 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map 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) @@ -814,15 +835,17 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map) { +function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map, runningTestIds: Set | undefined) { const tokens = filterText.trim().toLowerCase().split(' '); const filtersStatuses = [...statusFilters.values()].some(Boolean); const filter = (testCase: TestCaseItem) => { const title = testCase.tests[0].titlePath().join(' ').toLowerCase(); - if (!tokens.every(token => title.includes(token))) + if (!tokens.every(token => title.includes(token)) && !testCase.tests.some(t => runningTestIds?.has(t.id))) return false; - testCase.children = (testCase.children as TestItem[]).filter(test => !filtersStatuses || statusFilters.get(test.status)); + testCase.children = (testCase.children as TestItem[]).filter(test => { + return !filtersStatuses || runningTestIds?.has(test.id) || statusFilters.get(test.status); + }); testCase.tests = (testCase.children as TestItem[]).map(c => c.test); return !!testCase.children.length; }; diff --git a/packages/web/src/components/codeMirrorWrapper.css b/packages/web/src/components/codeMirrorWrapper.css index 2adf030c08..5abb4cc722 100644 --- a/packages/web/src/components/codeMirrorWrapper.css +++ b/packages/web/src/components/codeMirrorWrapper.css @@ -122,8 +122,11 @@ body.dark-mode .CodeMirror span.cm-type { border-right: none; } +.CodeMirror .CodeMirror-gutter-elt { + background-color: var(--vscode-editorGutter-background); +} + .CodeMirror .CodeMirror-gutterwrapper { - background: var(--vscode-editor-background); border-right: 1px solid var(--vscode-editorGroup-border); color: var(--vscode-editorLineNumber-foreground); } diff --git a/tests/playwright-test/ui-mode-fixtures.ts b/tests/playwright-test/ui-mode-fixtures.ts index 792e0853b1..b7908ae97b 100644 --- a/tests/playwright-test/ui-mode-fixtures.ts +++ b/tests/playwright-test/ui-mode-fixtures.ts @@ -26,6 +26,7 @@ import { createGuid } from '../../packages/playwright-core/src/utils/crypto'; type Latch = { blockingCode: string; open: () => void; + close: () => void; }; type Fixtures = { @@ -33,8 +34,8 @@ type Fixtures = { createLatch: () => Latch; }; -export function dumpTestTree(page: Page): () => Promise { - return () => page.getByTestId('test-tree').evaluate(async treeElement => { +export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () => Promise { + return () => page.getByTestId('test-tree').evaluate(async (treeElement, options) => { function iconName(iconElement: Element): string { const icon = iconElement.className.replace('codicon codicon-', ''); if (icon === 'chevron-right') @@ -55,6 +56,8 @@ export function dumpTestTree(page: Page): () => Promise { return '👁'; if (icon === 'loading') return '↻'; + if (icon === 'clock') + return '🕦'; return icon; } @@ -67,10 +70,13 @@ export function dumpTestTree(page: Page): () => Promise { const indent = listItem.querySelectorAll('.list-view-indent').length; const watch = listItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : ''; const selected = listItem.classList.contains('selected') ? ' <=' : ''; - result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + listItem.textContent + watch + selected); + const title = listItem.querySelector('.watch-mode-list-item-title').textContent; + const timeElement = options.time ? listItem.querySelector('.watch-mode-list-item-time') : undefined; + const time = timeElement ? ' ' + timeElement.textContent.replace(/\d+m?s/, 'XXms') : ''; + result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected); } return '\n' + result.join('\n') + '\n '; - }); + }, options); } export const test = base @@ -112,6 +118,7 @@ export const test = base return { blockingCode: `await ((${waitForLatch})(${JSON.stringify(latchFile)}))`, open: () => fs.writeFileSync(latchFile, 'ok'), + close: () => fs.unlinkSync(latchFile), }; }); }, diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts index ce9c9d6e93..4adc151c62 100644 --- a/tests/playwright-test/ui-mode-test-filters.spec.ts +++ b/tests/playwright-test/ui-mode-test-filters.spec.ts @@ -148,3 +148,33 @@ test('should filter by project', async ({ runUITest }) => { await expect(page.getByText('Projects: foo bar')).toBeVisible(); }); + +test('should not hide filtered while running', async ({ runUITest, createLatch }) => { + const latch = createLatch(); + const page = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', async () => { + ${latch.blockingCode} + expect(1).toBe(2); + }); + `, + }); + await page.getByTitle('Run all').click(); + latch.open(); + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ❌ a.test.ts + ✅ passes + ❌ fails <= + `); + + latch.close(); + await page.getByText('Status:').click(); + await page.getByLabel('failed').setChecked(true); + await page.getByTitle('Run all').click(); + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ↻ a.test.ts + ↻ fails <= + `); +}); diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index 1c526961a5..96c1d08ea1 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -238,7 +238,7 @@ test('should stop', async ({ runUITest }) => { ⊘ test 0 ✅ test 1 ↻ test 2 - ↻ test 3 + 🕦 test 3 `); await expect(page.getByTitle('Run all')).toBeDisabled(); @@ -282,3 +282,27 @@ test('should run folder', async ({ runUITest }) => { ◯ passes `); }); + +test('should show time', async ({ runUITest }) => { + const page = await runUITest(basicTestTree); + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` + ▼ ◯ a.test.ts + `); + + await page.getByTitle('Run all').click(); + + await expect.poll(dumpTestTree(page, { time: true }), { timeout: 15000 }).toBe(` + ▼ ❌ a.test.ts + ✅ passes XXms + ❌ fails XXms <= + ► ❌ suite + ▼ ❌ b.test.ts + ✅ passes XXms + ❌ fails XXms + ▼ ✅ c.test.ts + ✅ passes XXms + ⊘ skipped + `); + + await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)'); +});