diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 8d2f432faa..3389179759 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -87,7 +87,8 @@ export async function showTraceViewer(traceUrls: string[], browserName: string, if (traceViewerBrowser === 'chromium') await installAppIcon(page); - await syncLocalStorageWithSettings(page, 'traceviewer'); + if (!isUnderTest()) + await syncLocalStorageWithSettings(page, 'traceviewer'); const params = traceUrls.map(t => `trace=${t}`); if (isUnderTest()) { diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index a1261c65e7..3c8065c5e0 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -62,7 +62,7 @@ class UIMode { projectDirs.add(p.testDir); let coalescingTimer: NodeJS.Timeout | undefined; const watcher = chokidar.watch([...projectDirs], { ignoreInitial: true, persistent: true }).on('all', async event => { - if (event !== 'add' && event !== 'change') + if (event !== 'add' && event !== 'change' && event !== 'unlink') return; if (coalescingTimer) clearTimeout(coalescingTimer); diff --git a/packages/trace-viewer/src/ui/watchMode.css b/packages/trace-viewer/src/ui/watchMode.css index 120e21a86a..8c09bff1b1 100644 --- a/packages/trace-viewer/src/ui/watchMode.css +++ b/packages/trace-viewer/src/ui/watchMode.css @@ -72,7 +72,7 @@ margin-top: 5px; } -.list-view-entry:not(.highlighted) .toolbar-button:not(.toggled) { +.list-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) { display: none; } diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index 66e5ade1f1..4349248cd1 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -149,7 +149,7 @@ export const WatchModeView: React.FC<{}> = ({
setSettingsVisible(false)}>Tests
- runTests(visibleTestIds)} disabled={isRunningTest}> + runTests(visibleTestIds)} disabled={isRunningTest}> sendMessageNoReply('stop')} disabled={!isRunningTest}> refreshRootSuite(true)} disabled={isRunningTest}>
@@ -202,7 +202,7 @@ export const WatchModeView: React.FC<{}> = ({
; }; -const TreeListView = TreeView; +const TestTreeView = TreeView; export const TestList: React.FC<{ projects: Map, @@ -299,7 +299,7 @@ export const TestList: React.FC<{ if (!isVisible) return <>; - return ; }; diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index 95f9ddea7b..dd3f9fc4e4 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -40,6 +40,7 @@ export type TreeViewProps = { dataTestId?: string, treeState: TreeState, setTreeState: (treeState: TreeState) => void, + autoExpandDeep?: boolean, }; const TreeListView = ListView; @@ -57,12 +58,13 @@ export function TreeView({ setTreeState, noItemsMessage, dataTestId, + autoExpandDeep, }: TreeViewProps) { const treeItems = React.useMemo(() => { for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent) treeState.expandedItems.set(item.id, true); - return flattenTree(rootItem, treeState.expandedItems); - }, [rootItem, selectedItem, treeState]); + return flattenTree(rootItem, treeState.expandedItems, autoExpandDeep); + }, [rootItem, selectedItem, treeState, autoExpandDeep]); return (rootItem: T, expandedItems: Map): Map { +function flattenTree(rootItem: T, expandedItems: Map, autoExpandDeep?: boolean): Map { const result = new Map(); const appendChildren = (parent: T, depth: number) => { for (const item of parent.children as T[]) { const expandState = expandedItems.get(item.id); - const autoExpandMatches = depth === 0 && result.size < 25 && expandState !== false; + const autoExpandMatches = (autoExpandDeep || depth === 0) && result.size < 25 && expandState !== false; const expanded = item.children.length ? expandState || autoExpandMatches : undefined; result.set(item, { depth, expanded, parent: rootItem === parent ? null : parent }); if (expanded) diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index f7272ebeef..80356e3876 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -223,6 +223,7 @@ export type RunOptions = { }; type Fixtures = { writeFiles: (files: Files) => Promise; + deleteFile: (file: string) => Promise; runInlineTest: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise; runWatchTest: (files: Files, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise; runTSC: (files: Files) => Promise; @@ -237,6 +238,13 @@ export const test = base await use(files => writeFiles(testInfo, files, false)); }, + deleteFile: async ({}, use, testInfo) => { + await use(async file => { + const baseDir = testInfo.outputPath(); + await fs.promises.unlink(path.join(baseDir, file)); + }); + }, + runInlineTest: async ({ childProcess }, use, testInfo: TestInfo) => { const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-')); await use(async (files: Files, params: Params = {}, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => { diff --git a/tests/playwright-test/ui-mode-fixtures.ts b/tests/playwright-test/ui-mode-fixtures.ts index b52ad77f1f..b09654d64c 100644 --- a/tests/playwright-test/ui-mode-fixtures.ts +++ b/tests/playwright-test/ui-mode-fixtures.ts @@ -38,10 +38,16 @@ export function dumpTestTree(page: Page): () => Promise { return ' '; if (icon === 'circle-outline') return '◯'; + if (icon === 'circle-slash') + return '⊘'; if (icon === 'check') return '✅'; if (icon === 'error') return '❌'; + if (icon === 'eye') + return '👁'; + if (icon === 'loading') + return '↻'; return icon; } @@ -52,8 +58,9 @@ export function dumpTestTree(page: Page): () => Promise { const treeIcon = iconName(iconElements[0]); const statusIcon = iconName(iconElements[1]); 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 + selected); + result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + listItem.textContent + watch + selected); } return '\n' + result.join('\n') + '\n '; }); diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts new file mode 100644 index 0000000000..a897b1258e --- /dev/null +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, dumpTestTree } from './ui-mode-fixtures'; +test.describe.configure({ mode: 'parallel' }); + +const basicTestTree = { + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => { expect(1).toBe(2); }); + test.describe('suite', () => { + test('inner passes', () => {}); + test('inner fails', () => { expect(1).toBe(2); }); + }); + `, + 'b.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => { expect(1).toBe(2); }); + `, + 'c.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test.skip('skipped', () => {}); + `, +}; + +test('should run visible', async ({ runUITest }) => { + const page = await runUITest(basicTestTree); + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + ▼ ◯ a.test.ts + `); + + await page.getByTitle('Run all').click(); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ❌ a.test.ts + ✅ passes + ❌ fails <= + ► ❌ suite + ▼ ❌ b.test.ts + ✅ passes + ❌ fails + ▼ ✅ c.test.ts + ✅ passes + ⊘ skipped + `); +}); + +test('should run on double click', async ({ runUITest }) => { + const page = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => { expect(1).toBe(2); }); + `, + }); + + await page.getByText('passes').dblclick(); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ a.test.ts + ✅ passes <= + ◯ fails + `); +}); + +test('should run on Enter', async ({ runUITest }) => { + const page = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => { expect(1).toBe(2); }); + `, + }); + + await page.getByText('fails').click(); + await page.keyboard.press('Enter'); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ❌ a.test.ts + ◯ passes + ❌ fails <= + `); +}); diff --git a/tests/playwright-test/ui-mode-test-tree.spec.ts b/tests/playwright-test/ui-mode-test-tree.spec.ts index ed8579fe42..af06f88a20 100644 --- a/tests/playwright-test/ui-mode-test-tree.spec.ts +++ b/tests/playwright-test/ui-mode-test-tree.spec.ts @@ -37,7 +37,7 @@ const basicTestTree = { test('should list tests', async ({ runUITest }) => { const page = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page)).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -51,7 +51,7 @@ test('should list tests', async ({ runUITest }) => { test('should traverse up/down', async ({ runUITest }) => { const page = await runUITest(basicTestTree); await page.getByText('a.test.ts').click(); - await expect.poll(dumpTestTree(page)).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` ▼ ◯ a.test.ts <= ◯ passes ◯ fails @@ -59,14 +59,14 @@ test('should traverse up/down', async ({ runUITest }) => { `); await page.keyboard.press('ArrowDown'); - await expect.poll(dumpTestTree(page)).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` ▼ ◯ a.test.ts ◯ passes <= ◯ fails ► ◯ suite `); await page.keyboard.press('ArrowDown'); - await expect.poll(dumpTestTree(page)).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` ▼ ◯ a.test.ts ◯ passes ◯ fails <= @@ -74,7 +74,7 @@ test('should traverse up/down', async ({ runUITest }) => { `); await page.keyboard.press('ArrowUp'); - await expect.poll(dumpTestTree(page)).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` ▼ ◯ a.test.ts ◯ passes <= ◯ fails @@ -87,7 +87,7 @@ test('should expand / collapse groups', async ({ runUITest }) => { await page.getByText('suite').click(); await page.keyboard.press('ArrowRight'); - await expect.poll(dumpTestTree(page)).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -97,7 +97,7 @@ test('should expand / collapse groups', async ({ runUITest }) => { `); await page.keyboard.press('ArrowLeft'); - await expect.poll(dumpTestTree(page)).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -106,14 +106,25 @@ test('should expand / collapse groups', async ({ runUITest }) => { await page.getByText('passes').first().click(); await page.keyboard.press('ArrowLeft'); - await expect.poll(dumpTestTree(page)).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` ▼ ◯ a.test.ts <= ◯ passes ◯ fails `); await page.keyboard.press('ArrowLeft'); - await expect.poll(dumpTestTree(page)).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` ► ◯ a.test.ts <= `); }); + +test('should filter by title', async ({ runUITest }) => { + const page = await runUITest(basicTestTree); + await page.getByPlaceholder('Filter').fill('inner'); + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ a.test.ts + ▼ ◯ suite + ◯ inner passes + ◯ inner fails + `); +}); diff --git a/tests/playwright-test/ui-mode-test-update.spec.ts b/tests/playwright-test/ui-mode-test-update.spec.ts new file mode 100644 index 0000000000..82c5c9028f --- /dev/null +++ b/tests/playwright-test/ui-mode-test-update.spec.ts @@ -0,0 +1,170 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, dumpTestTree } from './ui-mode-fixtures'; + +test.describe.configure({ mode: 'parallel' }); + +const basicTestTree = { + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => {}); + test.describe('suite', () => { + test('inner passes', () => {}); + test('inner fails', () => {}); + }); + `, + 'b.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => {}); + `, +}; + +test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFile }) => { + const page = await runUITest(basicTestTree); + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ► ◯ suite + ▼ ◯ b.test.ts + ◯ passes + ◯ fails + `); + + await writeFiles({ + 'c.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => {}); + ` + }); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ► ◯ suite + ▼ ◯ b.test.ts + ◯ passes + ◯ fails + ▼ ◯ c.test.ts + ◯ passes + ◯ fails + `); + + await deleteFile('a.test.ts'); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ b.test.ts + ◯ passes + ◯ fails + ▼ ◯ c.test.ts + ◯ passes + ◯ fails + `); +}); + +test('should pick new / deleted tests', async ({ runUITest, writeFiles, deleteFile }) => { + const page = await runUITest(basicTestTree); + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ► ◯ suite + ▼ ◯ b.test.ts + ◯ passes + ◯ fails + `); + + await writeFiles({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('new', () => {}); + test('fails', () => {}); + ` + }); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ a.test.ts + ◯ passes + ◯ new + ◯ fails + ▼ ◯ b.test.ts + ◯ passes + ◯ fails + `); + + await deleteFile('a.test.ts'); + + await writeFiles({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('new', () => {}); + ` + }); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ a.test.ts + ◯ new + ▼ ◯ b.test.ts + ◯ passes + ◯ fails + `); +}); + +test('should pick new / deleted nested tests', async ({ runUITest, writeFiles, deleteFile }) => { + const page = await runUITest(basicTestTree); + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ► ◯ suite + `); + + await page.getByText('suite').click(); + await page.keyboard.press('ArrowRight'); + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ▼ ◯ suite <= + ◯ inner passes + ◯ inner fails + `); + + await writeFiles({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test.describe('suite', () => { + test('inner new', () => {}); + test('inner fails', () => {}); + }); + ` + }); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + ▼ ◯ a.test.ts + ◯ passes + ▼ ◯ suite <= + ◯ inner new + ◯ inner fails + `); +}); diff --git a/tests/playwright-test/ui-mode-test-watch.spec.ts b/tests/playwright-test/ui-mode-test-watch.spec.ts new file mode 100644 index 0000000000..eeb0044726 --- /dev/null +++ b/tests/playwright-test/ui-mode-test-watch.spec.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, dumpTestTree } from './ui-mode-fixtures'; + +test.describe.configure({ mode: 'parallel' }); + +test('should watch files', async ({ runUITest, writeFiles }) => { + const page = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => { expect(1).toBe(2); }); + `, + }); + + await page.getByText('fails').click(); + await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Watch').click(); + await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Run').click(); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ❌ a.test.ts + ◯ passes + ❌ fails 👁 <= + `); + + await writeFiles({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + test('fails', () => { expect(1).toBe(1); }); + ` + }); + + await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + ▼ ◯ a.test.ts + ◯ passes + ✅ fails 👁 <= + `); +});