diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts
index f8c61ba902..249c338c9a 100644
--- a/packages/html-reporter/src/filter.ts
+++ b/packages/html-reporter/src/filter.ts
@@ -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();
+}
diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx
index 1dc5420eec..925bc64721 100644
--- a/packages/html-reporter/src/headerView.tsx
+++ b/packages/html-reporter/src/headerView.tsx
@@ -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 = ({ stats }) => {
+ const searchParams = new URLSearchParams(window.location.hash.slice(1));
+ const q = searchParams.get('q')?.toString() || '';
+ const tokens = q.split(' ');
return ;
diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx
index daece0aefe..cededa0dbc 100644
--- a/packages/html-reporter/src/links.tsx
+++ b/packages/html-reporter/src/links.tsx
@@ -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 {children};
+}> = ({ href, click, ctrlClick, className, children, title }) => {
+ return {
+ if (click) {
+ e.preventDefault();
+ navigate(e.metaKey || e.ctrlKey ? ctrlClick || click : click);
+ }
+ }}>{children};
};
export const ProjectLink: React.FunctionComponent<{
diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx
index 4f508c42b8..8e478f95d7 100644
--- a/packages/html-reporter/src/testFileView.tsx
+++ b/packages/html-reporter/src/testFileView.tsx
@@ -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 {
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 ? (
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index afc8fa5dda..0f8505889e 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -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);