feat(html): show number of filtered tests, update total time (#23743)

This commit is contained in:
Yury Semikhatsky 2023-06-16 09:22:57 -07:00 committed by GitHub
parent 0358f6c434
commit 11770156eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 7 deletions

View File

@ -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 <div className='htmlreport vbox px-4 pb-4'>
<main>
@ -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}
/>
</Route>
<Route predicate={testCaseRoutePredicate}>
@ -95,3 +96,17 @@ const TestCaseViewLoader: React.FC<{
}, [test, report, testId]);
return <TestCaseView projectNames={report.json().projectNames} test={test} anchor={anchor} run={run}></TestCaseView>;
};
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;
}

View File

@ -57,7 +57,7 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
<LabelsClickView labels={labels(test)} />
</span>
</div>
<span style={{ minWidth: '50px', textAlign: 'right' }}>{msToString(test.duration)}</span>
<span data-testid='test-duration' style={{ minWidth: '50px', textAlign: 'right' }}>{msToString(test.duration)}</span>
</div>
<div className='test-file-details-row'>
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' ')} className='test-file-path-link'>

View File

@ -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<string, boolean>,
setExpandedFiles: (value: Map<string, boolean>) => 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 <>
<div className='p-2' style={{ display: 'flex' }}>
{projectNames.length === 1 && !!projectNames[0] && <div data-testid="project-name" style={{ color: 'var(--color-fg-subtle)' }}>Project: {projectNames[0]}</div>}
{!filter.empty() && <div data-testid="filtered-tests-count" style={{ color: 'var(--color-fg-subtle)' }}>Filtered: {filteredStats.total}</div>}
<div style={{ flex: 'auto' }}></div>
<div data-testid="overall-duration" style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(stats.duration)}</div>
<div data-testid="overall-duration" style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(filteredStats.duration)}</div>
</div>
{report && filteredFiles.map(({ file, defaultExpanded }) => {
return <TestFileView

View File

@ -26,6 +26,11 @@ export type Stats = {
duration: number;
};
export type FilteredStats = {
total: number
duration: number,
};
export type Location = {
file: string;
line: number;

View File

@ -1316,6 +1316,12 @@ for (const useIntermediateMergeReport of [false, true] as const) {
await expect(regressionLabelButton).not.toBeVisible();
{
const testDuration = await page.getByTestId('test-duration').textContent();
const totalDuration = await page.getByTestId('overall-duration').textContent();
expect(totalDuration).toBe('Total time: ' + testDuration);
}
await searchInput.clear();
await expect(regressionLabelButton).toBeVisible();
@ -1330,6 +1336,12 @@ for (const useIntermediateMergeReport of [false, true] as const) {
await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0);
await expect(page.locator('.test-file-test .test-file-title')).toHaveText('Error Pages @regression passes');
{
const testDuration = await page.getByTestId('test-duration').textContent();
const totalDuration = await page.getByTestId('overall-duration').textContent();
expect(totalDuration).toBe('Total time: ' + testDuration);
}
await searchInput.clear();
const tagWithDash = page.locator('.test-file-test', { has: page.getByText('Error Pages @GCC-1508 passes', { exact: true }) }).locator('.label', { hasText: 'GCC-1508' });
@ -1437,6 +1449,70 @@ for (const useIntermediateMergeReport of [false, true] as const) {
await expect(page).not.toHaveURL(/@regression/);
});
test('filter should update stats', async ({ runInlineTest, showReport, page }) => {
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': `