feat(html): add NOT filtering to HTML reporter (#35390)

This commit is contained in:
Mark Skelton 2025-03-31 09:18:13 -04:00 committed by GitHub
parent 62a8d4e01f
commit 3d603d1e5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 121 additions and 40 deletions

View File

@ -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<string>();
const status = new Set<string>();
const text: string[] = [];
const labels = new Set<string>();
const annotations = new Set<string>();
for (const token of tokens) {
const project = new Set<FilterToken>();
const status = new Set<FilterToken>();
const text: FilterToken[] = [];
const labels = new Set<FilterToken>();
const annotations = new Set<FilterToken>();
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;
}

View File

@ -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 }) => {