From 35ab3f36882605deda5127217687141bce7c19c6 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:06:56 +0200 Subject: [PATCH 01/17] feat(html-reporter): add sorting support for test results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ability to sort test results in the HTML reporter using query syntax: - `o:duration` sorts by duration ascending - `!o:duration` sorts by duration descending - Supports multiple sort criteria for priority sorting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/html-reporter/src/filter.ts | 23 ++++++++++++++ packages/html-reporter/src/reportView.tsx | 5 +++- tests/playwright-test/reporter-html.spec.ts | 33 +++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 7e54c98d4d0dd..62fbac2ec401c 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -27,6 +27,7 @@ export class Filter { text: FilterToken[] = []; labels: FilterToken[] = []; annotations: FilterToken[] = []; + sort: FilterToken[] = []; empty(): boolean { return ( @@ -42,11 +43,16 @@ export class Filter { const text: FilterToken[] = []; const labels = new Set(); const annotations = new Set(); + const sort: FilterToken[] = []; for (let token of tokens) { const not = token.startsWith('!'); if (not) token = token.slice(1); + if (token.startsWith('o:')) { + sort.push({ name: token.slice(2), not }); + continue; + } if (token.startsWith('p:')) { project.add({ name: token.slice(2), not }); continue; @@ -72,6 +78,7 @@ export class Filter { filter.status = [...status]; filter.labels = [...labels]; filter.annotations = [...annotations]; + filter.sort = sort; return filter; } @@ -169,6 +176,22 @@ export class Filter { } return true; } + + sortTests(tests: TestCaseSummary[]): TestCaseSummary[] { + if (!this.sort.length) + return tests; + + return tests.slice().sort((a, b) => { + for (const sortToken of this.sort) { + let comparison = 0; + if (sortToken.name === 'duration') + comparison = a.duration - b.duration; + if (comparison !== 0) + return sortToken.not ? -comparison : comparison; + } + return 0; + }); + } } type SearchValues = { diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index c21ded5c7d669..6972b878b26a5 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -207,7 +207,7 @@ function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats { function createFilesModel(report: LoadedReport | undefined, filter: Filter): TestModelSummary { const result: TestModelSummary = { files: [], tests: [] }; for (const file of report?.json().files || []) { - const tests = file.tests.filter(t => filter.matches(t)); + const tests = filter.sortTests(file.tests.filter(t => filter.matches(t))); if (tests.length) result.files.push({ ...file, tests }); result.tests.push(...tests); @@ -241,6 +241,9 @@ function createMergedFilesModel(report: LoadedReport | undefined, filter: Filter groups.sort((a, b) => a.fileName.localeCompare(b.fileName)); + for (const group of groups) + group.tests = filter.sortTests(group.tests); + const result: TestModelSummary = { files: groups, tests: [] }; for (const group of groups) result.tests.push(...group.tests); diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 8e53892c1525c..cd98905772120 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3247,6 +3247,39 @@ test('should support merge files option', async ({ runInlineTest, showReport, pa `); }); +test('should support sorting by duration', 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, 10)); + }); + test('slow test', async ({}) => { + await new Promise(resolve => setTimeout(resolve, 100)); + }); + test('medium test', async ({}) => { + await new Promise(resolve => setTimeout(resolve, 50)); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + + await showReport(); + + const searchInput = page.locator('.subnav-search-input'); + const testTitles = page.locator('.test-file-test .test-file-title'); + + await expect(testTitles).toHaveText(['fast test', 'slow test', 'medium test']); + + await searchInput.fill('o:duration'); + await expect(testTitles).toHaveText(['fast test', 'medium test', 'slow test']); + + await searchInput.fill('!o:duration'); + await expect(testTitles).toHaveText(['slow test', 'medium test', 'fast test']); + + await searchInput.clear(); + await expect(testTitles).toHaveText(['fast test', 'slow test', 'medium test']); +}); + function readAllFromStream(stream: NodeJS.ReadableStream): Promise { return new Promise(resolve => { const chunks: Buffer[] = []; From 1f31366f3bb13d766a9a2cf6d198b952aeb625e5 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:10:02 +0200 Subject: [PATCH 02/17] refactor(html-reporter): simplify Filter.parse() method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unnecessary Sets and spread operations - Push directly onto filter arrays - Move tokenize call into for loop header - Replace continue statements with if/else chain 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/html-reporter/src/filter.ts | 50 ++++++++-------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 62fbac2ec401c..b39361c65bae1 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -37,48 +37,26 @@ export class Filter { } static parse(expression: string): Filter { - const tokens = Filter.tokenize(expression); - const project = new Set(); - const status = new Set(); - const text: FilterToken[] = []; - const labels = new Set(); - const annotations = new Set(); - const sort: 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('o:')) { - sort.push({ name: token.slice(2), not }); - continue; - } - 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]; - filter.sort = sort; return filter; } From aa677ded8b0a78d085929f9b21d5c804ea4b0237 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:12:00 +0200 Subject: [PATCH 03/17] refactor(html-reporter): make sortTests mutate in place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change sortTests to return void and mutate the array in place instead of creating a copy. This is more efficient and clearer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/html-reporter/src/filter.ts | 6 +++--- packages/html-reporter/src/reportView.tsx | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index b39361c65bae1..6ac3dd37ee085 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -155,11 +155,11 @@ export class Filter { return true; } - sortTests(tests: TestCaseSummary[]): TestCaseSummary[] { + sortTests(tests: TestCaseSummary[]): void { if (!this.sort.length) - return tests; + return; - return tests.slice().sort((a, b) => { + tests.sort((a, b) => { for (const sortToken of this.sort) { let comparison = 0; if (sortToken.name === 'duration') diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index 6972b878b26a5..5d8ecd87a1de0 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -207,7 +207,8 @@ function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats { function createFilesModel(report: LoadedReport | undefined, filter: Filter): TestModelSummary { const result: TestModelSummary = { files: [], tests: [] }; for (const file of report?.json().files || []) { - const tests = filter.sortTests(file.tests.filter(t => filter.matches(t))); + const tests = file.tests.filter(t => filter.matches(t)); + filter.sortTests(tests); if (tests.length) result.files.push({ ...file, tests }); result.tests.push(...tests); @@ -242,7 +243,7 @@ function createMergedFilesModel(report: LoadedReport | undefined, filter: Filter groups.sort((a, b) => a.fileName.localeCompare(b.fileName)); for (const group of groups) - group.tests = filter.sortTests(group.tests); + filter.sortTests(group.tests); const result: TestModelSummary = { files: groups, tests: [] }; for (const group of groups) From dba8f75b433e827324e9aac43374479b5c9139f9 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:15:10 +0200 Subject: [PATCH 04/17] test(html-reporter): add cross-file sorting test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update sorting test to include multiple files to verify that sorting works across all tests globally, not just within each file. This test currently fails, exposing the bug that sorting is only applied per-file rather than across all tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/playwright-test/reporter-html.spec.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index cd98905772120..e41b47068f4df 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3261,6 +3261,15 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p await new Promise(resolve => setTimeout(resolve, 50)); }); `, + 'b.test.js': ` + import { test, expect } from '@playwright/test'; + test('very fast test', async ({}) => { + await new Promise(resolve => setTimeout(resolve, 5)); + }); + test('very slow test', async ({}) => { + await new Promise(resolve => setTimeout(resolve, 150)); + }); + `, }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); await showReport(); @@ -3268,16 +3277,16 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p const searchInput = page.locator('.subnav-search-input'); const testTitles = page.locator('.test-file-test .test-file-title'); - await expect(testTitles).toHaveText(['fast test', 'slow test', 'medium test']); + await expect(testTitles).toHaveText(['fast test', 'slow test', 'medium test', 'very fast test', 'very slow test']); await searchInput.fill('o:duration'); - await expect(testTitles).toHaveText(['fast test', 'medium test', 'slow test']); + await expect(testTitles).toHaveText(['very fast test', 'fast test', 'medium test', 'slow test', 'very slow test']); await searchInput.fill('!o:duration'); - await expect(testTitles).toHaveText(['slow test', 'medium test', 'fast test']); + await expect(testTitles).toHaveText(['very slow test', 'slow test', 'medium test', 'fast test', 'very fast test']); await searchInput.clear(); - await expect(testTitles).toHaveText(['fast test', 'slow test', 'medium test']); + await expect(testTitles).toHaveText(['fast test', 'slow test', 'medium test', 'very fast test', 'very slow test']); }); function readAllFromStream(stream: NodeJS.ReadableStream): Promise { From 3277f4e79b219d8429ce7bb9ad80783b131448af Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:18:11 +0200 Subject: [PATCH 05/17] fix(html-reporter): disable file grouping when sorting is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When sorting is active, group all tests into a single anonymous group to maintain the global sort order. File/describe grouping would break the sorting since tests would be displayed grouped by file. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/html-reporter/src/reportView.tsx | 29 +++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index 5d8ecd87a1de0..0d666e8e21cea 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -206,13 +206,25 @@ function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats { function createFilesModel(report: LoadedReport | undefined, filter: Filter): TestModelSummary { const result: TestModelSummary = { files: [], tests: [] }; + for (const file of report?.json().files || []) { const tests = file.tests.filter(t => filter.matches(t)); - filter.sortTests(tests); if (tests.length) result.files.push({ ...file, tests }); result.tests.push(...tests); } + + filter.sortTests(result.tests); + + if (filter.sort.length) { + result.files = [{ + fileId: '', + fileName: '', + tests: result.tests, + stats: { total: 0, expected: 0, unexpected: 0, flaky: 0, skipped: 0, ok: true } + }]; + } + return result; } @@ -242,11 +254,20 @@ function createMergedFilesModel(report: LoadedReport | undefined, filter: Filter groups.sort((a, b) => a.fileName.localeCompare(b.fileName)); - for (const group of groups) - filter.sortTests(group.tests); - const result: TestModelSummary = { files: groups, tests: [] }; for (const group of groups) result.tests.push(...group.tests); + + filter.sortTests(result.tests); + + if (filter.sort.length) { + result.files = [{ + fileId: '', + fileName: '', + tests: result.tests, + stats: { total: 0, expected: 0, unexpected: 0, flaky: 0, skipped: 0, ok: true } + }]; + } + return result; } From dc5c8fed61d3601f3c1516d4f4a75a1417010aec Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:22:21 +0200 Subject: [PATCH 06/17] test(html-reporter): use aria snapshots for sorting test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace toHaveText assertions with toMatchAriaSnapshot for more comprehensive testing that verifies the entire UI structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/playwright-test/reporter-html.spec.ts | 185 +++++++++++++++++++- 1 file changed, 180 insertions(+), 5 deletions(-) diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index e41b47068f4df..7f63c15ac33a9 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3275,18 +3275,193 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p await showReport(); const searchInput = page.locator('.subnav-search-input'); - const testTitles = page.locator('.test-file-test .test-file-title'); - await expect(testTitles).toHaveText(['fast test', 'slow test', 'medium test', 'very fast test', 'very slow test']); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - main: + - navigation: + - link "All5": + - /url: "#?" + - link "Passed5": + - /url: "#?q=s:passed" + - link "Failed0": + - /url: "#?q=s:failed" + - link "Flaky0": + - /url: "#?q=s:flaky" + - link "Skipped0": + - /url: "#?q=s:skipped" + - button "Settings" + - textbox + - text: "/\\\\d+\\\\/\\\\d+\\\\/\\\\d+, 1:\\\\d+:\\\\d+ PM Total time: \\\\d+[hmsp]+/" + - button "a.test.js" [expanded] + - region: + - link "fast test": + - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" + - text: /\\d+[hmsp]+/ + - link "a.test.js:3": + - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" + - link "slow test": + - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" + - text: /\\d+[hmsp]+/ + - link "a.test.js:6": + - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" + - link "medium test": + - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" + - text: /\\d+[hmsp]+/ + - link "a.test.js:9": + - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" + - button "b.test.js" [expanded] + - region: + - link "very fast test": + - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + - text: /\\d+[hmsp]+/ + - link "b.test.js:3": + - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + - link "very slow test": + - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + - text: /\\d+[hmsp]+/ + - link "b.test.js:6": + - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + `); await searchInput.fill('o:duration'); - await expect(testTitles).toHaveText(['very fast test', 'fast test', 'medium test', 'slow test', 'very slow test']); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - main: + - navigation: + - link "All5": + - /url: "#?" + - link "Passed5": + - /url: "#?q=s:passed" + - link "Failed0": + - /url: "#?q=s:failed" + - link "Flaky0": + - /url: "#?q=s:flaky" + - link "Skipped0": + - /url: "#?q=s:skipped" + - button "Settings" + - textbox: o:duration + - text: "/\\\\d+\\\\/\\\\d+\\\\/\\\\d+, 1:\\\\d+:\\\\d+ PM Total time: \\\\d+[hmsp]+/" + - button [expanded] + - region: + - link "very fast test": + - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + - text: /\\d+[hmsp]+/ + - link "b.test.js:3": + - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + - link "fast test": + - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" + - text: /\\d+[hmsp]+/ + - link "a.test.js:3": + - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" + - link "medium test": + - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" + - text: /\\d+[hmsp]+/ + - link "a.test.js:9": + - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" + - link "slow test": + - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" + - text: /\\d+[hmsp]+/ + - link "a.test.js:6": + - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" + - link "very slow test": + - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + - text: /\\d+[hmsp]+/ + - link "b.test.js:6": + - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + `); await searchInput.fill('!o:duration'); - await expect(testTitles).toHaveText(['very slow test', 'slow test', 'medium test', 'fast test', 'very fast test']); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - main: + - navigation: + - link "All5": + - /url: "#?" + - link "Passed5": + - /url: "#?q=s:passed" + - link "Failed0": + - /url: "#?q=s:failed" + - link "Flaky0": + - /url: "#?q=s:flaky" + - link "Skipped0": + - /url: "#?q=s:skipped" + - button "Settings" + - textbox: "!o:duration" + - text: "/\\\\d+\\\\/\\\\d+\\\\/\\\\d+, 1:\\\\d+:\\\\d+ PM Total time: \\\\d+[hmsp]+/" + - button [expanded] + - region: + - link "very slow test": + - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + - text: /\\d+[hmsp]+/ + - link "b.test.js:6": + - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + - link "slow test": + - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" + - text: /\\d+[hmsp]+/ + - link "a.test.js:6": + - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" + - link "medium test": + - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" + - text: /\\d+[hmsp]+/ + - link "a.test.js:9": + - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" + - link "fast test": + - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" + - text: /\\d+[hmsp]+/ + - link "a.test.js:3": + - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" + - link "very fast test": + - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + - text: /\\d+[hmsp]+/ + - link "b.test.js:3": + - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + `); await searchInput.clear(); - await expect(testTitles).toHaveText(['fast test', 'slow test', 'medium test', 'very fast test', 'very slow test']); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - main: + - navigation: + - link "All5": + - /url: "#?" + - link "Passed5": + - /url: "#?q=s:passed" + - link "Failed0": + - /url: "#?q=s:failed" + - link "Flaky0": + - /url: "#?q=s:flaky" + - link "Skipped0": + - /url: "#?q=s:skipped" + - button "Settings" + - textbox + - text: "/\\\\d+\\\\/\\\\d+\\\\/\\\\d+, 1:\\\\d+:\\\\d+ PM Total time: \\\\d+[hmsp]+/" + - button "a.test.js" [expanded] + - region: + - link "fast test": + - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" + - text: /\\d+[hmsp]+/ + - link "a.test.js:3": + - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" + - link "slow test": + - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" + - text: /\\d+[hmsp]+/ + - link "a.test.js:6": + - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" + - link "medium test": + - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" + - text: /\\d+[hmsp]+/ + - link "a.test.js:9": + - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" + - button "b.test.js" [expanded] + - region: + - link "very fast test": + - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + - text: /\\d+[hmsp]+/ + - link "b.test.js:3": + - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + - link "very slow test": + - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + - text: /\\d+[hmsp]+/ + - link "b.test.js:6": + - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + `); }); function readAllFromStream(stream: NodeJS.ReadableStream): Promise { From 0feb87dfb374b5190899f722279976b768072d0d Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:24:02 +0200 Subject: [PATCH 07/17] refactor(test): trim aria snapshots to essential elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce snapshot verbosity by only checking buttons, regions, and test links - the parts we actually care about for sorting verification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/playwright-test/reporter-html.spec.ts | 204 +++----------------- 1 file changed, 32 insertions(+), 172 deletions(-) diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 7f63c15ac33a9..8d6f89edc5df2 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3277,190 +3277,50 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p const searchInput = page.locator('.subnav-search-input'); await expect(page.locator('body')).toMatchAriaSnapshot(` - - main: - - navigation: - - link "All5": - - /url: "#?" - - link "Passed5": - - /url: "#?q=s:passed" - - link "Failed0": - - /url: "#?q=s:failed" - - link "Flaky0": - - /url: "#?q=s:flaky" - - link "Skipped0": - - /url: "#?q=s:skipped" - - button "Settings" - - textbox - - text: "/\\\\d+\\\\/\\\\d+\\\\/\\\\d+, 1:\\\\d+:\\\\d+ PM Total time: \\\\d+[hmsp]+/" - - button "a.test.js" [expanded] - - region: - - link "fast test": - - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" - - text: /\\d+[hmsp]+/ - - link "a.test.js:3": - - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" - - link "slow test": - - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" - - text: /\\d+[hmsp]+/ - - link "a.test.js:6": - - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" - - link "medium test": - - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" - - text: /\\d+[hmsp]+/ - - link "a.test.js:9": - - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" - - button "b.test.js" [expanded] - - region: - - link "very fast test": - - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" - - text: /\\d+[hmsp]+/ - - link "b.test.js:3": - - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" - - link "very slow test": - - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" - - text: /\\d+[hmsp]+/ - - link "b.test.js:6": - - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + - 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" `); await searchInput.fill('o:duration'); await expect(page.locator('body')).toMatchAriaSnapshot(` - - main: - - navigation: - - link "All5": - - /url: "#?" - - link "Passed5": - - /url: "#?q=s:passed" - - link "Failed0": - - /url: "#?q=s:failed" - - link "Flaky0": - - /url: "#?q=s:flaky" - - link "Skipped0": - - /url: "#?q=s:skipped" - - button "Settings" - - textbox: o:duration - - text: "/\\\\d+\\\\/\\\\d+\\\\/\\\\d+, 1:\\\\d+:\\\\d+ PM Total time: \\\\d+[hmsp]+/" - - button [expanded] - - region: - - link "very fast test": - - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" - - text: /\\d+[hmsp]+/ - - link "b.test.js:3": - - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" - - link "fast test": - - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" - - text: /\\d+[hmsp]+/ - - link "a.test.js:3": - - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" - - link "medium test": - - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" - - text: /\\d+[hmsp]+/ - - link "a.test.js:9": - - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" - - link "slow test": - - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" - - text: /\\d+[hmsp]+/ - - link "a.test.js:6": - - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" - - link "very slow test": - - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" - - text: /\\d+[hmsp]+/ - - link "b.test.js:6": - - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + - button [expanded] + - region: + - link "very fast test" + - link "fast test" + - link "medium test" + - link "slow test" + - link "very slow test" `); await searchInput.fill('!o:duration'); await expect(page.locator('body')).toMatchAriaSnapshot(` - - main: - - navigation: - - link "All5": - - /url: "#?" - - link "Passed5": - - /url: "#?q=s:passed" - - link "Failed0": - - /url: "#?q=s:failed" - - link "Flaky0": - - /url: "#?q=s:flaky" - - link "Skipped0": - - /url: "#?q=s:skipped" - - button "Settings" - - textbox: "!o:duration" - - text: "/\\\\d+\\\\/\\\\d+\\\\/\\\\d+, 1:\\\\d+:\\\\d+ PM Total time: \\\\d+[hmsp]+/" - - button [expanded] - - region: - - link "very slow test": - - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" - - text: /\\d+[hmsp]+/ - - link "b.test.js:6": - - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" - - link "slow test": - - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" - - text: /\\d+[hmsp]+/ - - link "a.test.js:6": - - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" - - link "medium test": - - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" - - text: /\\d+[hmsp]+/ - - link "a.test.js:9": - - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" - - link "fast test": - - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" - - text: /\\d+[hmsp]+/ - - link "a.test.js:3": - - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" - - link "very fast test": - - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" - - text: /\\d+[hmsp]+/ - - link "b.test.js:3": - - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" + - button [expanded] + - region: + - link "very slow test" + - link "slow test" + - link "medium test" + - link "fast test" + - link "very fast test" `); await searchInput.clear(); await expect(page.locator('body')).toMatchAriaSnapshot(` - - main: - - navigation: - - link "All5": - - /url: "#?" - - link "Passed5": - - /url: "#?q=s:passed" - - link "Failed0": - - /url: "#?q=s:failed" - - link "Flaky0": - - /url: "#?q=s:flaky" - - link "Skipped0": - - /url: "#?q=s:skipped" - - button "Settings" - - textbox - - text: "/\\\\d+\\\\/\\\\d+\\\\/\\\\d+, 1:\\\\d+:\\\\d+ PM Total time: \\\\d+[hmsp]+/" - - button "a.test.js" [expanded] - - region: - - link "fast test": - - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" - - text: /\\d+[hmsp]+/ - - link "a.test.js:3": - - /url: "#?testId=17c2af56efedce05daab-64b07a38d632a2a9987c" - - link "slow test": - - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" - - text: /\\d+[hmsp]+/ - - link "a.test.js:6": - - /url: "#?testId=17c2af56efedce05daab-3bd304b2dc4dd6f5e519" - - link "medium test": - - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" - - text: /\\d+[hmsp]+/ - - link "a.test.js:9": - - /url: "#?testId=17c2af56efedce05daab-bd277126f3467d5bae5d" - - button "b.test.js" [expanded] - - region: - - link "very fast test": - - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" - - text: /\\d+[hmsp]+/ - - link "b.test.js:3": - - /url: "#?testId=970b9ee1a59d47a8ca1c-3b2a25f05f406137f27f" - - link "very slow test": - - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" - - text: /\\d+[hmsp]+/ - - link "b.test.js:6": - - /url: "#?testId=970b9ee1a59d47a8ca1c-5471a3464ee1e078930b" + - 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" `); }); From d29bb82c70434ca9a5615dbed44f414b90932185 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:26:20 +0200 Subject: [PATCH 08/17] refactor(test): use getByRole('main') for narrower scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace page.locator('body') with page.getByRole('main') to more precisely target the test content area. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/playwright-test/reporter-html.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 8d6f89edc5df2..0b8a013fec831 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3276,7 +3276,7 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p const searchInput = page.locator('.subnav-search-input'); - await expect(page.locator('body')).toMatchAriaSnapshot(` + await expect(page.getByRole('main')).toMatchAriaSnapshot(` - button "a.test.js" [expanded] - region: - link "fast test" @@ -3289,7 +3289,7 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p `); await searchInput.fill('o:duration'); - await expect(page.locator('body')).toMatchAriaSnapshot(` + await expect(page.getByRole('main')).toMatchAriaSnapshot(` - button [expanded] - region: - link "very fast test" @@ -3300,7 +3300,7 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p `); await searchInput.fill('!o:duration'); - await expect(page.locator('body')).toMatchAriaSnapshot(` + await expect(page.getByRole('main')).toMatchAriaSnapshot(` - button [expanded] - region: - link "very slow test" @@ -3311,7 +3311,7 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p `); await searchInput.clear(); - await expect(page.locator('body')).toMatchAriaSnapshot(` + await expect(page.getByRole('main')).toMatchAriaSnapshot(` - button "a.test.js" [expanded] - region: - link "fast test" From 709eaeb00e754b9bfac8277d57b9e169c2543051 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:38:01 +0200 Subject: [PATCH 09/17] flip ordering --- packages/html-reporter/src/filter.ts | 2 +- tests/playwright-test/reporter-html.spec.ts | 27 +++++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 6ac3dd37ee085..71cddd7fa0c2b 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -163,7 +163,7 @@ export class Filter { for (const sortToken of this.sort) { let comparison = 0; if (sortToken.name === 'duration') - comparison = a.duration - b.duration; + comparison = b.duration - a.duration; if (comparison !== 0) return sortToken.not ? -comparison : comparison; } diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 0b8a013fec831..6e1de7d712878 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3269,6 +3269,10 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p test('very slow test', async ({}) => { await new Promise(resolve => setTimeout(resolve, 150)); }); + test('known slow', async ({}) => { + test.slow(); + await new Promise(resolve => setTimeout(resolve, 75)); + }); `, }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); @@ -3286,28 +3290,34 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p - 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 fast test" - - link "fast test" - - link "medium test" - - link "slow test" - 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 !@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 slow test" - - link "slow test" - - link "medium test" - - link "fast test" - link "very fast test" + - link "fast test" + - link "medium test" + - link "known slow" + - link "slow test" + - link "very slow test" `); await searchInput.clear(); @@ -3321,6 +3331,7 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p - region: - link "very fast test" - link "very slow test" + - link "known slow" `); }); From 737a17574df94e7af9b3d72491844c36cebb84a5 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:39:32 +0200 Subject: [PATCH 10/17] annot, not tag --- tests/playwright-test/reporter-html.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 6e1de7d712878..f351f9772b486 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3305,7 +3305,7 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p - link "very fast test" `); - await searchInput.fill('o:duration !@slow'); + await searchInput.fill('o:duration !annot:slow'); await expect(page.getByRole('link', { name: 'known slow' })).not.toBeVisible(); await searchInput.fill('!o:duration'); From f7b68f21f5bd7dce0d2bef0883dabf7e0d3c127e Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 13:42:49 +0200 Subject: [PATCH 11/17] move sort into clause --- packages/html-reporter/src/reportView.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index 0d666e8e21cea..2a20e6024a3b8 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -214,9 +214,8 @@ function createFilesModel(report: LoadedReport | undefined, filter: Filter): Tes result.tests.push(...tests); } - filter.sortTests(result.tests); - if (filter.sort.length) { + filter.sortTests(result.tests); result.files = [{ fileId: '', fileName: '', @@ -258,9 +257,8 @@ function createMergedFilesModel(report: LoadedReport | undefined, filter: Filter for (const group of groups) result.tests.push(...group.tests); - filter.sortTests(result.tests); - if (filter.sort.length) { + filter.sortTests(result.tests); result.files = [{ fileId: '', fileName: '', From 1e43b8c2eb3625e31bf97177cdae982f6c9904fd Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 15:17:33 +0200 Subject: [PATCH 12/17] feat(html-reporter): add sorting by duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for sorting tests by duration in the HTML reporter using the query syntax: - `o:duration` - sort by duration, slowest tests first - `!o:duration` - sort by duration, fastest tests first When sorting is active, file grouping is disabled and all tests are displayed in a single list sorted according to the specified criteria. The implementation supports multiple sort criteria for priority sorting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/html-reporter/src/reportView.tsx | 39 +++++++++++------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index 2a20e6024a3b8..9561b5ff86151 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -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(() => { @@ -206,24 +210,12 @@ function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats { function createFilesModel(report: LoadedReport | undefined, filter: Filter): TestModelSummary { const result: TestModelSummary = { files: [], tests: [] }; - for (const file of report?.json().files || []) { const tests = file.tests.filter(t => filter.matches(t)); if (tests.length) result.files.push({ ...file, tests }); result.tests.push(...tests); } - - if (filter.sort.length) { - filter.sortTests(result.tests); - result.files = [{ - fileId: '', - fileName: '', - tests: result.tests, - stats: { total: 0, expected: 0, unexpected: 0, flaky: 0, skipped: 0, ok: true } - }]; - } - return result; } @@ -256,16 +248,21 @@ function createMergedFilesModel(report: LoadedReport | undefined, filter: Filter const result: TestModelSummary = { files: groups, tests: [] }; for (const group of groups) result.tests.push(...group.tests); + return result; +} - if (filter.sort.length) { - filter.sortTests(result.tests); - result.files = [{ +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); + + return { + files: [{ fileId: '', fileName: '', - tests: result.tests, + tests, stats: { total: 0, expected: 0, unexpected: 0, flaky: 0, skipped: 0, ok: true } - }]; - } - - return result; + }], + tests + }; } From 44bd4869def2bb9f485f7d9c0aaa9390ad15c2b4 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 15:28:17 +0200 Subject: [PATCH 13/17] feat(html-reporter): add support for additional sort fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend sorting to support multiple fields beyond duration: - `o:title` - sort by test title alphabetically - `o:status` - sort by test status (expected/failed/flaky/skipped) - `o:file` - sort by file path - `o:line` - sort by line number - `o:project` - sort by project name All fields support the `!` prefix for reversed sorting. Multiple sort criteria can be combined for priority sorting (e.g., `o:file o:line`). Reuses `cacheSearchValues` for status, file, and project fields to improve performance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/html-reporter/src/filter.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 71cddd7fa0c2b..fc0b53e25050e 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -161,15 +161,25 @@ export class Filter { tests.sort((a, b) => { for (const sortToken of this.sort) { - let comparison = 0; - if (sortToken.name === 'duration') - comparison = b.duration - a.duration; + const comparison = this._compareTests(a, b, sortToken.name); if (comparison !== 0) return sortToken.not ? -comparison : comparison; } return 0; }); } + + 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') + return cacheSearchValues(a)[field].localeCompare(cacheSearchValues(b)[field]); + return 0; + } } type SearchValues = { From f487e6f1607a0920b1576ed24da2cc1ef5a542e9 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 15:34:18 +0200 Subject: [PATCH 14/17] add default sorting --- packages/html-reporter/src/filter.ts | 6 ++++-- tests/playwright-test/reporter-html.spec.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index fc0b53e25050e..1caa46e896219 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -176,9 +176,11 @@ export class Filter { return a.title.localeCompare(b.title); if (field === 'line') return a.location.line - b.location.line; + const searchValuesA = cacheSearchValues(a); + const searchValuesB = cacheSearchValues(b); if (field === 'status' || field === 'file' || field === 'project') - return cacheSearchValues(a)[field].localeCompare(cacheSearchValues(b)[field]); - return 0; + return searchValuesA[field].localeCompare(searchValuesB[field]); + return searchValuesA.text.localeCompare(searchValuesB.text); } } diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index f351f9772b486..cc53d40950c7d 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3252,26 +3252,26 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p 'a.test.js': ` import { test, expect } from '@playwright/test'; test('fast test', async ({}) => { - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise(resolve => setTimeout(resolve, 200)); }); test('slow test', async ({}) => { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 500)); }); test('medium test', async ({}) => { - await new Promise(resolve => setTimeout(resolve, 50)); + 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, 5)); + await new Promise(resolve => setTimeout(resolve, 100)); }); test('very slow test', async ({}) => { - await new Promise(resolve => setTimeout(resolve, 150)); + await new Promise(resolve => setTimeout(resolve, 600)); }); test('known slow', async ({}) => { test.slow(); - await new Promise(resolve => setTimeout(resolve, 75)); + await new Promise(resolve => setTimeout(resolve, 400)); }); `, }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); From 7febe1e27d6388096dbd9bae058feefb31758245 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 15:51:28 +0200 Subject: [PATCH 15/17] test(html-reporter): extend sorting test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename test to "should support sorting" to reflect broader coverage - Add tests for o:title (alphabetical sorting) - Add tests for o:file (file path sorting) - Increase timeout values to multiples of 100ms to prevent flakiness 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/playwright-test/reporter-html.spec.ts | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index cc53d40950c7d..1881d64cd13ce 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3247,7 +3247,7 @@ test('should support merge files option', async ({ runInlineTest, showReport, pa `); }); -test('should support sorting by duration', async ({ runInlineTest, showReport, page }) => { +test('should support sorting', async ({ runInlineTest, showReport, page }) => { await runInlineTest({ 'a.test.js': ` import { test, expect } from '@playwright/test'; @@ -3320,6 +3320,30 @@ test('should support sorting by duration', async ({ runInlineTest, showReport, p - 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] From 4f6b6e99856fbb65e04ba857dcb0f27439d9b908 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 15:54:14 +0200 Subject: [PATCH 16/17] ensure text is only used as last resort --- packages/html-reporter/src/filter.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 1caa46e896219..1bee2c63f1c09 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -165,7 +165,7 @@ export class Filter { if (comparison !== 0) return sortToken.not ? -comparison : comparison; } - return 0; + return this._compareTests(a, b, 'file'); }); } @@ -176,11 +176,9 @@ export class Filter { return a.title.localeCompare(b.title); if (field === 'line') return a.location.line - b.location.line; - const searchValuesA = cacheSearchValues(a); - const searchValuesB = cacheSearchValues(b); - if (field === 'status' || field === 'file' || field === 'project') - return searchValuesA[field].localeCompare(searchValuesB[field]); - return searchValuesA.text.localeCompare(searchValuesB.text); + if (field !== 'status' && field !== 'file' && field !== 'project' && field !== 'text') + return 0; + return cacheSearchValues(a)[field].localeCompare(cacheSearchValues(b)[field]); } } From 0b5e2592d6a44eff588dab1919a846a21a25eab2 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 16 Oct 2025 16:23:17 +0200 Subject: [PATCH 17/17] chunk by 100 --- packages/html-reporter/src/reportView.tsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index 9561b5ff86151..7e0e051beed96 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -256,13 +256,21 @@ function createSortedNoFilesModel(report: LoadedReport | undefined, filter: Filt tests = tests.filter(t => filter.matches(t)); filter.sortTests(tests); - return { - files: [{ - fileId: '', - fileName: '', - 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 }; }