rework attachment revealing
This commit is contained in:
parent
1fbb66d739
commit
4276d45ee9
|
|
@ -115,7 +115,17 @@ export function generateTraceUrl(traces: TestAttachment[]) {
|
||||||
|
|
||||||
const kMissingContentType = 'x-playwright/missing';
|
const kMissingContentType = 'x-playwright/missing';
|
||||||
|
|
||||||
export type AnchorID = string | ((id: string) => boolean) | undefined;
|
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
||||||
|
|
||||||
|
function matchesAnchor(id: AnchorID, anchor: string): boolean {
|
||||||
|
if (typeof id === 'undefined')
|
||||||
|
return false;
|
||||||
|
if (typeof id === 'string')
|
||||||
|
return id === anchor;
|
||||||
|
if (Array.isArray(id))
|
||||||
|
return id.includes(anchor);
|
||||||
|
return id(anchor);
|
||||||
|
}
|
||||||
|
|
||||||
export function useAnchor(id: AnchorID, onReveal: () => void) {
|
export function useAnchor(id: AnchorID, onReveal: () => void) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -127,8 +137,7 @@ export function useAnchor(id: AnchorID, onReveal: () => void) {
|
||||||
if (!params.has('anchor'))
|
if (!params.has('anchor'))
|
||||||
return;
|
return;
|
||||||
const anchor = params.get('anchor');
|
const anchor = params.get('anchor');
|
||||||
const isRevealed = typeof id === 'function' ? id(anchor!) : anchor === id;
|
if (matchesAnchor(id, anchor!))
|
||||||
if (isRevealed)
|
|
||||||
onReveal();
|
onReveal();
|
||||||
};
|
};
|
||||||
window.addEventListener('popstate', listener);
|
window.addEventListener('popstate', listener);
|
||||||
|
|
@ -141,7 +150,7 @@ export function useIsAnchored(id: AnchorID) {
|
||||||
if (!searchParams.has('anchor'))
|
if (!searchParams.has('anchor'))
|
||||||
return false;
|
return false;
|
||||||
const anchor = searchParams.get('anchor');
|
const anchor = searchParams.get('anchor');
|
||||||
return typeof id === 'function' ? id(anchor!) : anchor === id;
|
return matchesAnchor(id, anchor!);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
|
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
|
||||||
|
|
|
||||||
|
|
@ -72,15 +72,17 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
||||||
};
|
};
|
||||||
|
|
||||||
function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
|
function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||||
const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => {
|
for (const result of test.results) {
|
||||||
return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/);
|
for (const attachment of result.attachments) {
|
||||||
}));
|
if (attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/))
|
||||||
return resultWithImageDiff ? <Link href={`#?testId=${test.testId}&anchor=diff-0&run=${test.results.indexOf(resultWithImageDiff)}`} title='View images' className='test-file-badge'>{image()}</Link> : undefined;
|
return <Link href={`#?testId=${test.testId}&anchor=attachment-${attachment.name}&run=${test.results.indexOf(result)}`} title='View images' className='test-file-badge'>{image()}</Link>;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function videoBadge(test: TestCaseSummary): JSX.Element | undefined {
|
function videoBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||||
const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video'));
|
const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video'));
|
||||||
return resultWithVideo ? <Link href={`#?testId=${test.testId}&anchor=videos&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
|
return resultWithVideo ? <Link href={`#?testId=${test.testId}&anchor=attachment-video&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
|
function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,12 @@ import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||||
import * as icons from './icons';
|
import * as icons from './icons';
|
||||||
import './testResultView.css';
|
import './testResultView.css';
|
||||||
|
|
||||||
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
interface ImageDiffWithAnchors extends ImageDiff {
|
||||||
const snapshotNameToImageDiff = new Map<string, ImageDiff>();
|
anchors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiffWithAnchors[] {
|
||||||
|
const snapshotNameToImageDiff = new Map<string, ImageDiffWithAnchors>();
|
||||||
for (const attachment of screenshots) {
|
for (const attachment of screenshots) {
|
||||||
const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
|
const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
|
||||||
if (!match)
|
if (!match)
|
||||||
|
|
@ -38,9 +42,10 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
||||||
const snapshotName = name + extension;
|
const snapshotName = name + extension;
|
||||||
let imageDiff = snapshotNameToImageDiff.get(snapshotName);
|
let imageDiff = snapshotNameToImageDiff.get(snapshotName);
|
||||||
if (!imageDiff) {
|
if (!imageDiff) {
|
||||||
imageDiff = { name: snapshotName };
|
imageDiff = { name: snapshotName, anchors: [`attachment-${name}`] };
|
||||||
snapshotNameToImageDiff.set(snapshotName, imageDiff);
|
snapshotNameToImageDiff.set(snapshotName, imageDiff);
|
||||||
}
|
}
|
||||||
|
imageDiff.anchors.push(`attachment-${attachment.name}`);
|
||||||
if (category === 'actual')
|
if (category === 'actual')
|
||||||
imageDiff.actual = { attachment };
|
imageDiff.actual = { attachment };
|
||||||
if (category === 'expected')
|
if (category === 'expected')
|
||||||
|
|
@ -66,19 +71,21 @@ export const TestResultView: React.FC<{
|
||||||
test: TestCase,
|
test: TestCase,
|
||||||
result: TestResult,
|
result: TestResult,
|
||||||
}> = ({ test, result }) => {
|
}> = ({ test, result }) => {
|
||||||
const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => {
|
const { screenshots, videos, traces, otherAttachments, diffs, errors } = React.useMemo(() => {
|
||||||
const attachments = result?.attachments || [];
|
const attachments = result?.attachments || [];
|
||||||
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
||||||
const videos = attachments.filter(a => a.contentType.startsWith('video/'));
|
const videos = attachments.filter(a => a.contentType.startsWith('video/'));
|
||||||
const traces = attachments.filter(a => a.name === 'trace');
|
const traces = attachments.filter(a => a.name === 'trace');
|
||||||
const htmls = attachments.filter(a => a.contentType.startsWith('text/html'));
|
|
||||||
const otherAttachments = new Set<TestAttachment>(attachments);
|
const otherAttachments = new Set<TestAttachment>(attachments);
|
||||||
[...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a));
|
[...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a));
|
||||||
const diffs = groupImageDiffs(screenshots);
|
const diffs = groupImageDiffs(screenshots);
|
||||||
const errors = classifyErrors(result.errors, diffs);
|
const errors = classifyErrors(result.errors, diffs);
|
||||||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls };
|
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors };
|
||||||
}, [result]);
|
}, [result]);
|
||||||
|
|
||||||
|
const screenshotAnchor = React.useMemo(() => screenshots.map(a => `attachment-${a.name}`), [screenshots]);
|
||||||
|
const otherAttachmentsAnchor = React.useMemo(() => [...otherAttachments].map(a => `attachment-${a.name}`), [otherAttachments]);
|
||||||
|
|
||||||
return <div className='test-result'>
|
return <div className='test-result'>
|
||||||
{!!errors.length && <AutoChip header='Errors'>
|
{!!errors.length && <AutoChip header='Errors'>
|
||||||
{errors.map((error, index) => {
|
{errors.map((error, index) => {
|
||||||
|
|
@ -92,14 +99,14 @@ export const TestResultView: React.FC<{
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
|
|
||||||
{diffs.map((diff, index) =>
|
{diffs.map((diff, index) =>
|
||||||
<Anchor key={`diff-${index}`} id={`diff-${index}`}>
|
<Anchor key={`diff-${index}`} id={diff.anchors}>
|
||||||
<AutoChip dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} revealOnAnchorId={`diff-${index}`}>
|
<AutoChip dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} revealOnAnchorId={diff.anchors}>
|
||||||
<ImageDiffView diff={diff}/>
|
<ImageDiffView diff={diff}/>
|
||||||
</AutoChip>
|
</AutoChip>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!!screenshots.length && <AutoChip header='Screenshots'>
|
{!!screenshots.length && <Anchor id={screenshotAnchor}><AutoChip header='Screenshots' revealOnAnchorId={screenshotAnchor}>
|
||||||
{screenshots.map((a, i) => {
|
{screenshots.map((a, i) => {
|
||||||
return <div key={`screenshot-${i}`}>
|
return <div key={`screenshot-${i}`}>
|
||||||
<a href={a.path}>
|
<a href={a.path}>
|
||||||
|
|
@ -108,9 +115,9 @@ export const TestResultView: React.FC<{
|
||||||
<AttachmentLink attachment={a}></AttachmentLink>
|
<AttachmentLink attachment={a}></AttachmentLink>
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
</AutoChip>}
|
</AutoChip></Anchor>}
|
||||||
|
|
||||||
{!!traces.length && <Anchor id='traces'><AutoChip header='Traces' revealOnAnchorId='traces'>
|
{!!traces.length && <Anchor id='attachment-trace'><AutoChip header='Traces' revealOnAnchorId='attachment-trace'>
|
||||||
{<div>
|
{<div>
|
||||||
<a href={generateTraceUrl(traces)}>
|
<a href={generateTraceUrl(traces)}>
|
||||||
<img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
<img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
||||||
|
|
@ -119,7 +126,7 @@ export const TestResultView: React.FC<{
|
||||||
</div>}
|
</div>}
|
||||||
</AutoChip></Anchor>}
|
</AutoChip></Anchor>}
|
||||||
|
|
||||||
{!!videos.length && <Anchor id='videos'><AutoChip header='Videos' revealOnAnchorId='videos'>
|
{!!videos.length && <Anchor id='attachment-video'><AutoChip header='Videos' revealOnAnchorId='attachment-video'>
|
||||||
{videos.map((a, i) => <div key={`video-${i}`}>
|
{videos.map((a, i) => <div key={`video-${i}`}>
|
||||||
<video controls>
|
<video controls>
|
||||||
<source src={a.path} type={a.contentType}/>
|
<source src={a.path} type={a.contentType}/>
|
||||||
|
|
@ -128,15 +135,10 @@ export const TestResultView: React.FC<{
|
||||||
</div>)}
|
</div>)}
|
||||||
</AutoChip></Anchor>}
|
</AutoChip></Anchor>}
|
||||||
|
|
||||||
{!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments' revealOnAnchorId={id => id.startsWith('attachment-')}>
|
{!!otherAttachments.size && <AutoChip header='Attachments' revealOnAnchorId={otherAttachmentsAnchor}>
|
||||||
{[...htmls].map((a, i) => (
|
|
||||||
<Anchor key={`html-link-${i}`} id={`attachment-${a.name}`}>
|
|
||||||
<AttachmentLink attachment={a} openInNewTab />
|
|
||||||
</Anchor>)
|
|
||||||
)}
|
|
||||||
{[...otherAttachments].map((a, i) =>
|
{[...otherAttachments].map((a, i) =>
|
||||||
<Anchor key={`attachment-link-${i}`} id={`attachment-${a.name}`}>
|
<Anchor key={`attachment-link-${i}`} id={`attachment-${a.name}`}>
|
||||||
<AttachmentLink attachment={a}/>
|
<AttachmentLink attachment={a} openInNewTab={a.contentType.startsWith('text/html')} />
|
||||||
</Anchor>
|
</Anchor>
|
||||||
)}
|
)}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
|
|
@ -176,7 +178,7 @@ const StepTreeItem: React.FC<{
|
||||||
const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1];
|
const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1];
|
||||||
return <TreeItem title={<span aria-label={step.title}>
|
return <TreeItem title={<span aria-label={step.title}>
|
||||||
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
||||||
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={`#?testId=${test.testId}&anchor=attachment-${attachmentName}&run=${test.results.indexOf(result)}`} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
|
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={`#?testId=${test.testId}&anchor=attachment-${encodeURIComponent(attachmentName)}&run=${test.results.indexOf(result)}`} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
|
||||||
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
||||||
<span>{step.title}</span>
|
<span>{step.title}</span>
|
||||||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue