From f5fa18a27988999fc3f9f2bc4b7b53cf19e3c8cd Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 13 Mar 2023 22:19:31 -0700 Subject: [PATCH] chore: reimplement filters (#21647) --- packages/playwright-test/src/runner/uiMode.ts | 2 + .../src/ui/networkResourceDetails.tsx | 2 +- packages/trace-viewer/src/ui/watchMode.css | 94 ++++-- packages/trace-viewer/src/ui/watchMode.tsx | 287 +++++++++--------- .../web/src/components/codeMirrorWrapper.css | 3 +- packages/web/src/components/expandable.css | 31 ++ packages/web/src/components/expandable.tsx | 10 +- packages/web/src/uiUtils.ts | 16 + tests/playwright-test/ui-mode-fixtures.ts | 3 +- .../ui-mode-test-filters.spec.ts | 150 +++++++++ .../playwright-test/ui-mode-test-run.spec.ts | 87 +++++- .../playwright-test/ui-mode-test-tree.spec.ts | 29 +- .../ui-mode-test-update.spec.ts | 18 +- .../ui-mode-test-watch.spec.ts | 4 +- 14 files changed, 515 insertions(+), 221 deletions(-) create mode 100644 packages/web/src/components/expandable.css create mode 100644 tests/playwright-test/ui-mode-test-filters.spec.ts diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index 3c8065c5e0..bfd7f79b87 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -46,6 +46,8 @@ class UIMode { config._internal.configCLIOverrides.updateSnapshots = undefined; config._internal.listOnly = false; config._internal.passWithNoTests = true; + for (const project of config.projects) + project._internal.deps = []; for (const p of config.projects) p.retries = 0; diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index ea46cccc00..69310402e1 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -125,7 +125,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{ return
setSelected(index)}> - +
{resource.time}ms
URL
diff --git a/packages/trace-viewer/src/ui/watchMode.css b/packages/trace-viewer/src/ui/watchMode.css index 8c09bff1b1..31f57f00e1 100644 --- a/packages/trace-viewer/src/ui/watchMode.css +++ b/packages/trace-viewer/src/ui/watchMode.css @@ -40,15 +40,31 @@ overflow: hidden } +.watch-mode-sidebar .toolbar { + min-height: 24px; +} + .watch-mode-sidebar .toolbar-button { margin: 0; } .watch-mode .section-title { + display: flex; + flex-direction: row; + align-items: center; font-size: 11px; text-transform: uppercase; font-weight: bold; - margin: 5px; + text-overflow: ellipsis; + overflow: hidden; + padding: 8px; +} + +.watch-mode img { + flex: none; + margin: 0 4px; + width: 24px; + height: 24px; } .status-line { @@ -68,38 +84,12 @@ margin: 0 5px; } -.list-view { - margin-top: 5px; -} - .list-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) { display: none; } -.filters { - flex: none; - margin-left: 5px; - line-height: 24px; -} - -.filters > span { - color: var(--vscode-panelTitle-inactiveForeground); - padding-left: 3px; -} - -.filters > div { - display: inline-block; - margin: 0 5px; - user-select: none; - cursor: pointer; -} - -.filters > div:hover, -.filters > div.filters-toggled { - color: var(--vscode-notificationLink-foreground); -} - .watch-mode-sidebar input[type=search] { + flex: auto; padding: 0 5px; line-height: 24px; outline: none; @@ -108,3 +98,51 @@ color: var(--vscode-input-foreground); background-color: var(--vscode-input-background); } + +.filters { + flex: none; + display: flex; + flex-direction: column; + margin-top: 8px; +} + +.filter-title, +.filter-summary { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: none; + cursor: pointer; +} + +.filter-label { + color: var(--vscode-disabledForeground); +} + +.filter-summary { + line-height: 24px; + margin-top: 2px; + margin-left: 20px; +} + +.filter-summary .filter-label { + margin-left: 5px; +} + +.filter-entry label { + display: flex; + align-items: center; + cursor: pointer; +} + +.filter-entry input { + flex: none; + display: flex; + align-items: center; + cursor: pointer; +} + +.filter-entry label div { + overflow: hidden; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index 4349248cd1..124317275a 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -20,7 +20,7 @@ import '@web/common.css'; import React from 'react'; import { TreeView } from '@web/components/treeView'; import type { TreeState } from '@web/components/treeView'; -import { TeleReporterReceiver } from '../../../playwright-test/src/isomorphic/teleReceiver'; +import { TeleReporterReceiver, TeleSuite } from '../../../playwright-test/src/isomorphic/teleReceiver'; import type { TeleTestCase } from '../../../playwright-test/src/isomorphic/teleReceiver'; import type { FullConfig, Suite, TestCase, TestResult, TestStep, Location } from '../../../playwright-test/types/testReporter'; import { SplitView } from '@web/components/splitView'; @@ -28,12 +28,12 @@ import { MultiTraceModel } from './modelUtil'; import './watchMode.css'; import { ToolbarButton } from '@web/components/toolbarButton'; import { Toolbar } from '@web/components/toolbar'; -import { toggleTheme } from '@web/theme'; import type { ContextEntry } from '../entries'; import type * as trace from '@trace/trace'; import type { XtermDataSource } from '@web/components/xtermWrapper'; import { XtermWrapper } from '@web/components/xtermWrapper'; import { Expandable } from '@web/components/expandable'; +import { toggleTheme } from '@web/theme'; let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {}; let updateStepsProgress: () => void = () => {}; @@ -52,39 +52,51 @@ const xtermDataSource: XtermDataSource = { export const WatchModeView: React.FC<{}> = ({ }) => { - const [filterText, setFilterText] = useSetting('test-ui-filter-text', ''); - const [filterExpanded, setFilterExpanded] = useSetting('test-ui-filter-expanded', false); - const [isShowingOutput, setIsShowingOutput] = useSetting('test-ui-show-output', false); + const [filterText, setFilterText] = React.useState(''); + const [isShowingOutput, setIsShowingOutput] = React.useState(false); - const [projects, setProjects] = React.useState>(new Map()); + const [statusFilters, setStatusFilters] = React.useState>(new Map([ + ['passed', false], + ['failed', false], + ['skipped', false], + ])); + const [projectFilters, setProjectFilters] = React.useState>(new Map()); const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined }); const [progress, setProgress] = React.useState({ total: 0, passed: 0, failed: 0, skipped: 0 }); const [selectedTest, setSelectedTest] = React.useState(undefined); - const [settingsVisible, setSettingsVisible] = React.useState(false); const [visibleTestIds, setVisibleTestIds] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(false); const [runningState, setRunningState] = React.useState<{ testIds: Set, itemSelectedByUser?: boolean }>(); const inputRef = React.useRef(null); + const reloadTests = () => { + setIsLoading(true); + updateRootSuite(new TeleSuite('', 'root'), { total: 0, passed: 0, failed: 0, skipped: 0 }); + refreshRootSuite(true).then(() => { + setIsLoading(false); + }); + }; + React.useEffect(() => { inputRef.current?.focus(); - refreshRootSuite(true); + reloadTests(); }, []); updateRootSuite = (rootSuite: Suite, newProgress: Progress) => { - for (const projectName of projects.keys()) { + for (const projectName of projectFilters.keys()) { if (!rootSuite.suites.find(s => s.title === projectName)) - projects.delete(projectName); + projectFilters.delete(projectName); } for (const projectSuite of rootSuite.suites) { - if (!projects.has(projectSuite.title)) - projects.set(projectSuite.title, false); + if (!projectFilters.has(projectSuite.title)) + projectFilters.set(projectSuite.title, false); } - if (![...projects.values()].includes(true)) - projects.set(projects.entries().next().value[0], true); + if (projectFilters.size && ![...projectFilters.values()].includes(true)) + projectFilters.set(projectFilters.entries().next().value[0], true); setRootSuite({ value: rootSuite }); - setProjects(new Map(projects)); + setProjectFilters(new Map(projectFilters)); setProgress(newProgress); }; @@ -108,24 +120,6 @@ export const WatchModeView: React.FC<{}> = ({ }); }; - const updateFilter = (name: string, value: string) => { - const result: string[] = []; - const prefix = name + ':'; - for (const t of filterText.split(' ')) { - if (t.startsWith(prefix)) { - if (value) { - result.push(prefix + value); - value = ''; - } - } else { - result.push(t); - } - } - if (value) - result.unshift(prefix + value); - setFilterText(result.join(' ')); - }; - const isRunningTest = !!runningState; const result = selectedTest?.results[0]; const isFinished = result && result.duration >= 0; @@ -134,7 +128,6 @@ export const WatchModeView: React.FC<{}> = ({
-
Output
xtermDataSource.clear()}>
setIsShowingOutput(false)}> @@ -148,52 +141,42 @@ export const WatchModeView: React.FC<{}> = ({
-
setSettingsVisible(false)}>Tests
- runTests(visibleTestIds)} disabled={isRunningTest}> - sendMessageNoReply('stop')} disabled={!isRunningTest}> - refreshRootSuite(true)} disabled={isRunningTest}> + +
Playwright
- { setSettingsVisible(!settingsVisible); }}> - { setIsShowingOutput(!isShowingOutput); }}> + toggleTheme()} /> + reloadTests()} disabled={isRunningTest || isLoading}> + { setIsShowingOutput(!isShowingOutput); }} /> +
+ runTests(visibleTestIds)} /> + +
Tests
+
+ runTests(visibleTestIds)} disabled={isRunningTest || isLoading}> + sendMessageNoReply('stop')} disabled={!isRunningTest || isLoading}>
- {!settingsVisible && { - setFilterText(e.target.value); - }} - onKeyDown={e => { - if (e.key === 'Enter') - runTests(visibleTestIds); - }}>} - style={{ flex: 'none', marginTop: 8 }} - expanded={filterExpanded} - setExpanded={setFilterExpanded}> -
- Status: -
updateFilter('s', '')}>all
- {['failed', 'passed', 'skipped'].map(s =>
updateFilter('s', s)}>{s}
)} -
- {[...projects.values()].filter(v => v).length > 1 &&
- Project: -
updateFilter('p', '')}>all
- {[...projects].filter(([k, v]) => v).map(([k, v]) => k).map(p =>
updateFilter('p', p)}>{p}
)} -
} -
} - {settingsVisible && setSettingsVisible(false)}>}
Total: {progress.total}
- {isRunningTest &&
Running {visibleTestIds.length}
} + {isRunningTest &&
{`Running ${visibleTestIds.length}\u2026`}
} + {isLoading &&
{'Loading\u2026'}
} {!isRunningTest &&
Showing: {visibleTestIds.length}
}
{progress.passed} passed
{progress.failed} failed
@@ -202,29 +185,89 @@ export const WatchModeView: React.FC<{}> = ({
; }; +const FiltersView: React.FC<{ + filterText: string; + setFilterText: (text: string) => void; + statusFilters: Map; + setStatusFilters: (filters: Map) => void; + projectFilters: Map; + setProjectFilters: (filters: Map) => void; + runTests: () => void; +}> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, runTests }) => { + const [expanded, setExpanded] = React.useState(false); + const inputRef = React.useRef(null); + React.useEffect(() => { + inputRef.current?.focus(); + }, []); + + const statusLine = [...statusFilters.entries()].filter(([_, v]) => v).map(([s]) => s).join(' ') || 'all'; + const projectsLine = [...projectFilters.entries()].filter(([_, v]) => v).map(([p]) => p).join(' ') || 'all'; + return
+ { + setFilterText(e.target.value); + }} + onKeyDown={e => { + if (e.key === 'Enter') + runTests(); + }} />}> + {
setExpanded(false)}>Status: {statusLine}
} + {[...statusFilters.entries()].map(([status, value]) => { + return
+ +
; + })} + + {
Projects: {projectsLine}
} + {[...projectFilters.entries()].map(([projectName, value]) => { + return
+ +
; + })} +
+ {!expanded &&
setExpanded(true)}> + Status: {statusLine} + Projects: {projectsLine} +
} +
; +}; + const TestTreeView = TreeView; -export const TestList: React.FC<{ - projects: Map, +const TestList: React.FC<{ + statusFilters: Map, + projectFilters: Map, filterText: string, rootSuite: { value: Suite | undefined }, runTests: (testIds: string[]) => void, runningState?: { testIds: Set, itemSelectedByUser?: boolean }, - isVisible: boolean, setVisibleTestIds: (testIds: string[]) => void, onTestSelected: (test: TestCase | undefined) => void, -}> = ({ projects, filterText, rootSuite, runTests, runningState, isVisible, onTestSelected, setVisibleTestIds }) => { +}> = ({ statusFilters, projectFilters, filterText, rootSuite, runTests, runningState, onTestSelected, setVisibleTestIds }) => { const [treeState, setTreeState] = React.useState({ expandedItems: new Map() }); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState(); const [watchedTreeIds] = React.useState>(new Set()); - React.useEffect(() => { - refreshRootSuite(true); - }, []); - const { rootItem, treeItemMap } = React.useMemo(() => { - const rootItem = createTree(rootSuite.value, projects); - filterTree(rootItem, filterText); + const rootItem = createTree(rootSuite.value, projectFilters); + filterTree(rootItem, filterText, statusFilters); hideOnlyTests(rootItem); const treeItemMap = new Map(); const visibleTestIds = new Set(); @@ -237,7 +280,7 @@ export const TestList: React.FC<{ visit(rootItem); setVisibleTestIds([...visibleTestIds]); return { rootItem, treeItemMap }; - }, [filterText, rootSuite, projects, setVisibleTestIds]); + }, [filterText, rootSuite, statusFilters, projectFilters, setVisibleTestIds]); React.useEffect(() => { // Look for a first failure within the run batch to select it. @@ -245,9 +288,9 @@ export const TestList: React.FC<{ return; let selectedTreeItem: TreeItem | undefined; const visit = (treeItem: TreeItem) => { + treeItem.children.forEach(visit); if (selectedTreeItem) return; - treeItem.children.forEach(visit); if (treeItem.status === 'failed') { if (treeItem.kind === 'test' && runningState.testIds.has(treeItem.test.id)) selectedTreeItem = treeItem; @@ -296,9 +339,6 @@ export const TestList: React.FC<{ runTests(testIds); }; - if (!isVisible) - return <>; - return ; }; -export const SettingsView: React.FC<{ - projects: Map, - setProjects: (projectNames: Map) => void, - onClose: () => void, -}> = ({ projects, setProjects, onClose }) => { - return
-
-
Projects
-
- -
- {[...projects.entries()].map(([projectName, value]) => { - return
- { - const copy = new Map(projects); - copy.set(projectName, !copy.get(projectName)); - if (![...copy.values()].includes(true)) - copy.set(projectName, true); - setProjects(copy); - }}/> - -
; - })} -
Appearance
-
- toggleTheme()}>Toggle color mode -
-
; -}; - -export const InProgressTraceView: React.FC<{ +const InProgressTraceView: React.FC<{ testResult: TestResult | undefined, }> = ({ testResult }) => { const [model, setModel] = React.useState(); @@ -386,7 +394,7 @@ export const InProgressTraceView: React.FC<{ return ; }; -export const FinishedTraceView: React.FC<{ +const FinishedTraceView: React.FC<{ testResult: TestResult, }> = ({ testResult }) => { const [model, setModel] = React.useState(); @@ -425,11 +433,9 @@ const throttleUpdateRootSuite = (rootSuite: Suite, progress: Progress, immediate throttleTimer = setTimeout(throttledAction, 250); }; -const refreshRootSuite = (eraseResults: boolean) => { - if (!eraseResults) { - sendMessageNoReply('list'); - return; - } +const refreshRootSuite = (eraseResults: boolean): Promise => { + if (!eraseResults) + return sendMessage('list', {}); let rootSuite: Suite; const progress: Progress = { @@ -477,12 +483,12 @@ const refreshRootSuite = (eraseResults: boolean) => { updateStepsProgress(); }, }); - sendMessageNoReply('list'); + return sendMessage('list', {}); }; (window as any).dispatch = (message: any) => { if (message.method === 'listChanged') { - refreshRootSuite(false); + refreshRootSuite(false).catch(() => {}); return; } @@ -577,7 +583,8 @@ type TestItem = TreeItemBase & { type TreeItem = GroupItem | TestCaseItem | TestItem; -function createTree(rootSuite: Suite | undefined, projects: Map): GroupItem { +function createTree(rootSuite: Suite | undefined, projectFilters: Map): GroupItem { + const filterProjects = [...projectFilters.values()].some(Boolean); const rootItem: GroupItem = { kind: 'group', id: 'root', @@ -650,7 +657,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map }; for (const projectSuite of rootSuite?.suites || []) { - if (!projects.get(projectSuite.title)) + if (filterProjects && !projectFilters.get(projectSuite.title)) continue; visitSuite(projectSuite.title, projectSuite, rootItem); } @@ -687,21 +694,15 @@ function createTree(rootSuite: Suite | undefined, projects: Map return rootItem; } -function filterTree(rootItem: GroupItem, filterText: string) { - const trimmedFilterText = filterText.trim(); - const filterTokens = trimmedFilterText.toLowerCase().split(' '); - const textTokens = filterTokens.filter(token => !token.match(/^[sp]:/)); - const statuses = new Set(filterTokens.filter(t => t.startsWith('s:')).map(t => t.substring(2))); - if (statuses.size) - statuses.add('running'); - const projects = new Set(filterTokens.filter(t => t.startsWith('p:')).map(t => t.substring(2))); +function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map) { + 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 (!textTokens.every(token => title.includes(token))) + if (!tokens.every(token => title.includes(token))) return false; - testCase.children = (testCase.children as TestItem[]).filter(test => !statuses.size || statuses.has(test.status)); - testCase.children = (testCase.children as TestItem[]).filter(test => !projects.size || projects.has(test.project)); + testCase.children = (testCase.children as TestItem[]).filter(test => !filtersStatuses || statusFilters.get(test.status)); testCase.tests = (testCase.children as TestItem[]).map(c => c.test); return !!testCase.children.length; }; @@ -803,17 +804,3 @@ function stepsToModel(result: TestResult): MultiTraceModel { return new MultiTraceModel([contextEntry]); } - -function useSetting(name: string, defaultValue: S): [S, React.Dispatch>] { - const string = localStorage.getItem(name); - let value = defaultValue; - if (string !== null) - value = JSON.parse(string); - - const [state, setState] = React.useState(value); - const setStateWrapper = (value: React.SetStateAction) => { - localStorage.setItem(name, JSON.stringify(value)); - setState(value); - }; - return [state, setStateWrapper]; -} diff --git a/packages/web/src/components/codeMirrorWrapper.css b/packages/web/src/components/codeMirrorWrapper.css index 3243456c85..987dbf7a91 100644 --- a/packages/web/src/components/codeMirrorWrapper.css +++ b/packages/web/src/components/codeMirrorWrapper.css @@ -118,7 +118,8 @@ body.dark-mode .CodeMirror span.cm-type { .CodeMirror .CodeMirror-gutters { z-index: 0; - background: var(--vscode-editorGutter-background); + background: 1px solid var(--vscode-editorGroup-border); + border-right: none; } .CodeMirror .CodeMirror-gutterwrapper { diff --git a/packages/web/src/components/expandable.css b/packages/web/src/components/expandable.css new file mode 100644 index 0000000000..ba0bb408b4 --- /dev/null +++ b/packages/web/src/components/expandable.css @@ -0,0 +1,31 @@ +/* + 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. +*/ + +.expandable { + flex: none; + flex-direction: column; + line-height: 28px; +} + +.expandable-title { + flex: none; + display: flex; + flex-direction: row; + align-items: center; + white-space: nowrap; + user-select: none; + cursor: pointer; +} diff --git a/packages/web/src/components/expandable.tsx b/packages/web/src/components/expandable.tsx index 4751381eb6..f283057408 100644 --- a/packages/web/src/components/expandable.tsx +++ b/packages/web/src/components/expandable.tsx @@ -15,21 +15,21 @@ */ import * as React from 'react'; +import './expandable.css'; export const Expandable: React.FunctionComponent> = ({ title, children, setExpanded, expanded, style }) => { - return
-
+}>> = ({ title, children, setExpanded, expanded }) => { + return
+
setExpanded(!expanded)} /> {title}
- { expanded &&
{children}
} + { expanded &&
{children}
}
; }; diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts index 1e933586d5..848593c71b 100644 --- a/packages/web/src/uiUtils.ts +++ b/packages/web/src/uiUtils.ts @@ -14,6 +14,8 @@ limitations under the License. */ +import React from 'react'; + export function msToString(ms: number): string { if (!isFinite(ms)) return '-'; @@ -76,3 +78,17 @@ export function copy(text: string) { document.execCommand('copy'); textArea.remove(); } + +export function useSetting(name: string, defaultValue: S): [S, React.Dispatch>] { + const string = localStorage.getItem(name); + let value = defaultValue; + if (string !== null) + value = JSON.parse(string); + + const [state, setState] = React.useState(value); + const setStateWrapper = (value: React.SetStateAction) => { + localStorage.setItem(name, JSON.stringify(value)); + setState(value); + }; + return [state, setStateWrapper]; +} diff --git a/tests/playwright-test/ui-mode-fixtures.ts b/tests/playwright-test/ui-mode-fixtures.ts index b09654d64c..f0d19dedb3 100644 --- a/tests/playwright-test/ui-mode-fixtures.ts +++ b/tests/playwright-test/ui-mode-fixtures.ts @@ -69,13 +69,14 @@ export function dumpTestTree(page: Page): () => Promise { export const test = base .extend({ runUITest: async ({ childProcess, playwright, headless }, use, testInfo: TestInfo) => { + testInfo.slow(); const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-')); let testProcess: TestChildProcess | undefined; let browser: Browser | undefined; await use(async (files: Files, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => { const baseDir = await writeFiles(testInfo, files, true); testProcess = childProcess({ - command: ['node', cliEntrypoint, 'ui', ...(options.additionalArgs || [])], + command: ['node', cliEntrypoint, 'ui', '--workers=1', ...(options.additionalArgs || [])], env: { ...cleanEnv(env), PWTEST_UNDER_TEST: '1', diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts new file mode 100644 index 0000000000..ce9c9d6e93 --- /dev/null +++ b/tests/playwright-test/ui-mode-test-filters.spec.ts @@ -0,0 +1,150 @@ +/** + * 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); }); + `, +}; + +test('should filter by title', async ({ runUITest }) => { + const page = await runUITest(basicTestTree); + await page.getByPlaceholder('Filter').fill('inner'); + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ◯ a.test.ts + ▼ ◯ suite + ◯ inner passes + ◯ inner fails + `); +}); + +test('should filter by status', async ({ runUITest }) => { + const page = await runUITest(basicTestTree); + + await page.getByTitle('Run all').click(); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ❌ a.test.ts + ✅ passes + ❌ fails <= + ► ❌ suite + ▼ ❌ b.test.ts + ✅ passes + ❌ fails + `); + + await expect(page.getByText('Status: all')).toBeVisible(); + + await page.getByText('Status:').click(); + await page.getByLabel('failed').setChecked(true); + await expect(page.getByText('Status: failed')).toBeVisible(); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ❌ a.test.ts + ❌ fails <= + ► ❌ suite + ▼ ❌ b.test.ts + ❌ fails + `); + + await page.getByLabel('passed').setChecked(true); + await expect(page.getByText('Status: passed failed')).toBeVisible(); + + await expect.poll(dumpTestTree(page), { timeout: 5000 }).toBe(` + ▼ ❌ a.test.ts + ✅ passes + ❌ fails <= + ► ❌ suite + ▼ ❌ b.test.ts + ✅ passes + ❌ fails + `); + +}); + +test('should filter by project', async ({ runUITest }) => { + const page = await runUITest({ + ...basicTestTree, + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + export default defineConfig({ + projects: [ + { name: 'foo' }, + { name: 'bar' }, + ], + }); + ` + }); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ► ◯ suite + ▼ ◯ b.test.ts + ◯ passes + ◯ fails + `); + + await expect(page.getByText('Projects: foo')).toBeVisible(); + + await page.getByText('Status:').click(); + await expect(page.getByLabel('foo')).toBeChecked(); + await expect(page.getByLabel('bar')).not.toBeChecked(); + await page.getByLabel('bar').setChecked(true); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ◯ a.test.ts + ► ◯ passes + ► ◯ fails + ► ◯ suite + ▼ ◯ b.test.ts + ► ◯ passes + ► ◯ fails + `); + + await page.getByText('passes').first().click(); + await page.keyboard.press('ArrowRight'); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ◯ a.test.ts + ▼ ◯ passes <= + ◯ foo + ◯ bar + ► ◯ fails + ► ◯ suite + ▼ ◯ b.test.ts + ► ◯ passes + ► ◯ fails + `); + + await expect(page.getByText('Projects: foo bar')).toBeVisible(); +}); diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index a897b1258e..ac8c41ed0b 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -41,13 +41,13 @@ const basicTestTree = { test('should run visible', async ({ runUITest }) => { const page = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` ▼ ◯ a.test.ts `); await page.getByTitle('Run all').click(); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ❌ a.test.ts ✅ passes ❌ fails <= @@ -72,7 +72,7 @@ test('should run on double click', async ({ runUITest }) => { await page.getByText('passes').dblclick(); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ◯ a.test.ts ✅ passes <= ◯ fails @@ -91,9 +91,88 @@ test('should run on Enter', async ({ runUITest }) => { await page.getByText('fails').click(); await page.keyboard.press('Enter'); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ❌ a.test.ts ◯ passes ❌ fails <= `); }); + +test('should run by project', async ({ runUITest }) => { + const page = await runUITest({ + ...basicTestTree, + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + export default defineConfig({ + projects: [ + { name: 'foo' }, + { name: 'bar' }, + ], + }); + ` + }); + + await page.getByTitle('Run all').click(); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ❌ a.test.ts + ✅ passes + ❌ fails <= + ► ❌ suite + ▼ ❌ b.test.ts + ✅ passes + ❌ fails + ▼ ✅ c.test.ts + ✅ passes + ⊘ skipped + `); + + await page.getByText('Status:').click(); + await page.getByLabel('bar').setChecked(true); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ❌ a.test.ts + ► ◯ passes + ► ❌ fails <= + ► ❌ suite + ▼ ❌ b.test.ts + ► ◯ passes + ► ❌ fails + ▼ ◯ c.test.ts + ► ◯ passes + ► ◯ skipped + `); + + await page.getByText('Status:').click(); + + await page.getByTestId('test-tree').getByText('passes').first().click(); + await page.keyboard.press('ArrowRight'); + + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` + ▼ ❌ a.test.ts + ▼ ◯ passes <= + ✅ foo + ◯ bar + ► ❌ fails + `); + + await expect(page.getByText('Projects: foo bar')).toBeVisible(); + + await page.getByTitle('Run all').click(); + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` + ▼ ❌ a.test.ts + ▼ ✅ passes + ✅ foo + ✅ bar + ▼ ❌ fails + ❌ foo <= + ❌ bar + ► ❌ suite + ▼ ❌ b.test.ts + ► ✅ passes + ► ❌ fails + ▼ ✅ c.test.ts + ► ✅ passes + ► ⊘ skipped + `); +}); diff --git a/tests/playwright-test/ui-mode-test-tree.spec.ts b/tests/playwright-test/ui-mode-test-tree.spec.ts index af06f88a20..c85d5cbf9b 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), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).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), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).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), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` ▼ ◯ a.test.ts ◯ passes <= ◯ fails ► ◯ suite `); await page.keyboard.press('ArrowDown'); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).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), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).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), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).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), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -106,25 +106,14 @@ test('should expand / collapse groups', async ({ runUITest }) => { await page.getByText('passes').first().click(); await page.keyboard.press('ArrowLeft'); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` ▼ ◯ a.test.ts <= ◯ passes ◯ fails `); await page.keyboard.press('ArrowLeft'); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).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 index 82c5c9028f..8d64a3b50c 100644 --- a/tests/playwright-test/ui-mode-test-update.spec.ts +++ b/tests/playwright-test/ui-mode-test-update.spec.ts @@ -37,7 +37,7 @@ const basicTestTree = { test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFile }) => { const page = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -55,7 +55,7 @@ test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFi ` }); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -70,7 +70,7 @@ test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFi await deleteFile('a.test.ts'); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ◯ b.test.ts ◯ passes ◯ fails @@ -82,7 +82,7 @@ test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFi test('should pick new / deleted tests', async ({ runUITest, writeFiles, deleteFile }) => { const page = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -101,7 +101,7 @@ test('should pick new / deleted tests', async ({ runUITest, writeFiles, deleteFi ` }); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ◯ a.test.ts ◯ passes ◯ new @@ -120,7 +120,7 @@ test('should pick new / deleted tests', async ({ runUITest, writeFiles, deleteFi ` }); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ◯ a.test.ts ◯ new ▼ ◯ b.test.ts @@ -131,7 +131,7 @@ test('should pick new / deleted tests', async ({ runUITest, writeFiles, deleteFi test('should pick new / deleted nested tests', async ({ runUITest, writeFiles, deleteFile }) => { const page = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -140,7 +140,7 @@ test('should pick new / deleted nested tests', async ({ runUITest, writeFiles, d await page.getByText('suite').click(); await page.keyboard.press('ArrowRight'); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` ▼ ◯ a.test.ts ◯ passes ◯ fails @@ -160,7 +160,7 @@ test('should pick new / deleted nested tests', async ({ runUITest, writeFiles, d ` }); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(` ▼ ◯ a.test.ts ◯ passes ▼ ◯ suite <= diff --git a/tests/playwright-test/ui-mode-test-watch.spec.ts b/tests/playwright-test/ui-mode-test-watch.spec.ts index eeb0044726..4ea90196a6 100644 --- a/tests/playwright-test/ui-mode-test-watch.spec.ts +++ b/tests/playwright-test/ui-mode-test-watch.spec.ts @@ -31,7 +31,7 @@ test('should watch files', async ({ runUITest, writeFiles }) => { 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(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ❌ a.test.ts ◯ passes ❌ fails 👁 <= @@ -45,7 +45,7 @@ test('should watch files', async ({ runUITest, writeFiles }) => { ` }); - await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` + await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(` ▼ ◯ a.test.ts ◯ passes ✅ fails 👁 <=