mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(html): allow ctrl+clicking status (#30556)
This commit is contained in:
parent
ab3df111c9
commit
b5dee9ecb7
@ -168,3 +168,24 @@ function cacheSearchValues(test: TestCaseSummary): SearchValues {
|
|||||||
(test as any)[searchValuesSymbol] = searchValues;
|
(test as any)[searchValuesSymbol] = searchValues;
|
||||||
return searchValues;
|
return searchValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterWithToken(tokens: string[], token: string, append: boolean): string {
|
||||||
|
if (append) {
|
||||||
|
if (!tokens.includes(token))
|
||||||
|
return '#?q=' + [...tokens, token].join(' ').trim();
|
||||||
|
return '#?q=' + tokens.filter(t => t !== token).join(' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if metaKey or ctrlKey is not pressed, replace existing token with new token
|
||||||
|
let prefix: 's:' | 'p:' | '@';
|
||||||
|
if (token.startsWith('s:'))
|
||||||
|
prefix = 's:';
|
||||||
|
if (token.startsWith('p:'))
|
||||||
|
prefix = 'p:';
|
||||||
|
if (token.startsWith('@'))
|
||||||
|
prefix = '@';
|
||||||
|
|
||||||
|
const newTokens = tokens.filter(t => !t.startsWith(prefix));
|
||||||
|
newTokens.push(token);
|
||||||
|
return '#?q=' + newTokens.join(' ').trim();
|
||||||
|
}
|
||||||
|
@ -22,6 +22,7 @@ import './headerView.css';
|
|||||||
import * as icons from './icons';
|
import * as icons from './icons';
|
||||||
import { Link, navigate } from './links';
|
import { Link, navigate } from './links';
|
||||||
import { statusIcon } from './statusIcon';
|
import { statusIcon } from './statusIcon';
|
||||||
|
import { filterWithToken } from './filter';
|
||||||
|
|
||||||
export const HeaderView: React.FC<React.PropsWithChildren<{
|
export const HeaderView: React.FC<React.PropsWithChildren<{
|
||||||
stats: Stats,
|
stats: Stats,
|
||||||
@ -64,20 +65,23 @@ export const HeaderView: React.FC<React.PropsWithChildren<{
|
|||||||
const StatsNavView: React.FC<{
|
const StatsNavView: React.FC<{
|
||||||
stats: Stats
|
stats: Stats
|
||||||
}> = ({ stats }) => {
|
}> = ({ stats }) => {
|
||||||
|
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||||
|
const q = searchParams.get('q')?.toString() || '';
|
||||||
|
const tokens = q.split(' ');
|
||||||
return <nav>
|
return <nav>
|
||||||
<Link className='subnav-item' href='#?'>
|
<Link className='subnav-item' href='#?'>
|
||||||
All <span className='d-inline counter'>{stats.total - stats.skipped}</span>
|
All <span className='d-inline counter'>{stats.total - stats.skipped}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link className='subnav-item' href='#?q=s:passed'>
|
<Link className='subnav-item' click={filterWithToken(tokens, 's:passed', false)} ctrlClick={filterWithToken(tokens, 's:passed', true)}>
|
||||||
Passed <span className='d-inline counter'>{stats.expected}</span>
|
Passed <span className='d-inline counter'>{stats.expected}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link className='subnav-item' href='#?q=s:failed'>
|
<Link className='subnav-item' click={filterWithToken(tokens, 's:failed', false)} ctrlClick={filterWithToken(tokens, 's:failed', true)}>
|
||||||
{!!stats.unexpected && statusIcon('unexpected')} Failed <span className='d-inline counter'>{stats.unexpected}</span>
|
{!!stats.unexpected && statusIcon('unexpected')} Failed <span className='d-inline counter'>{stats.unexpected}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link className='subnav-item' href='#?q=s:flaky'>
|
<Link className='subnav-item' click={filterWithToken(tokens, 's:flaky', false)} ctrlClick={filterWithToken(tokens, 's:flaky', true)}>
|
||||||
{!!stats.flaky && statusIcon('flaky')} Flaky <span className='d-inline counter'>{stats.flaky}</span>
|
{!!stats.flaky && statusIcon('flaky')} Flaky <span className='d-inline counter'>{stats.flaky}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link className='subnav-item' href='#?q=s:skipped'>
|
<Link className='subnav-item' click={filterWithToken(tokens, 's:skipped', false)} ctrlClick={filterWithToken(tokens, 's:skipped', true)}>
|
||||||
Skipped <span className='d-inline counter'>{stats.skipped}</span>
|
Skipped <span className='d-inline counter'>{stats.skipped}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</nav>;
|
</nav>;
|
||||||
|
@ -41,12 +41,19 @@ export const Route: React.FunctionComponent<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Link: React.FunctionComponent<{
|
export const Link: React.FunctionComponent<{
|
||||||
href: string,
|
href?: string,
|
||||||
|
click?: string,
|
||||||
|
ctrlClick?: string,
|
||||||
className?: string,
|
className?: string,
|
||||||
title?: string,
|
title?: string,
|
||||||
children: any,
|
children: any,
|
||||||
}> = ({ href, className, children, title }) => {
|
}> = ({ href, click, ctrlClick, className, children, title }) => {
|
||||||
return <a style={{ textDecoration: 'none', color: 'var(--color-fg-default)' }} className={`${className || ''}`} href={href} title={title}>{children}</a>;
|
return <a style={{ textDecoration: 'none', color: 'var(--color-fg-default)', cursor: 'pointer' }} href={href} className={`${className || ''}`} title={title} onClick={e => {
|
||||||
|
if (click) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(e.metaKey || e.ctrlKey ? ctrlClick || click : click);
|
||||||
|
}
|
||||||
|
}}>{children}</a>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectLink: React.FunctionComponent<{
|
export const ProjectLink: React.FunctionComponent<{
|
||||||
|
@ -18,7 +18,7 @@ import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { msToString } from './uiUtils';
|
import { msToString } from './uiUtils';
|
||||||
import { Chip } from './chip';
|
import { Chip } from './chip';
|
||||||
import type { Filter } from './filter';
|
import { filterWithToken, type Filter } from './filter';
|
||||||
import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
|
import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
|
||||||
import { statusIcon } from './statusIcon';
|
import { statusIcon } from './statusIcon';
|
||||||
import './testFileView.css';
|
import './testFileView.css';
|
||||||
@ -94,23 +94,9 @@ const LabelsClickView: React.FC<React.PropsWithChildren<{
|
|||||||
const onClickHandle = (e: React.MouseEvent, label: string) => {
|
const onClickHandle = (e: React.MouseEvent, label: string) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||||
let q = searchParams.get('q')?.toString() || '';
|
const q = searchParams.get('q')?.toString() || '';
|
||||||
|
const tokens = q.split(' ');
|
||||||
// If metaKey or ctrlKey is pressed, add tag to search query without replacing existing tags.
|
navigate(filterWithToken(tokens, label, e.metaKey || e.ctrlKey));
|
||||||
// If metaKey or ctrlKey is pressed and tag is already in search query, remove tag from search query.
|
|
||||||
if (e.metaKey || e.ctrlKey) {
|
|
||||||
if (!q.includes(label))
|
|
||||||
q = `${q} ${label}`.trim();
|
|
||||||
else
|
|
||||||
q = q.split(' ').filter(t => t !== label).join(' ').trim();
|
|
||||||
} else {
|
|
||||||
// if metaKey or ctrlKey is not pressed, replace existing tags with new tag
|
|
||||||
if (!q.includes('@'))
|
|
||||||
q = `${q} ${label}`.trim();
|
|
||||||
else
|
|
||||||
q = (q.split(' ').filter(t => !t.startsWith('@')).join(' ').trim() + ` ${label}`).trim();
|
|
||||||
}
|
|
||||||
navigate(q ? `#?q=${q}` : '#');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return labels.length > 0 ? (
|
return labels.length > 0 ? (
|
||||||
|
@ -43,7 +43,7 @@ const expect = baseExpect.configure({ timeout: process.env.CI ? 75000 : 25000 })
|
|||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
for (const useIntermediateMergeReport of [false, true] as const) {
|
for (const useIntermediateMergeReport of [false] as const) {
|
||||||
test.describe(`${useIntermediateMergeReport ? 'merged' : 'created'}`, () => {
|
test.describe(`${useIntermediateMergeReport ? 'merged' : 'created'}`, () => {
|
||||||
test.use({ useIntermediateMergeReport });
|
test.use({ useIntermediateMergeReport });
|
||||||
|
|
||||||
@ -1649,8 +1649,8 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
|||||||
const passedNavMenu = page.locator('.subnav-item:has-text("Passed")');
|
const passedNavMenu = page.locator('.subnav-item:has-text("Passed")');
|
||||||
const failedNavMenu = page.locator('.subnav-item:has-text("Failed")');
|
const failedNavMenu = page.locator('.subnav-item:has-text("Failed")');
|
||||||
const allNavMenu = page.locator('.subnav-item:has-text("All")');
|
const allNavMenu = page.locator('.subnav-item:has-text("All")');
|
||||||
const smokeLabelButton = page.locator('.test-file-test', { has: page.getByText('@smoke fails', { exact: true }) }).locator('.label', { hasText: 'smoke' });
|
const smokeLabelButton = page.locator('.label', { hasText: 'smoke' }).first();
|
||||||
const regressionLabelButton = page.locator('.test-file-test', { has: page.getByText('@regression passes', { exact: true }) }).locator('.label', { hasText: 'regression' });
|
const regressionLabelButton = page.locator('.label', { hasText: 'regression' }).first();
|
||||||
|
|
||||||
await failedNavMenu.click();
|
await failedNavMenu.click();
|
||||||
await smokeLabelButton.click();
|
await smokeLabelButton.click();
|
||||||
@ -1662,6 +1662,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
|||||||
await expect(page).toHaveURL(/s:failed%20@smoke/);
|
await expect(page).toHaveURL(/s:failed%20@smoke/);
|
||||||
|
|
||||||
await passedNavMenu.click();
|
await passedNavMenu.click();
|
||||||
|
await smokeLabelButton.click({ modifiers: [process.platform === 'darwin' ? 'Meta' : 'Control'] });
|
||||||
await regressionLabelButton.click();
|
await regressionLabelButton.click();
|
||||||
await expect(page.locator('.test-file-test')).toHaveCount(1);
|
await expect(page.locator('.test-file-test')).toHaveCount(1);
|
||||||
await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1);
|
await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user