diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts
index 280c9a566b..135336e869 100644
--- a/packages/html-reporter/src/filter.ts
+++ b/packages/html-reporter/src/filter.ts
@@ -15,12 +15,18 @@
*/
import type { TestCaseSummary } from './types';
+
+type FilterToken = {
+ name: string;
+ not: boolean;
+};
+
export class Filter {
- project: string[] = [];
- status: string[] = [];
- text: string[] = [];
- labels: string[] = [];
- annotations: string[] = [];
+ project: FilterToken[] = [];
+ status: FilterToken[] = [];
+ text: FilterToken[] = [];
+ labels: FilterToken[] = [];
+ annotations: FilterToken[] = [];
empty(): boolean {
return this.project.length + this.status.length + this.text.length === 0;
@@ -28,29 +34,33 @@ export class Filter {
static parse(expression: string): Filter {
const tokens = Filter.tokenize(expression);
- const project = new Set();
- const status = new Set();
- const text: string[] = [];
- const labels = new Set();
- const annotations = new Set();
- for (const token of tokens) {
+ const project = new Set();
+ const status = new Set();
+ const text: FilterToken[] = [];
+ const labels = new Set();
+ const annotations = new Set();
+ for (let token of tokens) {
+ const not = token.startsWith('!');
+ if (not)
+ token = token.slice(1);
+
if (token.startsWith('p:')) {
- project.add(token.slice(2));
+ project.add({ name: token.slice(2), not });
continue;
}
if (token.startsWith('s:')) {
- status.add(token.slice(2));
+ status.add({ name: token.slice(2), not });
continue;
}
if (token.startsWith('@')) {
- labels.add(token);
+ labels.add({ name: token, not });
continue;
}
if (token.startsWith('annot:')) {
- annotations.add(token.slice('annot:'.length));
+ annotations.add({ name: token.slice('annot:'.length), not });
continue;
}
- text.push(token.toLowerCase());
+ text.push({ name: token.toLowerCase(), not });
}
const filter = new Filter();
@@ -106,12 +116,18 @@ export class Filter {
matches(test: TestCaseSummary): boolean {
const searchValues = cacheSearchValues(test);
if (this.project.length) {
- const matches = !!this.project.find(p => searchValues.project.includes(p));
+ const matches = !!this.project.find(p => {
+ const match = searchValues.project.includes(p.name);
+ return p.not ? !match : match;
+ });
if (!matches)
return false;
}
if (this.status.length) {
- const matches = !!this.status.find(s => searchValues.status.includes(s));
+ const matches = !!this.status.find(s => {
+ const match = searchValues.status.includes(s.name);
+ return s.not ? !match : match;
+ });
if (!matches)
return false;
} else {
@@ -119,23 +135,32 @@ export class Filter {
return false;
}
if (this.text.length) {
- for (const text of this.text) {
- if (searchValues.text.includes(text))
- continue;
- const [fileName, line, column] = text.split(':');
+ const matches = this.text.every(text => {
+ if (searchValues.text.includes(text.name))
+ return text.not ? false : true;
+
+ const [fileName, line, column] = text.name.split(':');
if (searchValues.file.includes(fileName) && searchValues.line === line && (column === undefined || searchValues.column === column))
- continue;
+ return text.not ? false : true;
+
+ return text.not ? true : false;
+ });
+ if (!matches)
return false;
- }
}
if (this.labels.length) {
- const matches = this.labels.every(l => searchValues.labels.includes(l));
+ const matches = this.labels.every(l => {
+ const match = searchValues.labels.includes(l.name);
+ return l.not ? !match : match;
+ });
if (!matches)
return false;
}
if (this.annotations.length) {
- const matches = this.annotations.every(annotation =>
- searchValues.annotations.some(a => a.includes(annotation)));
+ const matches = this.annotations.every(annotation => {
+ const match = searchValues.annotations.some(a => a.includes(annotation.name));
+ return annotation.not ? !match : match;
+ });
if (!matches)
return false;
}
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index 18d23de7c8..fc353a233e 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -2010,7 +2010,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await expect(page.locator('.test-file-test')).toHaveCount(2);
await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1);
await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1);
- await expect(page.locator('.test-file-test .test-file-title')).toHaveCount(2);
+ await expect(page.locator('.test-file-test .test-file-title')).toHaveText(['@smoke fails', '@smoke passes']);
await expect(searchInput).toHaveValue('@smoke ');
await expect(page).toHaveURL(/%40smoke/);
@@ -2019,9 +2019,18 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await expect(page.locator('.test-file-test')).toHaveCount(2);
await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1);
await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1);
- await expect(page.locator('.test-file-test .test-file-title')).toHaveCount(2);
+ await expect(page.locator('.test-file-test .test-file-title')).toHaveText(['@regression fails', '@regression passes']);
await expect(searchInput).toHaveValue('@regression ');
await expect(page).toHaveURL(/%40regression/);
+
+ await searchInput.fill('!@regression');
+ await searchInput.press('Enter');
+ await expect(page.locator('.test-file-test')).toHaveCount(2);
+ await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1);
+ await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1);
+ await expect(page.locator('.test-file-test .test-file-title')).toHaveText(['@smoke fails', '@smoke passes']);
+ await expect(searchInput).toHaveValue('!@regression ');
+ await expect(page).toHaveURL(/%21%40regression/);
});
test('if label contains similar words only one label should be selected', async ({ runInlineTest, showReport, page }) => {
@@ -2421,13 +2430,33 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await expect(page.getByText('file-a.test.js', { exact: true })).toBeVisible();
await expect(page.getByText('a test 1')).toBeVisible();
await expect(page.getByText('a test 2')).toBeVisible();
- await expect(page.getByText('file-b.test.js', { exact: true })).not.toBeVisible();
- await expect(page.getByText('b test 1')).not.toBeVisible();
- await expect(page.getByText('b test 2')).not.toBeVisible();
+ await expect(page.getByText('file-b.test.js', { exact: true })).toBeHidden();
+ await expect(page.getByText('b test 1')).toBeHidden();
+ await expect(page.getByText('b test 2')).toBeHidden();
+
+ await searchInput.fill('!file-a');
+ await expect(page.getByText('file-a.test.js', { exact: true })).toBeHidden();
+ await expect(page.getByText('a test 1')).toBeHidden();
+ await expect(page.getByText('a test 2')).toBeHidden();
+ await expect(page.getByText('file-b.test.js', { exact: true })).toBeVisible();
+ await expect(page.getByText('b test 1')).toBeVisible();
+ await expect(page.getByText('b test 2')).toBeVisible();
await searchInput.fill('file-a:3');
+ await expect(page.getByText('file-a.test.js', { exact: true })).toBeVisible();
await expect(page.getByText('a test 1')).toBeVisible();
- await expect(page.getByText('a test 2')).not.toBeVisible();
+ await expect(page.getByText('a test 2')).toBeHidden();
+ await expect(page.getByText('file-b.test.js', { exact: true })).toBeHidden();
+ await expect(page.getByText('b test 1')).toBeHidden();
+ await expect(page.getByText('b test 2')).toBeHidden();
+
+ await searchInput.fill('!file-a:3');
+ await expect(page.getByText('file-a.test.js', { exact: true })).toBeVisible();
+ await expect(page.getByText('a test 1')).toBeHidden();
+ await expect(page.getByText('a test 2')).toBeVisible();
+ await expect(page.getByText('file-b.test.js', { exact: true })).toBeVisible();
+ await expect(page.getByText('b test 1')).toBeVisible();
+ await expect(page.getByText('b test 2')).toBeVisible();
});
test('tests should filter by status', async ({ runInlineTest, showReport, page }) => {
@@ -2449,17 +2478,22 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await searchInput.fill('s:failed');
await expect(page.getByText('a.test.js', { exact: true })).toBeVisible();
- await expect(page.getByText('failed title')).not.toBeVisible();
+ await expect(page.getByText('failed title')).toBeHidden();
await expect(page.getByText('passes title')).toBeVisible();
+
+ await searchInput.fill('!s:failed');
+ await expect(page.getByText('a.test.js', { exact: true })).toBeVisible();
+ await expect(page.getByText('failed title')).toBeVisible();
+ await expect(page.getByText('passes title')).toBeHidden();
});
test('tests should filter by annotation texts', async ({ runInlineTest, showReport, page }) => {
const result = await runInlineTest({
'a.test.js': `
const { test, expect } = require('@playwright/test');
- test('annotated test',{ annotation :[{type:'key',description:'value'}]}, async ({}) => {expect(1).toBe(1);});
+ test('with annotation',{ annotation :[{type:'key',description:'value'}]}, async ({}) => {expect(1).toBe(1);});
test('slow test', () => { test.slow(); });
- test('non-annotated test', async ({}) => {expect(1).toBe(2);});
+ test('without annotation', async ({}) => {expect(1).toBe(2);});
`,
}, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' });
@@ -2474,15 +2508,29 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await test.step('filter by type and value', async () => {
await searchInput.fill('annot:key=value');
await expect(page.getByText('a.test.js', { exact: true })).toBeVisible();
- await expect(page.getByText('non-annotated test')).not.toBeVisible();
- await expect(page.getByText('annotated test')).toBeVisible();
+ await expect(page.getByText('without annotation')).toBeHidden();
+ await expect(page.getByText('with annotation')).toBeVisible();
+ });
+
+ await test.step('NOT filter by type and value', async () => {
+ await searchInput.fill('!annot:key=value');
+ await expect(page.getByText('a.test.js', { exact: true })).toBeVisible();
+ await expect(page.getByText('without annotation')).toBeVisible();
+ await expect(page.getByText('with annotation')).toBeHidden();
});
await test.step('filter by type', async () => {
await searchInput.fill('annot:key');
await expect(page.getByText('a.test.js', { exact: true })).toBeVisible();
- await expect(page.getByText('non-annotated test')).not.toBeVisible();
- await expect(page.getByText('annotated test')).toBeVisible();
+ await expect(page.getByText('without annotation')).toBeHidden();
+ await expect(page.getByText('with annotation')).toBeVisible();
+ });
+
+ await test.step('NOT filter by type', async () => {
+ await searchInput.fill('!annot:key');
+ await expect(page.getByText('a.test.js', { exact: true })).toBeVisible();
+ await expect(page.getByText('without annotation')).toBeVisible();
+ await expect(page.getByText('with annotation')).toBeHidden();
});
await test.step('filter by result annotation', async () => {
@@ -2516,9 +2564,17 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await expect(page.getByText('a.test.js:3', { exact: true })).toBeVisible();
await expect(page.getByText('a.test.js:4', { exact: true })).toBeHidden();
+ await searchInput.fill('!a.test.js:3');
+ await expect(page.getByText('a.test.js:3', { exact: true })).toBeHidden();
+ await expect(page.getByText('a.test.js:4', { exact: true })).toBeVisible();
+
await searchInput.fill('a.test.js:4:15');
await expect(page.getByText('a.test.js:3', { exact: true })).toBeHidden();
await expect(page.getByText('a.test.js:4', { exact: true })).toBeVisible();
+
+ await searchInput.fill('!a.test.js:4:15');
+ await expect(page.getByText('a.test.js:3', { exact: true })).toBeVisible();
+ await expect(page.getByText('a.test.js:4', { exact: true })).toBeHidden();
});
test('should properly display beforeEach with and without title', async ({ runInlineTest, showReport, page }) => {