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': `