Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 41 additions & 30 deletions packages/html-reporter/src/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class Filter {
text: FilterToken[] = [];
labels: FilterToken[] = [];
annotations: FilterToken[] = [];
sort: FilterToken[] = [];

empty(): boolean {
return (
Expand All @@ -36,42 +37,26 @@ export class Filter {
}

static parse(expression: string): Filter {
const tokens = Filter.tokenize(expression);
const project = new Set<FilterToken>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the sets got useless through #35390.

const status = new Set<FilterToken>();
const text: FilterToken[] = [];
const labels = new Set<FilterToken>();
const annotations = new Set<FilterToken>();
for (let token of tokens) {
const filter = new Filter();
for (let token of Filter.tokenize(expression)) {
const not = token.startsWith('!');
if (not)
token = token.slice(1);

if (token.startsWith('p:')) {
project.add({ name: token.slice(2), not });
continue;
}
if (token.startsWith('s:')) {
status.add({ name: token.slice(2), not });
continue;
}
if (token.startsWith('@')) {
labels.add({ name: token, not });
continue;
}
if (token.startsWith('annot:')) {
annotations.add({ name: token.slice('annot:'.length), not });
continue;
}
text.push({ name: token.toLowerCase(), not });
if (token.startsWith('o:'))
filter.sort.push({ name: token.slice(2), not });
else if (token.startsWith('p:'))
filter.project.push({ name: token.slice(2), not });
else if (token.startsWith('s:'))
filter.status.push({ name: token.slice(2), not });
else if (token.startsWith('@'))
filter.labels.push({ name: token, not });
else if (token.startsWith('annot:'))
filter.annotations.push({ name: token.slice('annot:'.length), not });
else
filter.text.push({ name: token.toLowerCase(), not });
}

const filter = new Filter();
filter.text = text;
filter.project = [...project];
filter.status = [...status];
filter.labels = [...labels];
filter.annotations = [...annotations];
return filter;
}

Expand Down Expand Up @@ -169,6 +154,32 @@ export class Filter {
}
return true;
}

sortTests(tests: TestCaseSummary[]): void {
if (!this.sort.length)
return;

tests.sort((a, b) => {
for (const sortToken of this.sort) {
const comparison = this._compareTests(a, b, sortToken.name);
if (comparison !== 0)
return sortToken.not ? -comparison : comparison;
}
return this._compareTests(a, b, 'file');
});
}

private _compareTests(a: TestCaseSummary, b: TestCaseSummary, field: string): number {
if (field === 'duration')
return b.duration - a.duration;
if (field === 'title')
return a.title.localeCompare(b.title);
if (field === 'line')
return a.location.line - b.location.line;
if (field !== 'status' && field !== 'file' && field !== 'project' && field !== 'text')
return 0;
return cacheSearchValues(a)[field].localeCompare(cacheSearchValues(b)[field]);
}
}

type SearchValues = {
Expand Down
30 changes: 29 additions & 1 deletion packages/html-reporter/src/reportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ export const ReportView: React.FC<{
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
const filteredStats = React.useMemo(() => filter.empty() ? undefined : computeStats(report?.json().files || [], filter), [report, filter]);
const testModel = React.useMemo(() => {
return mergeFiles ? createMergedFilesModel(report, filter) : createFilesModel(report, filter);
if (filter.sort.length)
return createSortedNoFilesModel(report, filter);
if (mergeFiles)
return createMergedFilesModel(report, filter);
return createFilesModel(report, filter);
}, [report, filter, mergeFiles]);

const { prev, next } = React.useMemo(() => {
Expand Down Expand Up @@ -246,3 +250,27 @@ function createMergedFilesModel(report: LoadedReport | undefined, filter: Filter
result.tests.push(...group.tests);
return result;
}

function createSortedNoFilesModel(report: LoadedReport | undefined, filter: Filter): TestModelSummary {
let tests = (report?.json().files ?? []).flatMap(file => file.tests);
tests = tests.filter(t => filter.matches(t));
filter.sortTests(tests);

const files: TestFileSummary[] = [];
const chunkSize = 100;

for (let i = 0; i < tests.length; i += chunkSize) {
const chunk = tests.slice(i, i + chunkSize);
files.push({
fileId: '' + i,
fileName: `${i} - ${i + chunkSize - 1}`,
tests: chunk,
stats: { total: 0, expected: 0, unexpected: 0, flaky: 0, skipped: 0, ok: true }
});
}

return {
files,
tests
};
}
112 changes: 112 additions & 0 deletions tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3247,6 +3247,118 @@ test('should support merge files option', async ({ runInlineTest, showReport, pa
`);
});

test('should support sorting', async ({ runInlineTest, showReport, page }) => {
await runInlineTest({
'a.test.js': `
import { test, expect } from '@playwright/test';
test('fast test', async ({}) => {
await new Promise(resolve => setTimeout(resolve, 200));
});
test('slow test', async ({}) => {
await new Promise(resolve => setTimeout(resolve, 500));
});
test('medium test', async ({}) => {
await new Promise(resolve => setTimeout(resolve, 300));
});
`,
'b.test.js': `
import { test, expect } from '@playwright/test';
test('very fast test', async ({}) => {
await new Promise(resolve => setTimeout(resolve, 100));
});
test('very slow test', async ({}) => {
await new Promise(resolve => setTimeout(resolve, 600));
});
test('known slow', async ({}) => {
test.slow();
await new Promise(resolve => setTimeout(resolve, 400));
});
`,
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });

await showReport();

const searchInput = page.locator('.subnav-search-input');

await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- button "a.test.js" [expanded]
- region:
- link "fast test"
- link "slow test"
- link "medium test"
- button "b.test.js" [expanded]
- region:
- link "very fast test"
- link "very slow test"
- link "known slow"
`);

await searchInput.fill('o:duration');
await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- button [expanded]
- region:
- link "very slow test"
- link "slow test"
- link "known slow"
- link "medium test"
- link "fast test"
- link "very fast test"
`);

await searchInput.fill('o:duration !annot:slow');
await expect(page.getByRole('link', { name: 'known slow' })).not.toBeVisible();

await searchInput.fill('!o:duration');
await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- button [expanded]
- region:
- link "very fast test"
- link "fast test"
- link "medium test"
- link "known slow"
- link "slow test"
- link "very slow test"
`);

await searchInput.fill('o:title');
await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- button [expanded]
- region:
- link "fast test"
- link "known slow"
- link "medium test"
- link "slow test"
- link "very fast test"
- link "very slow test"
`);

await searchInput.fill('o:file');
await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- button [expanded]
- region:
- link "fast test"
- link "slow test"
- link "medium test"
- link "very fast test"
- link "very slow test"
- link "known slow"
`);

await searchInput.clear();
await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- button "a.test.js" [expanded]
- region:
- link "fast test"
- link "slow test"
- link "medium test"
- button "b.test.js" [expanded]
- region:
- link "very fast test"
- link "very slow test"
- link "known slow"
`);
});

function readAllFromStream(stream: NodeJS.ReadableStream): Promise<Buffer> {
return new Promise(resolve => {
const chunks: Buffer[] = [];
Expand Down