From 4276d45ee925bcd5aa4c3a9c667424e06f55b2eb Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 20 Nov 2024 10:05:57 +0100 Subject: [PATCH] rework attachment revealing --- packages/html-reporter/src/links.tsx | 17 +++++-- packages/html-reporter/src/testFileView.tsx | 12 ++--- packages/html-reporter/src/testResultView.tsx | 44 ++++++++++--------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index be1bc7ced8..14078aa625 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -115,7 +115,17 @@ export function generateTraceUrl(traces: TestAttachment[]) { 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) { React.useEffect(() => { @@ -127,8 +137,7 @@ export function useAnchor(id: AnchorID, onReveal: () => void) { if (!params.has('anchor')) return; const anchor = params.get('anchor'); - const isRevealed = typeof id === 'function' ? id(anchor!) : anchor === id; - if (isRevealed) + if (matchesAnchor(id, anchor!)) onReveal(); }; window.addEventListener('popstate', listener); @@ -141,7 +150,7 @@ export function useIsAnchored(id: AnchorID) { if (!searchParams.has('anchor')) return false; 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 }>) { diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx index 6b31d2ebe2..ae80838b77 100644 --- a/packages/html-reporter/src/testFileView.tsx +++ b/packages/html-reporter/src/testFileView.tsx @@ -72,15 +72,17 @@ export const TestFileView: React.FC result.attachments.some(attachment => { - return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/); - })); - return resultWithImageDiff ? {image()} : undefined; + for (const result of test.results) { + for (const attachment of result.attachments) { + if (attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/)) + return {image()}; + } + } } function videoBadge(test: TestCaseSummary): JSX.Element | undefined { const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video')); - return resultWithVideo ? {video()} : undefined; + return resultWithVideo ? {video()} : undefined; } function traceBadge(test: TestCaseSummary): JSX.Element | undefined { diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 094b47e576..44509a626c 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -28,8 +28,12 @@ import { TestErrorView, TestScreenshotErrorView } from './testErrorView'; import * as icons from './icons'; import './testResultView.css'; -function groupImageDiffs(screenshots: Set): ImageDiff[] { - const snapshotNameToImageDiff = new Map(); +interface ImageDiffWithAnchors extends ImageDiff { + anchors: string[]; +} + +function groupImageDiffs(screenshots: Set): ImageDiffWithAnchors[] { + const snapshotNameToImageDiff = new Map(); for (const attachment of screenshots) { const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/); if (!match) @@ -38,9 +42,10 @@ function groupImageDiffs(screenshots: Set): ImageDiff[] { const snapshotName = name + extension; let imageDiff = snapshotNameToImageDiff.get(snapshotName); if (!imageDiff) { - imageDiff = { name: snapshotName }; + imageDiff = { name: snapshotName, anchors: [`attachment-${name}`] }; snapshotNameToImageDiff.set(snapshotName, imageDiff); } + imageDiff.anchors.push(`attachment-${attachment.name}`); if (category === 'actual') imageDiff.actual = { attachment }; if (category === 'expected') @@ -66,19 +71,21 @@ export const TestResultView: React.FC<{ test: TestCase, result: TestResult, }> = ({ 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 screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); const videos = attachments.filter(a => a.contentType.startsWith('video/')); const traces = attachments.filter(a => a.name === 'trace'); - const htmls = attachments.filter(a => a.contentType.startsWith('text/html')); const otherAttachments = new Set(attachments); - [...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a)); + [...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a)); const diffs = groupImageDiffs(screenshots); const errors = classifyErrors(result.errors, diffs); - return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls }; + return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors }; }, [result]); + const screenshotAnchor = React.useMemo(() => screenshots.map(a => `attachment-${a.name}`), [screenshots]); + const otherAttachmentsAnchor = React.useMemo(() => [...otherAttachments].map(a => `attachment-${a.name}`), [otherAttachments]); + return
{!!errors.length && {errors.map((error, index) => { @@ -92,14 +99,14 @@ export const TestResultView: React.FC<{ } {diffs.map((diff, index) => - - + + )} - {!!screenshots.length && + {!!screenshots.length && {screenshots.map((a, i) => { return ; })} - } + } - {!!traces.length && + {!!traces.length && {} } - {!!videos.length && + {!!videos.length && {videos.map((a, i) =>
)}
} - {!!(otherAttachments.size + htmls.length) && id.startsWith('attachment-')}> - {[...htmls].map((a, i) => ( - - - ) - )} + {!!otherAttachments.size && {[...otherAttachments].map((a, i) => - + )} } @@ -176,7 +178,7 @@ const StepTreeItem: React.FC<{ const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1]; return {msToString(step.duration)} - {attachmentName &&
{ evt.stopPropagation(); }}>{icons.attachment()}} + {attachmentName && { evt.stopPropagation(); }}>{icons.attachment()}} {statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')} {step.title} {step.count > 1 && <> ✕ {step.count}}