feat(html-reporter): add navigation to test details
This commit is contained in:
parent
b535139b32
commit
724725bf68
|
|
@ -35,6 +35,12 @@ export const rightArrow = () => {
|
|||
</svg>;
|
||||
};
|
||||
|
||||
export const leftArrow = () => {
|
||||
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-fg-muted' transform='rotate(180)'>
|
||||
<path fillRule='evenodd' d='M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z'></path>
|
||||
</svg>;
|
||||
};
|
||||
|
||||
export const warning = () => {
|
||||
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-text-warning'>
|
||||
<path fillRule='evenodd' d='M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z'></path>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { MetadataView } from './metadataView';
|
|||
import { TestCaseView } from './testCaseView';
|
||||
import { TestFilesView } from './testFilesView';
|
||||
import './theme.css';
|
||||
import { useSearchParams } from './use-search-params';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -42,29 +43,49 @@ const testCaseRoutePredicate = (params: URLSearchParams) => params.has('testId')
|
|||
export const ReportView: React.FC<{
|
||||
report: LoadedReport | undefined,
|
||||
}> = ({ report }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const searchParams = useSearchParams();
|
||||
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
||||
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
|
||||
|
||||
const reportData = report?.json();
|
||||
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
|
||||
const filteredStats = React.useMemo(() => computeStats(report?.json().files || [], filter), [report, filter]);
|
||||
|
||||
const filteredFiles = React.useMemo(() => {
|
||||
const files = reportData?.files ?? [];
|
||||
|
||||
return files
|
||||
.map(file => {
|
||||
const tests = file.tests.filter(test => filter.matches(test));
|
||||
return { ...file, tests };
|
||||
})
|
||||
.filter(file => file.tests.length !== 0);
|
||||
}, [filter, reportData?.files]);
|
||||
|
||||
const filteredStats = React.useMemo(() => computeStats(filteredFiles), [filteredFiles]);
|
||||
|
||||
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} />}
|
||||
{reportData && <HeaderView stats={reportData.stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||
{reportData?.metadata && <MetadataView {...reportData.metadata as Metainfo} />}
|
||||
<Route predicate={testFilesRoutePredicate}>
|
||||
<TestFilesView
|
||||
report={report?.json()}
|
||||
report={reportData}
|
||||
filter={filter}
|
||||
expandedFiles={expandedFiles}
|
||||
setExpandedFiles={setExpandedFiles}
|
||||
projectNames={report?.json().projectNames || []}
|
||||
projectNames={reportData?.projectNames ?? []}
|
||||
filteredStats={filteredStats}
|
||||
filteredFiles={filteredFiles}
|
||||
/>
|
||||
</Route>
|
||||
<Route predicate={testCaseRoutePredicate}>
|
||||
{!!report && <TestCaseViewLoader report={report}></TestCaseViewLoader>}
|
||||
{!!report && (
|
||||
<TestCaseViewLoader
|
||||
report={report}
|
||||
filteredFiles={filteredFiles}
|
||||
filter={filter}
|
||||
/>
|
||||
)}
|
||||
</Route>
|
||||
</main>
|
||||
</div>;
|
||||
|
|
@ -72,21 +93,25 @@ export const ReportView: React.FC<{
|
|||
|
||||
const TestCaseViewLoader: React.FC<{
|
||||
report: LoadedReport,
|
||||
}> = ({ report }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
filteredFiles: TestFileSummary[],
|
||||
filter: Filter
|
||||
}> = ({ report, filteredFiles, filter }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const [test, setTest] = React.useState<TestCase | undefined>();
|
||||
const testId = searchParams.get('testId');
|
||||
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 file of filteredFiles) {
|
||||
for (const test of file.tests)
|
||||
map.set(test.testId, file.fileId);
|
||||
}
|
||||
return map;
|
||||
}, [report]);
|
||||
}, [filteredFiles]);
|
||||
|
||||
const { prevTestId, nextTestId } = getAdjacentTestIds(testIdToFileIdMap, testId);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
|
|
@ -104,19 +129,43 @@ 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}
|
||||
test={test}
|
||||
anchor={anchor}
|
||||
run={run}
|
||||
prevTestId={prevTestId}
|
||||
nextTestId={nextTestId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats {
|
||||
|
||||
function computeStats(filteredFiles: TestFileSummary[]): 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)
|
||||
for (const file of filteredFiles) {
|
||||
stats.total += file.tests.length;
|
||||
for (const test of file.tests)
|
||||
stats.duration += test.duration;
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
function getAdjacentTestIds(testIdToFileIdMap: Map<string, string>, currentTestId: string) {
|
||||
const testIds = [...testIdToFileIdMap.keys()];
|
||||
const currentIndex = testIds.indexOf(currentTestId);
|
||||
|
||||
const lastIndex = testIds.length - 1;
|
||||
const nextIndex = currentIndex === lastIndex ? 0 : currentIndex + 1;
|
||||
const prevIndex = currentIndex === 0 ? lastIndex : currentIndex - 1;
|
||||
|
||||
return {
|
||||
prevTestId: testIds[prevIndex],
|
||||
nextTestId: testIds[nextIndex],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,14 +34,26 @@
|
|||
color: var(--color-fg-default);
|
||||
}
|
||||
|
||||
.test-case-title-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: auto 32px;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.test-case-title {
|
||||
flex: none;
|
||||
padding: 8px;
|
||||
font-weight: 400;
|
||||
font-size: 32px !important;
|
||||
line-height: 1.25 !important;
|
||||
}
|
||||
|
||||
.test-case-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.test-case-location,
|
||||
.test-case-duration {
|
||||
flex: none;
|
||||
|
|
@ -73,4 +85,4 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,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} run={0} anchor='' prevTestId='' nextTestId=''></TestCaseView>);
|
||||
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
|
||||
await expect(component.getByText('Hidden annotation')).toBeHidden();
|
||||
await component.getByText('Annotations').click();
|
||||
|
|
@ -96,7 +96,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} run={0} anchor='' prevTestId='' nextTestId=''></TestCaseView>);
|
||||
|
||||
const firstLink = await component.getByText('https://playwright.dev/docs/intro').first();
|
||||
await expect(firstLink).toBeVisible();
|
||||
|
|
@ -154,7 +154,7 @@ const attachmentLinkRenderingTestCase: TestCase = {
|
|||
};
|
||||
|
||||
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} run={0} anchor='' prevTestId='' nextTestId=''></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();
|
||||
|
|
@ -163,8 +163,24 @@ 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} run={0} anchor='' prevTestId='' nextTestId=''></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 navigation links', async ({ mount, page }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} run={0} anchor='' prevTestId='prev-test-id' nextTestId='next-test-id'></TestCaseView>);
|
||||
|
||||
const prevLink = component.getByRole('link').and(page.getByTitle('Prev test case'));
|
||||
const nextLink = component.getByRole('link').and(page.getByTitle('Next test case'));
|
||||
expect(prevLink).toBeVisible();
|
||||
expect(nextLink).toBeVisible();
|
||||
|
||||
await prevLink.click();
|
||||
await expect(page).toHaveURL(/testId=prev-test-id/);
|
||||
|
||||
await nextLink.click();
|
||||
await expect(page).toHaveURL(/testId=next-test-id/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -19,19 +19,26 @@ import * as React from 'react';
|
|||
import { TabbedPane } from './tabbedPane';
|
||||
import { AutoChip } from './chip';
|
||||
import './common.css';
|
||||
import { ProjectLink } from './links';
|
||||
import { Link, ProjectLink } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import './testCaseView.css';
|
||||
import { TestResultView } from './testResultView';
|
||||
import { linkifyText } from './renderUtils';
|
||||
import { hashStringToInt, msToString } from './utils';
|
||||
import * as icons from './icons';
|
||||
import { useSearchParams } from './use-search-params';
|
||||
|
||||
export const TestCaseView: React.FC<{
|
||||
projectNames: string[],
|
||||
test: TestCase | undefined,
|
||||
anchor: 'video' | 'diff' | '',
|
||||
run: number,
|
||||
}> = ({ projectNames, test, run, anchor }) => {
|
||||
prevTestId: string
|
||||
nextTestId: string
|
||||
}> = ({ projectNames, test, run, anchor, prevTestId, nextTestId }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const q = searchParams.get('q') ?? '';
|
||||
|
||||
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
|
||||
|
||||
const labels = React.useMemo(() => {
|
||||
|
|
@ -46,7 +53,15 @@ export const TestCaseView: React.FC<{
|
|||
|
||||
return <div className='test-case-column vbox'>
|
||||
{test && <div className='test-case-path'>{test.path.join(' › ')}</div>}
|
||||
{test && <div className='test-case-title'>{test?.title}</div>}
|
||||
{test && (
|
||||
<div className='test-case-title-wrapper'>
|
||||
<div className='test-case-title'>{test?.title}</div>
|
||||
<div className="test-case-navigation">
|
||||
<Link href={`#?q=${q}&testId=${prevTestId}`} title="Prev test case">{icons.leftArrow()}</Link>
|
||||
<Link href={`#?q=${q}&testId=${nextTestId}`} title="Next test case">{icons.rightArrow()}</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{test && <div className='hbox'>
|
||||
<div className='test-case-location'>{test.location.file}:{test.location.line}</div>
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
|
|||
import { statusIcon } from './statusIcon';
|
||||
import './testFileView.css';
|
||||
import { video, image, trace } from './icons';
|
||||
import { useSearchParams } from './use-search-params';
|
||||
|
||||
export const TestFileView: React.FC<React.PropsWithChildren<{
|
||||
report: HTMLReport;
|
||||
|
|
@ -31,6 +32,9 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
setFileExpanded: (fileId: string, expanded: boolean) => void;
|
||||
filter: Filter;
|
||||
}>> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const q = searchParams.get('q') ?? '';
|
||||
|
||||
return <Chip
|
||||
expanded={isFileExpanded(file.fileId)}
|
||||
noInsets={true}
|
||||
|
|
@ -38,7 +42,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={'test-file-test test-file-test-outcome-' + test.outcome}>
|
||||
<div className='hbox' style={{ alignItems: 'flex-start' }}>
|
||||
<div className="hbox">
|
||||
|
|
@ -46,7 +50,7 @@ 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={`#?q=${q}&testId=${test.testId}`} title={[...test.path, test.title].join(' › ')}>
|
||||
<span className='test-file-title'>{[...test.path, test.title].join(' › ')}</span>
|
||||
</Link>
|
||||
{report.projectNames.length > 1 && !!test.projectName &&
|
||||
|
|
@ -57,11 +61,11 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
<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'>
|
||||
<Link href={`#?q=${q}&testId=${test.testId}`} title={[...test.path, test.title].join(' › ')} className='test-file-path-link'>
|
||||
<span className='test-file-path'>{test.location.file}:{test.location.line}</span>
|
||||
</Link>
|
||||
{imageDiffBadge(test)}
|
||||
{videoBadge(test)}
|
||||
{imageDiffBadge(test, q)}
|
||||
{videoBadge(test, q)}
|
||||
{traceBadge(test)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -69,16 +73,16 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
</Chip>;
|
||||
};
|
||||
|
||||
function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
function imageDiffBadge(test: TestCaseSummary, q: string): JSX.Element | undefined {
|
||||
const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => {
|
||||
return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/);
|
||||
}));
|
||||
return resultWithImageDiff ? <Link href={`#?testId=${test.testId}&anchor=diff&run=${test.results.indexOf(resultWithImageDiff)}`} title='View images' className='test-file-badge'>{image()}</Link> : undefined;
|
||||
return resultWithImageDiff ? <Link href={`#?q=${q}&testId=${test.testId}&anchor=diff&run=${test.results.indexOf(resultWithImageDiff)}`} title='View images' className='test-file-badge'>{image()}</Link> : undefined;
|
||||
}
|
||||
|
||||
function videoBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
function videoBadge(test: TestCaseSummary, q: string): JSX.Element | undefined {
|
||||
const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video'));
|
||||
return resultWithVideo ? <Link href={`#?testId=${test.testId}&anchor=video&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
|
||||
return resultWithVideo ? <Link href={`#?q=${q}&testId=${test.testId}&anchor=video&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
|
||||
}
|
||||
|
||||
function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
|
|
|
|||
|
|
@ -30,18 +30,18 @@ export const TestFilesView: React.FC<{
|
|||
filter: Filter,
|
||||
filteredStats: FilteredStats,
|
||||
projectNames: string[],
|
||||
}> = ({ report, filter, expandedFiles, setExpandedFiles, projectNames, filteredStats }) => {
|
||||
const filteredFiles = React.useMemo(() => {
|
||||
filteredFiles: TestFileSummary[]
|
||||
}> = ({ report, filter, expandedFiles, setExpandedFiles, projectNames, filteredStats, filteredFiles }) => {
|
||||
const filesSummary = 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 filteredFiles) {
|
||||
visibleTests += file.tests.length;
|
||||
result.push({ file, defaultExpanded: visibleTests < 200 });
|
||||
}
|
||||
return result;
|
||||
}, [report, filter]);
|
||||
}, [filteredFiles]);
|
||||
|
||||
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>}
|
||||
|
|
@ -53,7 +53,7 @@ export const TestFilesView: React.FC<{
|
|||
{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 }) => {
|
||||
{report && filesSummary.map(({ file, defaultExpanded }) => {
|
||||
return <TestFileView
|
||||
key={`file-${file.fileId}`}
|
||||
report={report}
|
||||
|
|
|
|||
34
packages/html-reporter/src/use-search-params.ts
Normal file
34
packages/html-reporter/src/use-search-params.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export function useSearchParams() {
|
||||
const [searchParams, setSearchParams] = React.useState(getCurrentSearchParams());
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = () => setSearchParams(getCurrentSearchParams());
|
||||
|
||||
window.addEventListener('popstate', handler);
|
||||
return () => window.removeEventListener('popstate', handler);
|
||||
}, []);
|
||||
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
function getCurrentSearchParams() {
|
||||
return new URLSearchParams(window.location.hash.slice(1));
|
||||
}
|
||||
|
|
@ -2437,6 +2437,111 @@ for (const useIntermediateMergeReport of [false] as const) {
|
|||
await testFilePathLink.click();
|
||||
await expect(page.locator('.test-case-path')).toHaveText('Root describe');
|
||||
});
|
||||
|
||||
test.describe('test details navigation', () => {
|
||||
test('should allow navigating between test cases', async ({ page, runInlineTest, showReport }) => {
|
||||
await runInlineTest({
|
||||
'a.test.js': `
|
||||
const { expect, test } = require('@playwright/test');
|
||||
test('first test', async ({}) => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
`,
|
||||
'b.test.js': `
|
||||
const { expect, test } = require('@playwright/test');
|
||||
test('second test', async ({}) => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
`,
|
||||
'c.test.js': `
|
||||
const { expect, test } = require('@playwright/test');
|
||||
test('third test', async ({}) => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
`,
|
||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||
|
||||
await showReport();
|
||||
|
||||
await page.getByText('first test').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('first test');
|
||||
|
||||
await page.getByTitle('Next test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('second test');
|
||||
|
||||
await page.getByTitle('Prev test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('first test');
|
||||
|
||||
await page.getByTitle('Next test case').click();
|
||||
await page.getByTitle('Next test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('third test');
|
||||
|
||||
await page.getByTitle('Next test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('first test');
|
||||
|
||||
await page.getByTitle('Prev test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('third test');
|
||||
});
|
||||
|
||||
test('filters should be preserved when navigating between test cases', async ({ page, runInlineTest, showReport }) => {
|
||||
await runInlineTest({
|
||||
'a.test.js': `
|
||||
const { expect, test } = require('@playwright/test');
|
||||
test('@regression first failed test', async ({}) => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
|
||||
test('@regression first passed test', async ({}) => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
`,
|
||||
'b.test.js': `
|
||||
const { expect, test } = require('@playwright/test');
|
||||
test('@smoke second failed test', async ({}) => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
|
||||
test('@smoke second passed test', async ({}) => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
`,
|
||||
'c.test.js': `
|
||||
const { expect, test } = require('@playwright/test');
|
||||
test('third failed test', async ({}) => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
|
||||
test('third passed test', async ({}) => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
`
|
||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||
|
||||
await showReport();
|
||||
|
||||
await page.locator('.subnav-item:has-text("Failed")').click();
|
||||
await page.getByText('@regression first failed test').click();
|
||||
|
||||
await page.getByTitle('Next test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('@smoke second failed test');
|
||||
|
||||
await page.getByTitle('Next test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('third failed test');
|
||||
|
||||
await page.getByTitle('Next test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('@regression first failed test');
|
||||
|
||||
await page.locator('.subnav-item:has-text("All")').click();
|
||||
await page.locator('.label:has-text("smoke")').first().click();
|
||||
await page.getByText('@smoke second failed test').click();
|
||||
|
||||
await page.getByTitle('Next test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('@smoke second passed test');
|
||||
|
||||
await page.getByTitle('Next test case').click();
|
||||
await expect(page.locator('.test-case-title')).toHaveText('@smoke second failed test');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue