diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx
index 1b25de38e0..390c12c1dd 100644
--- a/packages/html-reporter/src/reportView.tsx
+++ b/packages/html-reporter/src/reportView.tsx
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import type { TestCase, TestFile } from './types';
+import type { FilteredStats, TestCase, TestFile, TestFileSummary } from './types';
import * as React from 'react';
import './colors.css';
import './common.css';
@@ -47,6 +47,7 @@ export const ReportView: React.FC<{
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
+ const filteredStats = React.useMemo(() => computeStats(report?.json().files || [], filter), [report, filter]);
return
@@ -59,7 +60,7 @@ export const ReportView: React.FC<{
expandedFiles={expandedFiles}
setExpandedFiles={setExpandedFiles}
projectNames={report?.json().projectNames || []}
- stats={report?.json().stats || { duration: 0 }}
+ filteredStats={filteredStats}
/>
@@ -95,3 +96,17 @@ const TestCaseViewLoader: React.FC<{
}, [test, report, testId]);
return ;
};
+
+function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats {
+ const stats: FilteredStats = {
+ total: 0,
+ duration: 0,
+ };
+ for (const file of files) {
+ const tests = file.tests.filter(t => filter.matches(t));
+ stats.total += tests.length;
+ for (const test of tests)
+ stats.duration += test.duration;
+ }
+ return stats;
+}
\ No newline at end of file
diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx
index 25044c0132..1bc34c40cb 100644
--- a/packages/html-reporter/src/testFileView.tsx
+++ b/packages/html-reporter/src/testFileView.tsx
@@ -57,7 +57,7 @@ export const TestFileView: React.FC
- {msToString(test.duration)}
+ {msToString(test.duration)}
diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx
index 447b9d7601..f00d0e6ba0 100644
--- a/packages/html-reporter/src/testFilesView.tsx
+++ b/packages/html-reporter/src/testFilesView.tsx
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import type { HTMLReport, TestFileSummary } from './types';
+import type { FilteredStats, HTMLReport, TestFileSummary } from './types';
import * as React from 'react';
import type { Filter } from './filter';
import { TestFileView } from './testFileView';
@@ -26,9 +26,9 @@ export const TestFilesView: React.FC<{
expandedFiles: Map
,
setExpandedFiles: (value: Map) => void,
filter: Filter,
- stats: { duration: number },
+ filteredStats: FilteredStats,
projectNames: string[],
-}> = ({ report, filter, expandedFiles, setExpandedFiles, projectNames, stats }) => {
+}> = ({ report, filter, expandedFiles, setExpandedFiles, projectNames, filteredStats }) => {
const filteredFiles = React.useMemo(() => {
const result: { file: TestFileSummary, defaultExpanded: boolean }[] = [];
let visibleTests = 0;
@@ -43,8 +43,9 @@ export const TestFilesView: React.FC<{
return <>
{projectNames.length === 1 && !!projectNames[0] &&
Project: {projectNames[0]}
}
+ {!filter.empty() &&
Filtered: {filteredStats.total}
}
-
Total time: {msToString(stats.duration)}
+
Total time: {msToString(filteredStats.duration)}
{report && filteredFiles.map(({ file, defaultExpanded }) => {
return {
+ const result = await runInlineTest({
+ 'a.test.js': `
+ const { expect, test } = require('@playwright/test');
+ const names = ['one foo', 'two foo', 'three bar', 'four bar', 'five baz'];
+ names.forEach(name => {
+ test(name, async ({}) => {
+ expect(name).not.toContain('foo');
+ });
+ });
+ `,
+ 'b.test.js': `
+ const { expect, test } = require('@playwright/test');
+ const names = ['one foo', 'two foo', 'three bar', 'four bar', 'five baz'];
+ names.forEach(name => {
+ test(name, async ({}) => {
+ expect(name).not.toContain('one');
+ });
+ });
+ `,
+ }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' });
+
+ expect(result.exitCode).toBe(1);
+ expect(result.passed).toBe(7);
+ expect(result.failed).toBe(3);
+
+ await showReport();
+
+ async function checkTotalDuration() {
+ let total = 0;
+ for (const text of await page.getByTestId('test-duration').allTextContents()) {
+ expect(text).toMatch(/\d+ms$/);
+ total += parseInt(text.substring(0, text.length - 2), 10);
+ }
+ const totalDuration = await page.getByTestId('overall-duration').textContent();
+ expect(totalDuration).toBe(`Total time: ${total}ms`);
+ }
+
+ const searchInput = page.locator('.subnav-search-input');
+ await expect(page.getByTestId('filtered-tests-count')).not.toBeVisible();
+
+ await searchInput.fill('s:failed');
+
+ await expect(page.getByTestId('filtered-tests-count')).toHaveText('Filtered: 3');
+ await checkTotalDuration();
+ await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('10');
+ await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('7');
+ await expect(page.locator('.subnav-item:has-text("Failed") .counter')).toHaveText('3');
+ await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('0');
+ await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('0');
+
+ await searchInput.clear();
+ await expect(page.getByTestId('filtered-tests-count')).not.toBeVisible();
+
+ await searchInput.fill('foo');
+ await expect(page.getByTestId('filtered-tests-count')).toHaveText('Filtered: 4');
+ await checkTotalDuration();
+ await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('10');
+ await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('7');
+ await expect(page.locator('.subnav-item:has-text("Failed") .counter')).toHaveText('3');
+ await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('0');
+ await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('0');
+ });
+
test('labels whould be applied together with status filter', async ({ runInlineTest, showReport, page }) => {
const result = await runInlineTest({
'a.test.js': `