mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: reimplement filters (#21647)
This commit is contained in:
parent
ccd5d7fd10
commit
f5fa18a279
@ -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;
|
||||
|
@ -125,7 +125,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
|
||||
return <div
|
||||
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-time'>{resource.time}ms</div>
|
||||
<div className='network-request-details-header'>URL</div>
|
||||
|
@ -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;
|
||||
}
|
@ -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<string>('test-ui-filter-text', '');
|
||||
const [filterExpanded, setFilterExpanded] = useSetting<boolean>('test-ui-filter-expanded', false);
|
||||
const [isShowingOutput, setIsShowingOutput] = useSetting<boolean>('test-ui-show-output', false);
|
||||
const [filterText, setFilterText] = React.useState<string>('');
|
||||
const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(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 [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0, skipped: 0 });
|
||||
const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined);
|
||||
const [settingsVisible, setSettingsVisible] = React.useState<boolean>(false);
|
||||
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean }>();
|
||||
|
||||
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(() => {
|
||||
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<{}> = ({
|
||||
<div className='vbox'>
|
||||
<div className={'vbox' + (isShowingOutput ? '' : ' hidden')}>
|
||||
<Toolbar>
|
||||
<div className='section-title'>Output</div>
|
||||
<ToolbarButton icon='circle-slash' title='Clear output' onClick={() => xtermDataSource.clear()}></ToolbarButton>
|
||||
<div className='spacer'></div>
|
||||
<ToolbarButton icon='close' title='Close' onClick={() => setIsShowingOutput(false)}></ToolbarButton>
|
||||
@ -148,52 +141,42 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
</div>
|
||||
<div className='vbox watch-mode-sidebar'>
|
||||
<Toolbar>
|
||||
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
|
||||
<ToolbarButton icon='play' title='Run all' onClick={() => runTests(visibleTestIds)} disabled={isRunningTest}></ToolbarButton>
|
||||
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
|
||||
<ToolbarButton icon='refresh' title='Reload' onClick={() => refreshRootSuite(true)} disabled={isRunningTest}></ToolbarButton>
|
||||
<img src='icon-32x32.png' />
|
||||
<div className='section-title'>Playwright</div>
|
||||
<div className='spacer'></div>
|
||||
<ToolbarButton icon='gear' title='Toggle color mode' toggled={settingsVisible} onClick={() => { setSettingsVisible(!settingsVisible); }}></ToolbarButton>
|
||||
<ToolbarButton icon='terminal' title='Toggle color mode' toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }}></ToolbarButton>
|
||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()} />
|
||||
<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>
|
||||
{!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
|
||||
projects={projects}
|
||||
statusFilters={statusFilters}
|
||||
projectFilters={projectFilters}
|
||||
filterText={filterText}
|
||||
rootSuite={rootSuite}
|
||||
runningState={runningState}
|
||||
runTests={runTests}
|
||||
onTestSelected={setSelectedTest}
|
||||
isVisible={!settingsVisible}
|
||||
setVisibleTestIds={setVisibleTestIds} />
|
||||
{settingsVisible && <SettingsView projects={projects} setProjects={setProjects} onClose={() => setSettingsVisible(false)}></SettingsView>}
|
||||
</div>
|
||||
</SplitView>
|
||||
<div className='status-line'>
|
||||
<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>}
|
||||
<div>{progress.passed} passed</div>
|
||||
<div>{progress.failed} failed</div>
|
||||
@ -202,29 +185,89 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
</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>;
|
||||
|
||||
export const TestList: React.FC<{
|
||||
projects: Map<string, boolean>,
|
||||
const TestList: React.FC<{
|
||||
statusFilters: Map<string, boolean>,
|
||||
projectFilters: Map<string, boolean>,
|
||||
filterText: string,
|
||||
rootSuite: { value: Suite | undefined },
|
||||
runTests: (testIds: string[]) => void,
|
||||
runningState?: { testIds: Set<string>, 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<TreeState>({ expandedItems: new Map() });
|
||||
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
||||
const [watchedTreeIds] = React.useState<Set<string>>(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<string, TreeItem>();
|
||||
const visibleTestIds = new Set<string>();
|
||||
@ -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 <TestTreeView
|
||||
treeState={treeState}
|
||||
setTreeState={setTreeState}
|
||||
@ -340,39 +380,7 @@ export const TestList: React.FC<{
|
||||
noItemsMessage='No tests' />;
|
||||
};
|
||||
|
||||
export const SettingsView: 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<{
|
||||
const InProgressTraceView: React.FC<{
|
||||
testResult: TestResult | undefined,
|
||||
}> = ({ testResult }) => {
|
||||
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} />;
|
||||
};
|
||||
|
||||
export const FinishedTraceView: React.FC<{
|
||||
const FinishedTraceView: React.FC<{
|
||||
testResult: TestResult,
|
||||
}> = ({ testResult }) => {
|
||||
const [model, setModel] = React.useState<MultiTraceModel | undefined>();
|
||||
@ -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<void> => {
|
||||
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<string, boolean>): GroupItem {
|
||||
function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, boolean>): 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<string, boolean>
|
||||
};
|
||||
|
||||
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<string, boolean>
|
||||
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<string, boolean>) {
|
||||
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<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];
|
||||
}
|
||||
|
@ -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 {
|
||||
|
31
packages/web/src/components/expandable.css
Normal file
31
packages/web/src/components/expandable.css
Normal 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;
|
||||
}
|
@ -15,21 +15,21 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import './expandable.css';
|
||||
|
||||
export const Expandable: React.FunctionComponent<React.PropsWithChildren<{
|
||||
title: JSX.Element | string,
|
||||
setExpanded: Function,
|
||||
expanded: boolean,
|
||||
style?: React.CSSProperties,
|
||||
}>> = ({ title, children, setExpanded, expanded, style }) => {
|
||||
return <div style={{ ...style, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', whiteSpace: 'nowrap' }}>
|
||||
}>> = ({ title, children, setExpanded, expanded }) => {
|
||||
return <div className={'expandable' + (expanded ? ' expanded' : '')}>
|
||||
<div className='expandable-title'>
|
||||
<div
|
||||
className={'codicon codicon-' + (expanded ? 'chevron-down' : 'chevron-right')}
|
||||
style={{ cursor: 'pointer', color: 'var(--vscode-foreground)', marginLeft: '5px' }}
|
||||
onClick={() => setExpanded(!expanded)} />
|
||||
{title}
|
||||
</div>
|
||||
{ expanded && <div style={{ margin: '5px 0 5px 20px' }}>{children}</div> }
|
||||
{ expanded && <div style={{ marginLeft: 25 }}>{children}</div> }
|
||||
</div>;
|
||||
};
|
||||
|
@ -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<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];
|
||||
}
|
||||
|
@ -69,13 +69,14 @@ export function dumpTestTree(page: Page): () => Promise<string> {
|
||||
export const test = base
|
||||
.extend<Fixtures>({
|
||||
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',
|
||||
|
150
tests/playwright-test/ui-mode-test-filters.spec.ts
Normal file
150
tests/playwright-test/ui-mode-test-filters.spec.ts
Normal 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();
|
||||
});
|
@ -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
|
||||
`);
|
||||
});
|
||||
|
@ -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
|
||||
`);
|
||||
});
|
||||
|
@ -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 <=
|
||||
|
@ -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 👁 <=
|
||||
|
Loading…
x
Reference in New Issue
Block a user