chore: reimplement filters (#21647)

This commit is contained in:
Pavel Feldman 2023-03-13 22:19:31 -07:00 committed by GitHub
parent ccd5d7fd10
commit f5fa18a279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 515 additions and 221 deletions

View File

@ -46,6 +46,8 @@ class UIMode {
config._internal.configCLIOverrides.updateSnapshots = undefined; config._internal.configCLIOverrides.updateSnapshots = undefined;
config._internal.listOnly = false; config._internal.listOnly = false;
config._internal.passWithNoTests = true; config._internal.passWithNoTests = true;
for (const project of config.projects)
project._internal.deps = [];
for (const p of config.projects) for (const p of config.projects)
p.retries = 0; p.retries = 0;

View File

@ -125,7 +125,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
return <div return <div
className={'network-request ' + (selected ? 'selected' : '')} onClick={() => setSelected(index)}> className={'network-request ' + (selected ? 'selected' : '')} onClick={() => setSelected(index)}>
<Expandable expanded={expanded} setExpanded={setExpanded} style={{ width: '100%' }} title={ renderTitle() }> <Expandable expanded={expanded} setExpanded={setExpanded} title={ renderTitle() }>
<div className='network-request-details'> <div className='network-request-details'>
<div className='network-request-details-time'>{resource.time}ms</div> <div className='network-request-details-time'>{resource.time}ms</div>
<div className='network-request-details-header'>URL</div> <div className='network-request-details-header'>URL</div>

View File

@ -40,15 +40,31 @@
overflow: hidden overflow: hidden
} }
.watch-mode-sidebar .toolbar {
min-height: 24px;
}
.watch-mode-sidebar .toolbar-button { .watch-mode-sidebar .toolbar-button {
margin: 0; margin: 0;
} }
.watch-mode .section-title { .watch-mode .section-title {
display: flex;
flex-direction: row;
align-items: center;
font-size: 11px; font-size: 11px;
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; 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 { .status-line {
@ -68,38 +84,12 @@
margin: 0 5px; margin: 0 5px;
} }
.list-view {
margin-top: 5px;
}
.list-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) { .list-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) {
display: none; 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] { .watch-mode-sidebar input[type=search] {
flex: auto;
padding: 0 5px; padding: 0 5px;
line-height: 24px; line-height: 24px;
outline: none; outline: none;
@ -108,3 +98,51 @@
color: var(--vscode-input-foreground); color: var(--vscode-input-foreground);
background-color: var(--vscode-input-background); 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;
}

View File

@ -20,7 +20,7 @@ import '@web/common.css';
import React from 'react'; import React from 'react';
import { TreeView } from '@web/components/treeView'; import { TreeView } from '@web/components/treeView';
import type { TreeState } 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 { TeleTestCase } from '../../../playwright-test/src/isomorphic/teleReceiver';
import type { FullConfig, Suite, TestCase, TestResult, TestStep, Location } from '../../../playwright-test/types/testReporter'; import type { FullConfig, Suite, TestCase, TestResult, TestStep, Location } from '../../../playwright-test/types/testReporter';
import { SplitView } from '@web/components/splitView'; import { SplitView } from '@web/components/splitView';
@ -28,12 +28,12 @@ import { MultiTraceModel } from './modelUtil';
import './watchMode.css'; import './watchMode.css';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
import { Toolbar } from '@web/components/toolbar'; import { Toolbar } from '@web/components/toolbar';
import { toggleTheme } from '@web/theme';
import type { ContextEntry } from '../entries'; import type { ContextEntry } from '../entries';
import type * as trace from '@trace/trace'; import type * as trace from '@trace/trace';
import type { XtermDataSource } from '@web/components/xtermWrapper'; import type { XtermDataSource } from '@web/components/xtermWrapper';
import { XtermWrapper } from '@web/components/xtermWrapper'; import { XtermWrapper } from '@web/components/xtermWrapper';
import { Expandable } from '@web/components/expandable'; import { Expandable } from '@web/components/expandable';
import { toggleTheme } from '@web/theme';
let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {}; let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {};
let updateStepsProgress: () => void = () => {}; let updateStepsProgress: () => void = () => {};
@ -52,39 +52,51 @@ const xtermDataSource: XtermDataSource = {
export const WatchModeView: React.FC<{}> = ({ export const WatchModeView: React.FC<{}> = ({
}) => { }) => {
const [filterText, setFilterText] = useSetting<string>('test-ui-filter-text', ''); const [filterText, setFilterText] = React.useState<string>('');
const [filterExpanded, setFilterExpanded] = useSetting<boolean>('test-ui-filter-expanded', false); const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(false);
const [isShowingOutput, setIsShowingOutput] = useSetting<boolean>('test-ui-show-output', false);
const [projects, setProjects] = React.useState<Map<string, boolean>>(new Map()); const [statusFilters, setStatusFilters] = React.useState<Map<string, boolean>>(new Map([
['passed', false],
['failed', false],
['skipped', false],
]));
const [projectFilters, setProjectFilters] = React.useState<Map<string, boolean>>(new Map());
const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined }); const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined });
const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0, skipped: 0 }); const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0, skipped: 0 });
const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined); const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined);
const [settingsVisible, setSettingsVisible] = React.useState<boolean>(false);
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]); const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean }>(); const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean }>();
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const reloadTests = () => {
setIsLoading(true);
updateRootSuite(new TeleSuite('', 'root'), { total: 0, passed: 0, failed: 0, skipped: 0 });
refreshRootSuite(true).then(() => {
setIsLoading(false);
});
};
React.useEffect(() => { React.useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
refreshRootSuite(true); reloadTests();
}, []); }, []);
updateRootSuite = (rootSuite: Suite, newProgress: Progress) => { 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)) if (!rootSuite.suites.find(s => s.title === projectName))
projects.delete(projectName); projectFilters.delete(projectName);
} }
for (const projectSuite of rootSuite.suites) { for (const projectSuite of rootSuite.suites) {
if (!projects.has(projectSuite.title)) if (!projectFilters.has(projectSuite.title))
projects.set(projectSuite.title, false); projectFilters.set(projectSuite.title, false);
} }
if (![...projects.values()].includes(true)) if (projectFilters.size && ![...projectFilters.values()].includes(true))
projects.set(projects.entries().next().value[0], true); projectFilters.set(projectFilters.entries().next().value[0], true);
setRootSuite({ value: rootSuite }); setRootSuite({ value: rootSuite });
setProjects(new Map(projects)); setProjectFilters(new Map(projectFilters));
setProgress(newProgress); 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 isRunningTest = !!runningState;
const result = selectedTest?.results[0]; const result = selectedTest?.results[0];
const isFinished = result && result.duration >= 0; const isFinished = result && result.duration >= 0;
@ -134,7 +128,6 @@ export const WatchModeView: React.FC<{}> = ({
<div className='vbox'> <div className='vbox'>
<div className={'vbox' + (isShowingOutput ? '' : ' hidden')}> <div className={'vbox' + (isShowingOutput ? '' : ' hidden')}>
<Toolbar> <Toolbar>
<div className='section-title'>Output</div>
<ToolbarButton icon='circle-slash' title='Clear output' onClick={() => xtermDataSource.clear()}></ToolbarButton> <ToolbarButton icon='circle-slash' title='Clear output' onClick={() => xtermDataSource.clear()}></ToolbarButton>
<div className='spacer'></div> <div className='spacer'></div>
<ToolbarButton icon='close' title='Close' onClick={() => setIsShowingOutput(false)}></ToolbarButton> <ToolbarButton icon='close' title='Close' onClick={() => setIsShowingOutput(false)}></ToolbarButton>
@ -148,52 +141,42 @@ export const WatchModeView: React.FC<{}> = ({
</div> </div>
<div className='vbox watch-mode-sidebar'> <div className='vbox watch-mode-sidebar'>
<Toolbar> <Toolbar>
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div> <img src='icon-32x32.png' />
<ToolbarButton icon='play' title='Run all' onClick={() => runTests(visibleTestIds)} disabled={isRunningTest}></ToolbarButton> <div className='section-title'>Playwright</div>
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
<ToolbarButton icon='refresh' title='Reload' onClick={() => refreshRootSuite(true)} disabled={isRunningTest}></ToolbarButton>
<div className='spacer'></div> <div className='spacer'></div>
<ToolbarButton icon='gear' title='Toggle color mode' toggled={settingsVisible} onClick={() => { setSettingsVisible(!settingsVisible); }}></ToolbarButton> <ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()} />
<ToolbarButton icon='terminal' title='Toggle color mode' toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }}></ToolbarButton> <ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
<ToolbarButton icon='terminal' title='Toggle output' toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
</Toolbar>
<FiltersView
filterText={filterText}
setFilterText={setFilterText}
statusFilters={statusFilters}
setStatusFilters={setStatusFilters}
projectFilters={projectFilters}
setProjectFilters={setProjectFilters}
runTests={() => runTests(visibleTestIds)} />
<Toolbar>
<div className='section-title'>Tests</div>
<div className='spacer'></div>
<ToolbarButton icon='play' title='Run all' onClick={() => runTests(visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton>
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest || isLoading}></ToolbarButton>
</Toolbar> </Toolbar>
{!settingsVisible && <Expandable
title={<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
onChange={e => {
setFilterText(e.target.value);
}}
onKeyDown={e => {
if (e.key === 'Enter')
runTests(visibleTestIds);
}}></input>}
style={{ flex: 'none', marginTop: 8 }}
expanded={filterExpanded}
setExpanded={setFilterExpanded}>
<div className='filters'>
<span>Status:</span>
<div onClick={() => updateFilter('s', '')}>all</div>
{['failed', 'passed', 'skipped'].map(s => <div className={filterText.includes('s:' + s) ? 'filters-toggled' : ''} onClick={() => updateFilter('s', s)}>{s}</div>)}
</div>
{[...projects.values()].filter(v => v).length > 1 && <div className='filters'>
<span>Project:</span>
<div onClick={() => updateFilter('p', '')}>all</div>
{[...projects].filter(([k, v]) => v).map(([k, v]) => k).map(p => <div className={filterText.includes('p:' + p) ? 'filters-toggled' : ''} onClick={() => updateFilter('p', p)}>{p}</div>)}
</div>}
</Expandable>}
<TestList <TestList
projects={projects} statusFilters={statusFilters}
projectFilters={projectFilters}
filterText={filterText} filterText={filterText}
rootSuite={rootSuite} rootSuite={rootSuite}
runningState={runningState} runningState={runningState}
runTests={runTests} runTests={runTests}
onTestSelected={setSelectedTest} onTestSelected={setSelectedTest}
isVisible={!settingsVisible}
setVisibleTestIds={setVisibleTestIds} /> setVisibleTestIds={setVisibleTestIds} />
{settingsVisible && <SettingsView projects={projects} setProjects={setProjects} onClose={() => setSettingsVisible(false)}></SettingsView>}
</div> </div>
</SplitView> </SplitView>
<div className='status-line'> <div className='status-line'>
<div>Total: {progress.total}</div> <div>Total: {progress.total}</div>
{isRunningTest && <div><span className='codicon codicon-loading'></span>Running {visibleTestIds.length}</div>} {isRunningTest && <div><span className='codicon codicon-loading'></span>{`Running ${visibleTestIds.length}\u2026`}</div>}
{isLoading && <div><span className='codicon codicon-loading'></span> {'Loading\u2026'}</div>}
{!isRunningTest && <div>Showing: {visibleTestIds.length}</div>} {!isRunningTest && <div>Showing: {visibleTestIds.length}</div>}
<div>{progress.passed} passed</div> <div>{progress.passed} passed</div>
<div>{progress.failed} failed</div> <div>{progress.failed} failed</div>
@ -202,29 +185,89 @@ export const WatchModeView: React.FC<{}> = ({
</div>; </div>;
}; };
const FiltersView: React.FC<{
filterText: string;
setFilterText: (text: string) => void;
statusFilters: Map<string, boolean>;
setStatusFilters: (filters: Map<string, boolean>) => void;
projectFilters: Map<string, boolean>;
setProjectFilters: (filters: Map<string, boolean>) => void;
runTests: () => void;
}> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, runTests }) => {
const [expanded, setExpanded] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(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 <div className='filters'>
<Expandable
expanded={expanded}
setExpanded={setExpanded}
title={<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
onChange={e => {
setFilterText(e.target.value);
}}
onKeyDown={e => {
if (e.key === 'Enter')
runTests();
}} />}>
{<div className='filter-title' title={statusLine} onClick={() => setExpanded(false)}><span className='filter-label'>Status:</span> {statusLine}</div>}
{[...statusFilters.entries()].map(([status, value]) => {
return <div className='filter-entry'>
<label>
<input type='checkbox' checked={value} onClick={() => {
const copy = new Map(statusFilters);
copy.set(status, !copy.get(status));
setStatusFilters(copy);
}}/>
<div>{status}</div>
</label>
</div>;
})}
{<div className='filter-title' title={projectsLine}><span className='filter-label'>Projects:</span> {projectsLine}</div>}
{[...projectFilters.entries()].map(([projectName, value]) => {
return <div className='filter-entry'>
<label>
<input type='checkbox' checked={value} onClick={() => {
const copy = new Map(projectFilters);
copy.set(projectName, !copy.get(projectName));
setProjectFilters(copy);
}}/>
<div>{projectName}</div>
</label>
</div>;
})}
</Expandable>
{!expanded && <div className='filter-summary' title={'Status: ' + statusLine + '\nProjects: ' + projectsLine} onClick={() => setExpanded(true)}>
<span className='filter-label'>Status:</span> {statusLine}
<span className='filter-label'>Projects:</span> {projectsLine}
</div>}
</div>;
};
const TestTreeView = TreeView<TreeItem>; const TestTreeView = TreeView<TreeItem>;
export const TestList: React.FC<{ const TestList: React.FC<{
projects: Map<string, boolean>, statusFilters: Map<string, boolean>,
projectFilters: Map<string, boolean>,
filterText: string, filterText: string,
rootSuite: { value: Suite | undefined }, rootSuite: { value: Suite | undefined },
runTests: (testIds: string[]) => void, runTests: (testIds: string[]) => void,
runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean }, runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean },
isVisible: boolean,
setVisibleTestIds: (testIds: string[]) => void, setVisibleTestIds: (testIds: string[]) => void,
onTestSelected: (test: TestCase | undefined) => 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<TreeState>({ expandedItems: new Map() }); const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>(); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
const [watchedTreeIds] = React.useState<Set<string>>(new Set()); const [watchedTreeIds] = React.useState<Set<string>>(new Set());
React.useEffect(() => {
refreshRootSuite(true);
}, []);
const { rootItem, treeItemMap } = React.useMemo(() => { const { rootItem, treeItemMap } = React.useMemo(() => {
const rootItem = createTree(rootSuite.value, projects); const rootItem = createTree(rootSuite.value, projectFilters);
filterTree(rootItem, filterText); filterTree(rootItem, filterText, statusFilters);
hideOnlyTests(rootItem); hideOnlyTests(rootItem);
const treeItemMap = new Map<string, TreeItem>(); const treeItemMap = new Map<string, TreeItem>();
const visibleTestIds = new Set<string>(); const visibleTestIds = new Set<string>();
@ -237,7 +280,7 @@ export const TestList: React.FC<{
visit(rootItem); visit(rootItem);
setVisibleTestIds([...visibleTestIds]); setVisibleTestIds([...visibleTestIds]);
return { rootItem, treeItemMap }; return { rootItem, treeItemMap };
}, [filterText, rootSuite, projects, setVisibleTestIds]); }, [filterText, rootSuite, statusFilters, projectFilters, setVisibleTestIds]);
React.useEffect(() => { React.useEffect(() => {
// Look for a first failure within the run batch to select it. // Look for a first failure within the run batch to select it.
@ -245,9 +288,9 @@ export const TestList: React.FC<{
return; return;
let selectedTreeItem: TreeItem | undefined; let selectedTreeItem: TreeItem | undefined;
const visit = (treeItem: TreeItem) => { const visit = (treeItem: TreeItem) => {
treeItem.children.forEach(visit);
if (selectedTreeItem) if (selectedTreeItem)
return; return;
treeItem.children.forEach(visit);
if (treeItem.status === 'failed') { if (treeItem.status === 'failed') {
if (treeItem.kind === 'test' && runningState.testIds.has(treeItem.test.id)) if (treeItem.kind === 'test' && runningState.testIds.has(treeItem.test.id))
selectedTreeItem = treeItem; selectedTreeItem = treeItem;
@ -296,9 +339,6 @@ export const TestList: React.FC<{
runTests(testIds); runTests(testIds);
}; };
if (!isVisible)
return <></>;
return <TestTreeView return <TestTreeView
treeState={treeState} treeState={treeState}
setTreeState={setTreeState} setTreeState={setTreeState}
@ -340,39 +380,7 @@ export const TestList: React.FC<{
noItemsMessage='No tests' />; noItemsMessage='No tests' />;
}; };
export const SettingsView: React.FC<{ const InProgressTraceView: React.FC<{
projects: Map<string, boolean>,
setProjects: (projectNames: Map<string, boolean>) => void,
onClose: () => void,
}> = ({ projects, setProjects, onClose }) => {
return <div className='vbox'>
<div className='hbox' style={{ flex: 'none' }}>
<div className='section-title' style={{ marginTop: 10 }}>Projects</div>
<div className='spacer'></div>
<ToolbarButton icon='close' title='Close settings' toggled={false} onClick={onClose}></ToolbarButton>
</div>
{[...projects.entries()].map(([projectName, value]) => {
return <div style={{ display: 'flex', alignItems: 'center', lineHeight: '24px', marginLeft: 5 }}>
<input id={`project-${projectName}`} type='checkbox' checked={value} style={{ cursor: 'pointer' }} onClick={() => {
const copy = new Map(projects);
copy.set(projectName, !copy.get(projectName));
if (![...copy.values()].includes(true))
copy.set(projectName, true);
setProjects(copy);
}}/>
<label htmlFor={`project-${projectName}`} style={{ cursor: 'pointer' }}>
{projectName}
</label>
</div>;
})}
<div className='section-title'>Appearance</div>
<div style={{ marginLeft: 3 }}>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}>Toggle color mode</ToolbarButton>
</div>
</div>;
};
export const InProgressTraceView: React.FC<{
testResult: TestResult | undefined, testResult: TestResult | undefined,
}> = ({ testResult }) => { }> = ({ testResult }) => {
const [model, setModel] = React.useState<MultiTraceModel | undefined>(); const [model, setModel] = React.useState<MultiTraceModel | undefined>();
@ -386,7 +394,7 @@ export const InProgressTraceView: React.FC<{
return <Workbench model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />; return <Workbench model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />;
}; };
export const FinishedTraceView: React.FC<{ const FinishedTraceView: React.FC<{
testResult: TestResult, testResult: TestResult,
}> = ({ testResult }) => { }> = ({ testResult }) => {
const [model, setModel] = React.useState<MultiTraceModel | undefined>(); const [model, setModel] = React.useState<MultiTraceModel | undefined>();
@ -425,11 +433,9 @@ const throttleUpdateRootSuite = (rootSuite: Suite, progress: Progress, immediate
throttleTimer = setTimeout(throttledAction, 250); throttleTimer = setTimeout(throttledAction, 250);
}; };
const refreshRootSuite = (eraseResults: boolean) => { const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
if (!eraseResults) { if (!eraseResults)
sendMessageNoReply('list'); return sendMessage('list', {});
return;
}
let rootSuite: Suite; let rootSuite: Suite;
const progress: Progress = { const progress: Progress = {
@ -477,12 +483,12 @@ const refreshRootSuite = (eraseResults: boolean) => {
updateStepsProgress(); updateStepsProgress();
}, },
}); });
sendMessageNoReply('list'); return sendMessage('list', {});
}; };
(window as any).dispatch = (message: any) => { (window as any).dispatch = (message: any) => {
if (message.method === 'listChanged') { if (message.method === 'listChanged') {
refreshRootSuite(false); refreshRootSuite(false).catch(() => {});
return; return;
} }
@ -577,7 +583,8 @@ type TestItem = TreeItemBase & {
type TreeItem = GroupItem | TestCaseItem | TestItem; type TreeItem = GroupItem | TestCaseItem | TestItem;
function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>): GroupItem { function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, boolean>): GroupItem {
const filterProjects = [...projectFilters.values()].some(Boolean);
const rootItem: GroupItem = { const rootItem: GroupItem = {
kind: 'group', kind: 'group',
id: 'root', id: 'root',
@ -650,7 +657,7 @@ function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>
}; };
for (const projectSuite of rootSuite?.suites || []) { for (const projectSuite of rootSuite?.suites || []) {
if (!projects.get(projectSuite.title)) if (filterProjects && !projectFilters.get(projectSuite.title))
continue; continue;
visitSuite(projectSuite.title, projectSuite, rootItem); visitSuite(projectSuite.title, projectSuite, rootItem);
} }
@ -687,21 +694,15 @@ function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>
return rootItem; return rootItem;
} }
function filterTree(rootItem: GroupItem, filterText: string) { function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map<string, boolean>) {
const trimmedFilterText = filterText.trim(); const tokens = filterText.trim().toLowerCase().split(' ');
const filterTokens = trimmedFilterText.toLowerCase().split(' '); const filtersStatuses = [...statusFilters.values()].some(Boolean);
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)));
const filter = (testCase: TestCaseItem) => { const filter = (testCase: TestCaseItem) => {
const title = testCase.tests[0].titlePath().join(' ').toLowerCase(); const title = testCase.tests[0].titlePath().join(' ').toLowerCase();
if (!textTokens.every(token => title.includes(token))) if (!tokens.every(token => title.includes(token)))
return false; return false;
testCase.children = (testCase.children as TestItem[]).filter(test => !statuses.size || statuses.has(test.status)); testCase.children = (testCase.children as TestItem[]).filter(test => !filtersStatuses || statusFilters.get(test.status));
testCase.children = (testCase.children as TestItem[]).filter(test => !projects.size || projects.has(test.project));
testCase.tests = (testCase.children as TestItem[]).map(c => c.test); testCase.tests = (testCase.children as TestItem[]).map(c => c.test);
return !!testCase.children.length; return !!testCase.children.length;
}; };
@ -803,17 +804,3 @@ function stepsToModel(result: TestResult): MultiTraceModel {
return new MultiTraceModel([contextEntry]); return new MultiTraceModel([contextEntry]);
} }
function useSetting<S>(name: string, defaultValue: S): [S, React.Dispatch<React.SetStateAction<S>>] {
const string = localStorage.getItem(name);
let value = defaultValue;
if (string !== null)
value = JSON.parse(string);
const [state, setState] = React.useState<S>(value);
const setStateWrapper = (value: React.SetStateAction<S>) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
};
return [state, setStateWrapper];
}

View File

@ -118,7 +118,8 @@ body.dark-mode .CodeMirror span.cm-type {
.CodeMirror .CodeMirror-gutters { .CodeMirror .CodeMirror-gutters {
z-index: 0; z-index: 0;
background: var(--vscode-editorGutter-background); background: 1px solid var(--vscode-editorGroup-border);
border-right: none;
} }
.CodeMirror .CodeMirror-gutterwrapper { .CodeMirror .CodeMirror-gutterwrapper {

View File

@ -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;
}

View File

@ -15,21 +15,21 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import './expandable.css';
export const Expandable: React.FunctionComponent<React.PropsWithChildren<{ export const Expandable: React.FunctionComponent<React.PropsWithChildren<{
title: JSX.Element | string, title: JSX.Element | string,
setExpanded: Function, setExpanded: Function,
expanded: boolean, expanded: boolean,
style?: React.CSSProperties, }>> = ({ title, children, setExpanded, expanded }) => {
}>> = ({ title, children, setExpanded, expanded, style }) => { return <div className={'expandable' + (expanded ? ' expanded' : '')}>
return <div style={{ ...style, display: 'flex', flexDirection: 'column' }}> <div className='expandable-title'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', whiteSpace: 'nowrap' }}>
<div <div
className={'codicon codicon-' + (expanded ? 'chevron-down' : 'chevron-right')} className={'codicon codicon-' + (expanded ? 'chevron-down' : 'chevron-right')}
style={{ cursor: 'pointer', color: 'var(--vscode-foreground)', marginLeft: '5px' }} style={{ cursor: 'pointer', color: 'var(--vscode-foreground)', marginLeft: '5px' }}
onClick={() => setExpanded(!expanded)} /> onClick={() => setExpanded(!expanded)} />
{title} {title}
</div> </div>
{ expanded && <div style={{ margin: '5px 0 5px 20px' }}>{children}</div> } { expanded && <div style={{ marginLeft: 25 }}>{children}</div> }
</div>; </div>;
}; };

View File

@ -14,6 +14,8 @@
limitations under the License. limitations under the License.
*/ */
import React from 'react';
export function msToString(ms: number): string { export function msToString(ms: number): string {
if (!isFinite(ms)) if (!isFinite(ms))
return '-'; return '-';
@ -76,3 +78,17 @@ export function copy(text: string) {
document.execCommand('copy'); document.execCommand('copy');
textArea.remove(); textArea.remove();
} }
export function useSetting<S>(name: string, defaultValue: S): [S, React.Dispatch<React.SetStateAction<S>>] {
const string = localStorage.getItem(name);
let value = defaultValue;
if (string !== null)
value = JSON.parse(string);
const [state, setState] = React.useState<S>(value);
const setStateWrapper = (value: React.SetStateAction<S>) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
};
return [state, setStateWrapper];
}

View File

@ -69,13 +69,14 @@ export function dumpTestTree(page: Page): () => Promise<string> {
export const test = base export const test = base
.extend<Fixtures>({ .extend<Fixtures>({
runUITest: async ({ childProcess, playwright, headless }, use, testInfo: TestInfo) => { runUITest: async ({ childProcess, playwright, headless }, use, testInfo: TestInfo) => {
testInfo.slow();
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-')); const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-'));
let testProcess: TestChildProcess | undefined; let testProcess: TestChildProcess | undefined;
let browser: Browser | undefined; let browser: Browser | undefined;
await use(async (files: Files, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => { await use(async (files: Files, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => {
const baseDir = await writeFiles(testInfo, files, true); const baseDir = await writeFiles(testInfo, files, true);
testProcess = childProcess({ testProcess = childProcess({
command: ['node', cliEntrypoint, 'ui', ...(options.additionalArgs || [])], command: ['node', cliEntrypoint, 'ui', '--workers=1', ...(options.additionalArgs || [])],
env: { env: {
...cleanEnv(env), ...cleanEnv(env),
PWTEST_UNDER_TEST: '1', PWTEST_UNDER_TEST: '1',

View File

@ -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();
});

View File

@ -41,13 +41,13 @@ const basicTestTree = {
test('should run visible', async ({ runUITest }) => { test('should run visible', async ({ runUITest }) => {
const page = await runUITest(basicTestTree); const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts a.test.ts
`); `);
await page.getByTitle('Run all').click(); 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 a.test.ts
passes passes
fails <= fails <=
@ -72,7 +72,7 @@ test('should run on double click', async ({ runUITest }) => {
await page.getByText('passes').dblclick(); await page.getByText('passes').dblclick();
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(`
a.test.ts a.test.ts
passes <= passes <=
fails fails
@ -91,9 +91,88 @@ test('should run on Enter', async ({ runUITest }) => {
await page.getByText('fails').click(); await page.getByText('fails').click();
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(`
a.test.ts a.test.ts
passes passes
fails <= 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
`);
});

View File

@ -37,7 +37,7 @@ const basicTestTree = {
test('should list tests', async ({ runUITest }) => { test('should list tests', async ({ runUITest }) => {
const page = await runUITest(basicTestTree); const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(`
a.test.ts a.test.ts
passes passes
fails fails
@ -51,7 +51,7 @@ test('should list tests', async ({ runUITest }) => {
test('should traverse up/down', async ({ runUITest }) => { test('should traverse up/down', async ({ runUITest }) => {
const page = await runUITest(basicTestTree); const page = await runUITest(basicTestTree);
await page.getByText('a.test.ts').click(); 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 <= a.test.ts <=
passes passes
fails fails
@ -59,14 +59,14 @@ test('should traverse up/down', async ({ runUITest }) => {
`); `);
await page.keyboard.press('ArrowDown'); await page.keyboard.press('ArrowDown');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts a.test.ts
passes <= passes <=
fails fails
suite suite
`); `);
await page.keyboard.press('ArrowDown'); await page.keyboard.press('ArrowDown');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts a.test.ts
passes passes
fails <= fails <=
@ -74,7 +74,7 @@ test('should traverse up/down', async ({ runUITest }) => {
`); `);
await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts a.test.ts
passes <= passes <=
fails fails
@ -87,7 +87,7 @@ test('should expand / collapse groups', async ({ runUITest }) => {
await page.getByText('suite').click(); await page.getByText('suite').click();
await page.keyboard.press('ArrowRight'); await page.keyboard.press('ArrowRight');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts a.test.ts
passes passes
fails fails
@ -97,7 +97,7 @@ test('should expand / collapse groups', async ({ runUITest }) => {
`); `);
await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowLeft');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts a.test.ts
passes passes
fails fails
@ -106,25 +106,14 @@ test('should expand / collapse groups', async ({ runUITest }) => {
await page.getByText('passes').first().click(); await page.getByText('passes').first().click();
await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowLeft');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts <= a.test.ts <=
passes passes
fails fails
`); `);
await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowLeft');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts <= 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
`);
});

View File

@ -37,7 +37,7 @@ const basicTestTree = {
test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFile }) => { test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFile }) => {
const page = await runUITest(basicTestTree); const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(`
a.test.ts a.test.ts
passes passes
fails 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 a.test.ts
passes passes
fails fails
@ -70,7 +70,7 @@ test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFi
await deleteFile('a.test.ts'); await deleteFile('a.test.ts');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(`
b.test.ts b.test.ts
passes passes
fails 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 }) => { test('should pick new / deleted tests', async ({ runUITest, writeFiles, deleteFile }) => {
const page = await runUITest(basicTestTree); const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(`
a.test.ts a.test.ts
passes passes
fails 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 a.test.ts
passes passes
new 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 a.test.ts
new new
b.test.ts 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 }) => { test('should pick new / deleted nested tests', async ({ runUITest, writeFiles, deleteFile }) => {
const page = await runUITest(basicTestTree); const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts a.test.ts
passes passes
fails fails
@ -140,7 +140,7 @@ test('should pick new / deleted nested tests', async ({ runUITest, writeFiles, d
await page.getByText('suite').click(); await page.getByText('suite').click();
await page.keyboard.press('ArrowRight'); await page.keyboard.press('ArrowRight');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(` await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
a.test.ts a.test.ts
passes passes
fails 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 a.test.ts
passes passes
suite <= suite <=

View File

@ -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('Watch').click();
await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Run').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 a.test.ts
passes passes
fails 👁 <= 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 a.test.ts
passes passes
fails 👁 <= fails 👁 <=