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;
|
||||
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 { Link, navigate } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import { filterWithToken } from './filter';
|
||||
|
||||
export const HeaderView: React.FC<React.PropsWithChildren<{
|
||||
stats: Stats,
|
||||
@ -64,20 +65,23 @@ export const HeaderView: React.FC<React.PropsWithChildren<{
|
||||
const StatsNavView: React.FC<{
|
||||
stats: Stats
|
||||
}> = ({ stats }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const q = searchParams.get('q')?.toString() || '';
|
||||
const tokens = q.split(' ');
|
||||
return <nav>
|
||||
<Link className='subnav-item' href='#?'>
|
||||
All <span className='d-inline counter'>{stats.total - stats.skipped}</span>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</Link>
|
||||
</nav>;
|
||||
|
@ -41,12 +41,19 @@ export const Route: React.FunctionComponent<{
|
||||
};
|
||||
|
||||
export const Link: React.FunctionComponent<{
|
||||
href: string,
|
||||
href?: string,
|
||||
click?: string,
|
||||
ctrlClick?: string,
|
||||
className?: string,
|
||||
title?: string,
|
||||
children: any,
|
||||
}> = ({ href, className, children, title }) => {
|
||||
return <a style={{ textDecoration: 'none', color: 'var(--color-fg-default)' }} className={`${className || ''}`} href={href} title={title}>{children}</a>;
|
||||
}> = ({ href, click, ctrlClick, className, children, title }) => {
|
||||
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<{
|
||||
|
@ -18,7 +18,7 @@ import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import { msToString } from './uiUtils';
|
||||
import { Chip } from './chip';
|
||||
import type { Filter } from './filter';
|
||||
import { filterWithToken, type Filter } from './filter';
|
||||
import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import './testFileView.css';
|
||||
@ -94,23 +94,9 @@ const LabelsClickView: React.FC<React.PropsWithChildren<{
|
||||
const onClickHandle = (e: React.MouseEvent, label: string) => {
|
||||
e.preventDefault();
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
let q = searchParams.get('q')?.toString() || '';
|
||||
|
||||
// If metaKey or ctrlKey is pressed, add tag to search query without replacing existing tags.
|
||||
// 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}` : '#');
|
||||
const q = searchParams.get('q')?.toString() || '';
|
||||
const tokens = q.split(' ');
|
||||
navigate(filterWithToken(tokens, label, e.metaKey || e.ctrlKey));
|
||||
};
|
||||
|
||||
return labels.length > 0 ? (
|
||||
|
@ -43,7 +43,7 @@ const expect = baseExpect.configure({ timeout: process.env.CI ? 75000 : 25000 })
|
||||
|
||||
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.use({ useIntermediateMergeReport });
|
||||
|
||||
@ -1649,8 +1649,8 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||
const passedNavMenu = page.locator('.subnav-item:has-text("Passed")');
|
||||
const failedNavMenu = page.locator('.subnav-item:has-text("Failed")');
|
||||
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 regressionLabelButton = page.locator('.test-file-test', { has: page.getByText('@regression passes', { exact: true }) }).locator('.label', { hasText: 'regression' });
|
||||
const smokeLabelButton = page.locator('.label', { hasText: 'smoke' }).first();
|
||||
const regressionLabelButton = page.locator('.label', { hasText: 'regression' }).first();
|
||||
|
||||
await failedNavMenu.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 passedNavMenu.click();
|
||||
await smokeLabelButton.click({ modifiers: [process.platform === 'darwin' ? 'Meta' : 'Control'] });
|
||||
await regressionLabelButton.click();
|
||||
await expect(page.locator('.test-file-test')).toHaveCount(1);
|
||||
await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1);
|
||||
|
Loading…
x
Reference in New Issue
Block a user