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 👁 <=
+ `);
+});