mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(ui): decorate pending, add time spent (#21821)
This commit is contained in:
parent
2dee3c4fc7
commit
a33cf10696
@ -26,7 +26,7 @@
|
||||
flex: none;
|
||||
align-items: center;
|
||||
margin: 0 4px;
|
||||
color: var(--gray);
|
||||
color: var(--vscode-editorCodeLens-foreground);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
|
@ -127,7 +127,7 @@
|
||||
|
||||
.timeline-bar.frame_waitforeventinfo,
|
||||
.timeline-bar.page_waitforeventinfo {
|
||||
--action-color: var(--gray);
|
||||
--action-color: var(--vscode-editorCodeLens-foreground);
|
||||
}
|
||||
|
||||
.timeline-label {
|
||||
|
@ -37,7 +37,18 @@
|
||||
.watch-mode-list-item-title {
|
||||
flex: auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.watch-mode-list-item-time {
|
||||
flex: none;
|
||||
color: var(--vscode-editorCodeLens-foreground);
|
||||
margin: 0 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.list-view-entry.highlighted .watch-mode-list-item-time {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.watch-mode .section-title {
|
||||
@ -56,7 +67,7 @@
|
||||
|
||||
.watch-mode-sidebar img {
|
||||
flex: none;
|
||||
margin: 0 4px;
|
||||
margin-left: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ import { XtermWrapper } from '@web/components/xtermWrapper';
|
||||
import { Expandable } from '@web/components/expandable';
|
||||
import { toggleTheme } from '@web/theme';
|
||||
import { artifactsFolderName } from '@testIsomorphic/folders';
|
||||
import { settings, useSetting } from '@web/uiUtils';
|
||||
import { msToString, settings, useSetting } from '@web/uiUtils';
|
||||
|
||||
let updateRootSuite: (config: FullConfig, rootSuite: Suite, progress: Progress | undefined) => void = () => {};
|
||||
let runWatchedTests = (fileNames: string[]) => {};
|
||||
@ -73,12 +73,14 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean } | undefined>();
|
||||
const [watchAll, setWatchAll] = useSetting<boolean>('watch-all', false);
|
||||
const [watchedTreeIds, setWatchedTreeIds] = React.useState<{ value: Set<string> }>({ value: new Set() });
|
||||
const runTestPromiseChain = React.useRef(Promise.resolve());
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const reloadTests = () => {
|
||||
setIsLoading(true);
|
||||
setWatchedTreeIds({ value: new Set() });
|
||||
updateRootSuite(baseFullConfig, new TeleSuite('', 'root'), undefined);
|
||||
refreshRootSuite(true).then(() => {
|
||||
setIsLoading(false);
|
||||
@ -165,7 +167,6 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
<div className='section-title'>Playwright</div>
|
||||
<ToolbarButton icon='color-mode' title='Toggle color mode' onClick={() => toggleTheme()} />
|
||||
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
|
||||
<ToolbarButton icon='eye' title='Watch all' toggled={watchAll} onClick={() => setWatchAll(!watchAll)}></ToolbarButton>
|
||||
<ToolbarButton icon='terminal' title='Toggle output' toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
||||
</Toolbar>
|
||||
<FiltersView
|
||||
@ -187,6 +188,7 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
</div>}
|
||||
<ToolbarButton icon='play' title='Run all' onClick={() => runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton>
|
||||
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest || isLoading}></ToolbarButton>
|
||||
<ToolbarButton icon='eye' title='Watch all' toggled={watchAll} onClick={() => setWatchAll(!watchAll)}></ToolbarButton>
|
||||
</Toolbar>
|
||||
<TestList
|
||||
statusFilters={statusFilters}
|
||||
@ -198,6 +200,8 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
onItemSelected={setSelectedItem}
|
||||
setVisibleTestIds={setVisibleTestIds}
|
||||
watchAll={watchAll}
|
||||
watchedTreeIds={watchedTreeIds}
|
||||
setWatchedTreeIds={setWatchedTreeIds}
|
||||
isLoading={isLoading} />
|
||||
</div>
|
||||
</SplitView>
|
||||
@ -281,19 +285,20 @@ const TestList: React.FC<{
|
||||
testModel: { rootSuite: Suite | undefined, config: FullConfig | undefined },
|
||||
runTests: (mode: 'bounce-if-busy' | 'queue-if-busy', testIds: Set<string>) => void,
|
||||
runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean },
|
||||
watchAll?: boolean,
|
||||
watchAll: boolean,
|
||||
watchedTreeIds: { value: Set<string> },
|
||||
setWatchedTreeIds: (ids: { value: Set<string> }) => void,
|
||||
isLoading?: boolean,
|
||||
setVisibleTestIds: (testIds: Set<string>) => void,
|
||||
onItemSelected: (item: { testCase?: TestCase, location?: Location }) => void,
|
||||
}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, isLoading, onItemSelected, setVisibleTestIds }) => {
|
||||
}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, setVisibleTestIds }) => {
|
||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
||||
const [watchedTreeIds, setWatchedTreeIds] = React.useState<{ value: Set<string> }>({ value: new Set() });
|
||||
|
||||
// Build the test tree.
|
||||
const { rootItem, treeItemMap, fileNames } = React.useMemo(() => {
|
||||
const rootItem = createTree(testModel.rootSuite, projectFilters);
|
||||
filterTree(rootItem, filterText, statusFilters);
|
||||
filterTree(rootItem, filterText, statusFilters, runningState?.testIds);
|
||||
hideOnlyTests(rootItem);
|
||||
const treeItemMap = new Map<string, TreeItem>();
|
||||
const visibleTestIds = new Set<string>();
|
||||
@ -309,7 +314,7 @@ const TestList: React.FC<{
|
||||
visit(rootItem);
|
||||
setVisibleTestIds(visibleTestIds);
|
||||
return { rootItem, treeItemMap, fileNames };
|
||||
}, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds]);
|
||||
}, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState]);
|
||||
|
||||
// Look for a first failure within the run batch to select it.
|
||||
React.useEffect(() => {
|
||||
@ -398,6 +403,7 @@ const TestList: React.FC<{
|
||||
render={treeItem => {
|
||||
return <div className='hbox watch-mode-list-item'>
|
||||
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
|
||||
{!!treeItem.duration && <div className='watch-mode-list-item-time'>{msToString(treeItem.duration)}</div>}
|
||||
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState}></ToolbarButton>
|
||||
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => sendMessageNoReply('open', { location: locationToOpen(treeItem) })}></ToolbarButton>
|
||||
{!watchAll && <ToolbarButton icon='eye' title='Watch' onClick={() => {
|
||||
@ -410,6 +416,8 @@ const TestList: React.FC<{
|
||||
</div>;
|
||||
}}
|
||||
icon={treeItem => {
|
||||
if (treeItem.status === 'scheduled')
|
||||
return 'codicon-clock';
|
||||
if (treeItem.status === 'running')
|
||||
return 'codicon-loading';
|
||||
if (treeItem.status === 'failed')
|
||||
@ -638,9 +646,10 @@ type TreeItemBase = {
|
||||
id: string;
|
||||
title: string;
|
||||
location: Location,
|
||||
duration: number;
|
||||
parent: TreeItem | undefined;
|
||||
children: TreeItem[];
|
||||
status: 'none' | 'running' | 'passed' | 'failed' | 'skipped';
|
||||
status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped';
|
||||
};
|
||||
|
||||
type GroupItem = TreeItemBase & {
|
||||
@ -677,6 +686,7 @@ function getFileItem(rootItem: GroupItem, filePath: string[], isFile: boolean, f
|
||||
id: fileName,
|
||||
title: filePath[filePath.length - 1],
|
||||
location: { file: fileName, line: 0, column: 0 },
|
||||
duration: 0,
|
||||
parent: parentFileItem,
|
||||
children: [],
|
||||
status: 'none',
|
||||
@ -694,6 +704,7 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
id: 'root',
|
||||
title: '',
|
||||
location: { file: '', line: 0, column: 0 },
|
||||
duration: 0,
|
||||
parent: undefined,
|
||||
children: [],
|
||||
status: 'none',
|
||||
@ -710,6 +721,7 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
id: parentGroup.id + '\x1e' + title,
|
||||
title,
|
||||
location: suite.location!,
|
||||
duration: 0,
|
||||
parent: parentGroup,
|
||||
children: [],
|
||||
status: 'none',
|
||||
@ -731,13 +743,16 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
children: [],
|
||||
tests: [],
|
||||
location: test.location,
|
||||
duration: 0,
|
||||
status: 'none',
|
||||
};
|
||||
parentGroup.children.push(testCaseItem);
|
||||
}
|
||||
|
||||
let status: 'none' | 'running' | 'passed' | 'failed' | 'skipped' = 'none';
|
||||
if (test.results.some(r => r.duration === -1))
|
||||
let status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none';
|
||||
if (test.results.some(r => r.workerIndex === -1))
|
||||
status = 'scheduled';
|
||||
else if (test.results.some(r => r.duration === -1))
|
||||
status = 'running';
|
||||
else if (test.results.length && test.results[0].status === 'skipped')
|
||||
status = 'skipped';
|
||||
@ -758,8 +773,10 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
parent: testCaseItem,
|
||||
children: [],
|
||||
status,
|
||||
project: projectName
|
||||
duration: test.results.length ? Math.max(0, test.results[0].duration) : 0,
|
||||
project: projectName,
|
||||
});
|
||||
testCaseItem.duration = (testCaseItem.children as TestItem[]).reduce((a, b) => a + b.duration, 0);
|
||||
}
|
||||
};
|
||||
|
||||
@ -788,16 +805,20 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
let allSkipped = treeItem.children.length > 0;
|
||||
let hasFailed = false;
|
||||
let hasRunning = false;
|
||||
let hasScheduled = false;
|
||||
|
||||
for (const child of treeItem.children) {
|
||||
allSkipped = allSkipped && child.status === 'skipped';
|
||||
allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped');
|
||||
hasFailed = hasFailed || child.status === 'failed';
|
||||
hasRunning = hasRunning || child.status === 'running';
|
||||
hasScheduled = hasScheduled || child.status === 'scheduled';
|
||||
}
|
||||
|
||||
if (hasRunning)
|
||||
treeItem.status = 'running';
|
||||
else if (hasScheduled)
|
||||
treeItem.status = 'scheduled';
|
||||
else if (hasFailed)
|
||||
treeItem.status = 'failed';
|
||||
else if (allSkipped)
|
||||
@ -814,15 +835,17 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
return shortRoot;
|
||||
}
|
||||
|
||||
function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map<string, boolean>) {
|
||||
function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map<string, boolean>, runningTestIds: Set<string> | undefined) {
|
||||
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 (!tokens.every(token => title.includes(token)))
|
||||
if (!tokens.every(token => title.includes(token)) && !testCase.tests.some(t => runningTestIds?.has(t.id)))
|
||||
return false;
|
||||
testCase.children = (testCase.children as TestItem[]).filter(test => !filtersStatuses || statusFilters.get(test.status));
|
||||
testCase.children = (testCase.children as TestItem[]).filter(test => {
|
||||
return !filtersStatuses || runningTestIds?.has(test.id) || statusFilters.get(test.status);
|
||||
});
|
||||
testCase.tests = (testCase.children as TestItem[]).map(c => c.test);
|
||||
return !!testCase.children.length;
|
||||
};
|
||||
|
@ -122,8 +122,11 @@ body.dark-mode .CodeMirror span.cm-type {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.CodeMirror .CodeMirror-gutter-elt {
|
||||
background-color: var(--vscode-editorGutter-background);
|
||||
}
|
||||
|
||||
.CodeMirror .CodeMirror-gutterwrapper {
|
||||
background: var(--vscode-editor-background);
|
||||
border-right: 1px solid var(--vscode-editorGroup-border);
|
||||
color: var(--vscode-editorLineNumber-foreground);
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import { createGuid } from '../../packages/playwright-core/src/utils/crypto';
|
||||
type Latch = {
|
||||
blockingCode: string;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
type Fixtures = {
|
||||
@ -33,8 +34,8 @@ type Fixtures = {
|
||||
createLatch: () => Latch;
|
||||
};
|
||||
|
||||
export function dumpTestTree(page: Page): () => Promise<string> {
|
||||
return () => page.getByTestId('test-tree').evaluate(async treeElement => {
|
||||
export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () => Promise<string> {
|
||||
return () => page.getByTestId('test-tree').evaluate(async (treeElement, options) => {
|
||||
function iconName(iconElement: Element): string {
|
||||
const icon = iconElement.className.replace('codicon codicon-', '');
|
||||
if (icon === 'chevron-right')
|
||||
@ -55,6 +56,8 @@ export function dumpTestTree(page: Page): () => Promise<string> {
|
||||
return '👁';
|
||||
if (icon === 'loading')
|
||||
return '↻';
|
||||
if (icon === 'clock')
|
||||
return '🕦';
|
||||
return icon;
|
||||
}
|
||||
|
||||
@ -67,10 +70,13 @@ export function dumpTestTree(page: Page): () => Promise<string> {
|
||||
const indent = listItem.querySelectorAll('.list-view-indent').length;
|
||||
const watch = listItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : '';
|
||||
const selected = listItem.classList.contains('selected') ? ' <=' : '';
|
||||
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + listItem.textContent + watch + selected);
|
||||
const title = listItem.querySelector('.watch-mode-list-item-title').textContent;
|
||||
const timeElement = options.time ? listItem.querySelector('.watch-mode-list-item-time') : undefined;
|
||||
const time = timeElement ? ' ' + timeElement.textContent.replace(/\d+m?s/, 'XXms') : '';
|
||||
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected);
|
||||
}
|
||||
return '\n' + result.join('\n') + '\n ';
|
||||
});
|
||||
}, options);
|
||||
}
|
||||
|
||||
export const test = base
|
||||
@ -112,6 +118,7 @@ export const test = base
|
||||
return {
|
||||
blockingCode: `await ((${waitForLatch})(${JSON.stringify(latchFile)}))`,
|
||||
open: () => fs.writeFileSync(latchFile, 'ok'),
|
||||
close: () => fs.unlinkSync(latchFile),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -148,3 +148,33 @@ test('should filter by project', async ({ runUITest }) => {
|
||||
|
||||
await expect(page.getByText('Projects: foo bar')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not hide filtered while running', async ({ runUITest, createLatch }) => {
|
||||
const latch = createLatch();
|
||||
const page = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
test('fails', async () => {
|
||||
${latch.blockingCode}
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
`,
|
||||
});
|
||||
await page.getByTitle('Run all').click();
|
||||
latch.open();
|
||||
await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(`
|
||||
▼ ❌ a.test.ts
|
||||
✅ passes
|
||||
❌ fails <=
|
||||
`);
|
||||
|
||||
latch.close();
|
||||
await page.getByText('Status:').click();
|
||||
await page.getByLabel('failed').setChecked(true);
|
||||
await page.getByTitle('Run all').click();
|
||||
await expect.poll(dumpTestTree(page), { timeout: 15000 }).toBe(`
|
||||
▼ ↻ a.test.ts
|
||||
↻ fails <=
|
||||
`);
|
||||
});
|
||||
|
@ -238,7 +238,7 @@ test('should stop', async ({ runUITest }) => {
|
||||
⊘ test 0
|
||||
✅ test 1
|
||||
↻ test 2
|
||||
↻ test 3
|
||||
🕦 test 3
|
||||
`);
|
||||
|
||||
await expect(page.getByTitle('Run all')).toBeDisabled();
|
||||
@ -282,3 +282,27 @@ test('should run folder', async ({ runUITest }) => {
|
||||
◯ passes
|
||||
`);
|
||||
});
|
||||
|
||||
test('should show time', async ({ runUITest }) => {
|
||||
const page = await runUITest(basicTestTree);
|
||||
await expect.poll(dumpTestTree(page), { timeout: 15000 }).toContain(`
|
||||
▼ ◯ a.test.ts
|
||||
`);
|
||||
|
||||
await page.getByTitle('Run all').click();
|
||||
|
||||
await expect.poll(dumpTestTree(page, { time: true }), { timeout: 15000 }).toBe(`
|
||||
▼ ❌ a.test.ts
|
||||
✅ passes XXms
|
||||
❌ fails XXms <=
|
||||
► ❌ suite
|
||||
▼ ❌ b.test.ts
|
||||
✅ passes XXms
|
||||
❌ fails XXms
|
||||
▼ ✅ c.test.ts
|
||||
✅ passes XXms
|
||||
⊘ skipped
|
||||
`);
|
||||
|
||||
await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)');
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user