feat(html): render prev/next test buttons (#33356)
This commit is contained in:
parent
9ce401d44a
commit
6f5c7b4358
|
@ -20,7 +20,7 @@ import './colors.css';
|
|||
import './common.css';
|
||||
import './headerView.css';
|
||||
import * as icons from './icons';
|
||||
import { Link, navigate } from './links';
|
||||
import { Link, navigate, SearchParamsContext } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import { filterWithToken } from './filter';
|
||||
|
||||
|
@ -65,7 +65,7 @@ export const HeaderView: React.FC<React.PropsWithChildren<{
|
|||
const StatsNavView: React.FC<{
|
||||
stats: Stats
|
||||
}> = ({ stats }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const q = searchParams.get('q')?.toString() || '';
|
||||
const tokens = q.split(' ');
|
||||
return <nav>
|
||||
|
|
|
@ -27,6 +27,7 @@ import { ReportView } from './reportView';
|
|||
const zipjs = zipImport as typeof zip;
|
||||
|
||||
import logo from '@web/assets/playwright-logo.svg';
|
||||
import { SearchParamsProvider } from './links';
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = logo;
|
||||
|
@ -40,7 +41,9 @@ const ReportLoader: React.FC = () => {
|
|||
const zipReport = new ZipReport();
|
||||
zipReport.load().then(() => setReport(zipReport));
|
||||
}, [report]);
|
||||
return <ReportView report={report}></ReportView>;
|
||||
return <SearchParamsProvider>
|
||||
<ReportView report={report} />
|
||||
</SearchParamsProvider>;
|
||||
};
|
||||
|
||||
window.onload = () => {
|
||||
|
|
|
@ -33,13 +33,8 @@ export const Route: React.FunctionComponent<{
|
|||
predicate: (params: URLSearchParams) => boolean,
|
||||
children: any
|
||||
}> = ({ predicate, children }) => {
|
||||
const [matches, setMatches] = React.useState(predicate(new URLSearchParams(window.location.hash.slice(1))));
|
||||
React.useEffect(() => {
|
||||
const listener = () => setMatches(predicate(new URLSearchParams(window.location.hash.slice(1))));
|
||||
window.addEventListener('popstate', listener);
|
||||
return () => window.removeEventListener('popstate', listener);
|
||||
}, [predicate]);
|
||||
return matches ? children : null;
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
return predicate(searchParams) ? children : null;
|
||||
};
|
||||
|
||||
export const Link: React.FunctionComponent<{
|
||||
|
@ -90,6 +85,20 @@ export const AttachmentLink: React.FunctionComponent<{
|
|||
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
|
||||
};
|
||||
|
||||
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||
|
||||
export const SearchParamsProvider: React.FunctionComponent<React.PropsWithChildren> = ({ children }) => {
|
||||
const [searchParams, setSearchParams] = React.useState<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => setSearchParams(new URLSearchParams(window.location.hash.slice(1)));
|
||||
window.addEventListener('popstate', listener);
|
||||
return () => window.removeEventListener('popstate', listener);
|
||||
}, []);
|
||||
|
||||
return <SearchParamsContext.Provider value={searchParams}>{children}</SearchParamsContext.Provider>;
|
||||
};
|
||||
|
||||
function downloadFileNameForAttachment(attachment: TestAttachment): string {
|
||||
if (attachment.name.includes('.') || !attachment.path)
|
||||
return attachment.name;
|
||||
|
|
|
@ -14,19 +14,19 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FilteredStats, TestCase, TestFile, TestFileSummary } from './types';
|
||||
import type { FilteredStats, TestCase, TestCaseSummary, TestFile, TestFileSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import './colors.css';
|
||||
import './common.css';
|
||||
import { Filter } from './filter';
|
||||
import { HeaderView } from './headerView';
|
||||
import { Route } from './links';
|
||||
import { Route, SearchParamsContext } from './links';
|
||||
import type { LoadedReport } from './loadedReport';
|
||||
import './reportView.css';
|
||||
import type { Metainfo } from './metadataView';
|
||||
import { MetadataView } from './metadataView';
|
||||
import { TestCaseView } from './testCaseView';
|
||||
import { TestFilesView } from './testFilesView';
|
||||
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||
import './theme.css';
|
||||
|
||||
declare global {
|
||||
|
@ -39,32 +39,55 @@ declare global {
|
|||
const testFilesRoutePredicate = (params: URLSearchParams) => !params.has('testId');
|
||||
const testCaseRoutePredicate = (params: URLSearchParams) => params.has('testId');
|
||||
|
||||
type TestModelSummary = {
|
||||
files: TestFileSummary[];
|
||||
tests: TestCaseSummary[];
|
||||
};
|
||||
|
||||
export const ReportView: React.FC<{
|
||||
report: LoadedReport | undefined,
|
||||
}> = ({ report }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
||||
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
|
||||
|
||||
const testIdToFileIdMap = React.useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const file of report?.json().files || []) {
|
||||
for (const test of file.tests)
|
||||
map.set(test.testId, file.fileId);
|
||||
}
|
||||
return map;
|
||||
}, [report]);
|
||||
|
||||
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
|
||||
const filteredStats = React.useMemo(() => computeStats(report?.json().files || [], filter), [report, filter]);
|
||||
const filteredStats = React.useMemo(() => filter.empty() ? undefined : computeStats(report?.json().files || [], filter), [report, filter]);
|
||||
const filteredTests = React.useMemo(() => {
|
||||
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);
|
||||
}
|
||||
return result;
|
||||
}, [report, filter]);
|
||||
|
||||
return <div className='htmlreport vbox px-4 pb-4'>
|
||||
<main>
|
||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||
{report?.json().metadata && <MetadataView {...report?.json().metadata as Metainfo} />}
|
||||
<Route predicate={testFilesRoutePredicate}>
|
||||
<TestFilesHeader report={report?.json()} filteredStats={filteredStats} />
|
||||
<TestFilesView
|
||||
report={report?.json()}
|
||||
filter={filter}
|
||||
tests={filteredTests.files}
|
||||
expandedFiles={expandedFiles}
|
||||
setExpandedFiles={setExpandedFiles}
|
||||
projectNames={report?.json().projectNames || []}
|
||||
filteredStats={filteredStats}
|
||||
/>
|
||||
</Route>
|
||||
<Route predicate={testCaseRoutePredicate}>
|
||||
{!!report && <TestCaseViewLoader report={report}></TestCaseViewLoader>}
|
||||
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
||||
</Route>
|
||||
</main>
|
||||
</div>;
|
||||
|
@ -72,21 +95,21 @@ export const ReportView: React.FC<{
|
|||
|
||||
const TestCaseViewLoader: React.FC<{
|
||||
report: LoadedReport,
|
||||
}> = ({ report }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
tests: TestCaseSummary[],
|
||||
testIdToFileIdMap: Map<string, string>,
|
||||
}> = ({ report, testIdToFileIdMap, tests }) => {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const [test, setTest] = React.useState<TestCase | undefined>();
|
||||
const testId = searchParams.get('testId');
|
||||
const anchor = (searchParams.get('anchor') || '') as 'video' | 'diff' | '';
|
||||
const run = +(searchParams.get('run') || '0');
|
||||
|
||||
const testIdToFileIdMap = React.useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const file of report.json().files) {
|
||||
for (const test of file.tests)
|
||||
map.set(test.testId, file.fileId);
|
||||
}
|
||||
return map;
|
||||
}, [report]);
|
||||
const { prev, next } = React.useMemo(() => {
|
||||
const index = tests.findIndex(t => t.testId === testId);
|
||||
const prev = index > 0 ? tests[index - 1] : undefined;
|
||||
const next = index < tests.length - 1 ? tests[index + 1] : undefined;
|
||||
return { prev, next };
|
||||
}, [testId, tests]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
|
@ -104,7 +127,15 @@ const TestCaseViewLoader: React.FC<{
|
|||
}
|
||||
})();
|
||||
}, [test, report, testId, testIdToFileIdMap]);
|
||||
return <TestCaseView projectNames={report.json().projectNames} test={test} anchor={anchor} run={run}></TestCaseView>;
|
||||
|
||||
return <TestCaseView
|
||||
projectNames={report.json().projectNames}
|
||||
next={next}
|
||||
prev={prev}
|
||||
test={test}
|
||||
anchor={anchor}
|
||||
run={run}
|
||||
/>;
|
||||
};
|
||||
|
||||
function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats {
|
||||
|
@ -119,4 +150,4 @@ function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats {
|
|||
stats.duration += test.duration;
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
.test-case-column {
|
||||
border-radius: 6px;
|
||||
margin: 24px 0;
|
||||
margin: 12px 0 24px 0;
|
||||
}
|
||||
|
||||
.test-case-column .tab-element.selected {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { TestCaseView } from './testCaseView';
|
||||
import type { TestCase, TestResult } from './types';
|
||||
import type { TestCase, TestCaseSummary, TestResult } from './types';
|
||||
|
||||
test.use({ viewport: { width: 800, height: 600 } });
|
||||
|
||||
|
@ -63,7 +63,7 @@ const testCase: TestCase = {
|
|||
};
|
||||
|
||||
test('should render test case', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>);
|
||||
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
|
||||
await expect(component.getByText('Hidden annotation')).toBeHidden();
|
||||
await component.getByText('Annotations').click();
|
||||
|
@ -79,7 +79,7 @@ test('should render test case', async ({ mount }) => {
|
|||
test('should render copy buttons for annotations', async ({ mount, page, context }) => {
|
||||
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>);
|
||||
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
|
||||
await component.getByText('Annotation text', { exact: false }).first().hover();
|
||||
await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible();
|
||||
|
@ -108,7 +108,7 @@ const annotationLinkRenderingTestCase: TestCase = {
|
|||
};
|
||||
|
||||
test('should correctly render links in annotations', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={annotationLinkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={annotationLinkRenderingTestCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>);
|
||||
|
||||
const firstLink = await component.getByText('https://playwright.dev/docs/intro').first();
|
||||
await expect(firstLink).toBeVisible();
|
||||
|
@ -165,8 +165,23 @@ const attachmentLinkRenderingTestCase: TestCase = {
|
|||
results: [resultWithAttachment]
|
||||
};
|
||||
|
||||
const testCaseSummary: TestCaseSummary = {
|
||||
testId: 'nextTestId',
|
||||
title: 'next test',
|
||||
path: [],
|
||||
projectName: 'chromium',
|
||||
location: { file: 'test.spec.ts', line: 42, column: 0 },
|
||||
tags: [],
|
||||
outcome: 'expected',
|
||||
duration: 10,
|
||||
ok: true,
|
||||
annotations: [],
|
||||
results: [resultWithAttachment]
|
||||
};
|
||||
|
||||
|
||||
test('should correctly render links in attachments', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>);
|
||||
await component.getByText('first attachment').click();
|
||||
const body = await component.getByText('The body with https://playwright.dev/docs/intro link');
|
||||
await expect(body).toBeVisible();
|
||||
|
@ -175,8 +190,17 @@ test('should correctly render links in attachments', async ({ mount }) => {
|
|||
});
|
||||
|
||||
test('should correctly render links in attachment name', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>);
|
||||
const link = component.getByText('attachment with inline link').locator('a');
|
||||
await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
|
||||
await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284');
|
||||
});
|
||||
|
||||
test('should correctly render prev and next', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={testCaseSummary} next={testCaseSummary} run={0} anchor=''></TestCaseView>);
|
||||
await expect(component).toMatchAriaSnapshot(`
|
||||
- link "« previous"
|
||||
- link "next »"
|
||||
- text: "My test test.spec.ts:42 10ms"
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -14,12 +14,12 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { TestCase, TestCaseAnnotation } from './types';
|
||||
import type { TestCase, TestCaseAnnotation, TestCaseSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import { TabbedPane } from './tabbedPane';
|
||||
import { AutoChip } from './chip';
|
||||
import './common.css';
|
||||
import { ProjectLink } from './links';
|
||||
import { Link, ProjectLink, SearchParamsContext } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import './testCaseView.css';
|
||||
import { TestResultView } from './testResultView';
|
||||
|
@ -31,10 +31,14 @@ import { CopyToClipboardContainer } from './copyToClipboard';
|
|||
export const TestCaseView: React.FC<{
|
||||
projectNames: string[],
|
||||
test: TestCase | undefined,
|
||||
next: TestCaseSummary | undefined,
|
||||
prev: TestCaseSummary | undefined,
|
||||
anchor: 'video' | 'diff' | '',
|
||||
run: number,
|
||||
}> = ({ projectNames, test, run, anchor }) => {
|
||||
}> = ({ projectNames, test, run, anchor, next, prev }) => {
|
||||
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
|
||||
|
||||
const labels = React.useMemo(() => {
|
||||
if (!test)
|
||||
|
@ -47,6 +51,11 @@ export const TestCaseView: React.FC<{
|
|||
}, [test?.annotations]);
|
||||
|
||||
return <div className='test-case-column vbox'>
|
||||
<div className='hbox'>
|
||||
{prev && <Link href={`#?testId=${prev.testId}${filterParam}`}>« previous</Link>}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
{next && <Link href={`#?testId=${next.testId}${filterParam}`}>next »</Link>}
|
||||
</div>
|
||||
{test && <div className='test-case-path'>{test.path.join(' › ')}</div>}
|
||||
{test && <div className='test-case-title'>{test?.title}</div>}
|
||||
{test && <div className='hbox'>
|
||||
|
|
|
@ -14,24 +14,25 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
|
||||
import type { TestCaseSummary, TestFileSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import { hashStringToInt, msToString } from './utils';
|
||||
import { Chip } from './chip';
|
||||
import { filterWithToken, type Filter } from './filter';
|
||||
import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
|
||||
import { filterWithToken } from './filter';
|
||||
import { generateTraceUrl, Link, navigate, ProjectLink, SearchParamsContext } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import './testFileView.css';
|
||||
import { video, image, trace } from './icons';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
|
||||
export const TestFileView: React.FC<React.PropsWithChildren<{
|
||||
report: HTMLReport;
|
||||
file: TestFileSummary;
|
||||
projectNames: string[];
|
||||
isFileExpanded: (fileId: string) => boolean;
|
||||
setFileExpanded: (fileId: string, expanded: boolean) => void;
|
||||
filter: Filter;
|
||||
}>> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => {
|
||||
}>> = ({ file, projectNames, isFileExpanded, setFileExpanded }) => {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
|
||||
return <Chip
|
||||
expanded={isFileExpanded(file.fileId)}
|
||||
noInsets={true}
|
||||
|
@ -39,7 +40,7 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
header={<span>
|
||||
{file.fileName}
|
||||
</span>}>
|
||||
{file.tests.filter(t => filter.matches(t)).map(test =>
|
||||
{file.tests.map(test =>
|
||||
<div key={`test-${test.testId}`} className={clsx('test-file-test', 'test-file-test-outcome-' + test.outcome)}>
|
||||
<div className='hbox' style={{ alignItems: 'flex-start' }}>
|
||||
<div className='hbox'>
|
||||
|
@ -47,11 +48,11 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
{statusIcon(test.outcome)}
|
||||
</span>
|
||||
<span>
|
||||
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' › ')}>
|
||||
<Link href={`#?testId=${test.testId}${filterParam}`} title={[...test.path, test.title].join(' › ')}>
|
||||
<span className='test-file-title'>{[...test.path, test.title].join(' › ')}</span>
|
||||
</Link>
|
||||
{report.projectNames.length > 1 && !!test.projectName &&
|
||||
<ProjectLink projectNames={report.projectNames} projectName={test.projectName} />}
|
||||
{projectNames.length > 1 && !!test.projectName &&
|
||||
<ProjectLink projectNames={projectNames} projectName={test.projectName} />}
|
||||
<LabelsClickView labels={test.tags} />
|
||||
</span>
|
||||
</div>
|
||||
|
@ -90,10 +91,10 @@ function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
|
|||
const LabelsClickView: React.FC<React.PropsWithChildren<{
|
||||
labels: string[],
|
||||
}>> = ({ labels }) => {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
|
||||
const onClickHandle = (e: React.MouseEvent, label: string) => {
|
||||
e.preventDefault();
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const q = searchParams.get('q')?.toString() || '';
|
||||
const tokens = q.split(' ');
|
||||
navigate(filterWithToken(tokens, label, e.metaKey || e.ctrlKey));
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
import type { FilteredStats, HTMLReport, TestFileSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import type { Filter } from './filter';
|
||||
import { TestFileView } from './testFileView';
|
||||
import './testFileView.css';
|
||||
import { msToString } from './utils';
|
||||
|
@ -24,40 +23,26 @@ import { AutoChip } from './chip';
|
|||
import { TestErrorView } from './testErrorView';
|
||||
|
||||
export const TestFilesView: React.FC<{
|
||||
report?: HTMLReport,
|
||||
tests: TestFileSummary[],
|
||||
expandedFiles: Map<string, boolean>,
|
||||
setExpandedFiles: (value: Map<string, boolean>) => void,
|
||||
filter: Filter,
|
||||
filteredStats: FilteredStats,
|
||||
projectNames: string[],
|
||||
}> = ({ report, filter, expandedFiles, setExpandedFiles, projectNames, filteredStats }) => {
|
||||
}> = ({ tests, expandedFiles, setExpandedFiles, projectNames }) => {
|
||||
const filteredFiles = React.useMemo(() => {
|
||||
const result: { file: TestFileSummary, defaultExpanded: boolean }[] = [];
|
||||
let visibleTests = 0;
|
||||
for (const file of report?.files || []) {
|
||||
const tests = file.tests.filter(t => filter.matches(t));
|
||||
visibleTests += tests.length;
|
||||
if (tests.length)
|
||||
result.push({ file, defaultExpanded: visibleTests < 200 });
|
||||
for (const file of tests) {
|
||||
visibleTests += file.tests.length;
|
||||
result.push({ file, defaultExpanded: visibleTests < 200 });
|
||||
}
|
||||
return result;
|
||||
}, [report, filter]);
|
||||
}, [tests]);
|
||||
return <>
|
||||
<div className='mt-2 mx-1' 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)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report?.duration ?? 0)}</div>
|
||||
</div>
|
||||
{report && !!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||
</AutoChip>}
|
||||
{report && filteredFiles.map(({ file, defaultExpanded }) => {
|
||||
{filteredFiles.map(({ file, defaultExpanded }) => {
|
||||
return <TestFileView
|
||||
key={`file-${file.fileId}`}
|
||||
report={report}
|
||||
file={file}
|
||||
projectNames={projectNames}
|
||||
isFileExpanded={fileId => {
|
||||
const value = expandedFiles.get(fileId);
|
||||
if (value === undefined)
|
||||
|
@ -68,9 +53,28 @@ export const TestFilesView: React.FC<{
|
|||
const newExpanded = new Map(expandedFiles);
|
||||
newExpanded.set(fileId, expanded);
|
||||
setExpandedFiles(newExpanded);
|
||||
}}
|
||||
filter={filter}>
|
||||
}}>
|
||||
</TestFileView>;
|
||||
})}
|
||||
</>;
|
||||
};
|
||||
|
||||
export const TestFilesHeader: React.FC<{
|
||||
report: HTMLReport | undefined,
|
||||
filteredStats?: FilteredStats,
|
||||
}> = ({ report, filteredStats }) => {
|
||||
if (!report)
|
||||
return;
|
||||
return <>
|
||||
<div className='mt-2 mx-1' style={{ display: 'flex' }}>
|
||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}
|
||||
{filteredStats && <div data-testid='filtered-tests-count' style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
||||
</div>
|
||||
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||
</AutoChip>}
|
||||
</>;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue