diff --git a/packages/trace-viewer/src/ui/attachmentsTab.tsx b/packages/trace-viewer/src/ui/attachmentsTab.tsx index 3ed7e26b90..481f08fe0a 100644 --- a/packages/trace-viewer/src/ui/attachmentsTab.tsx +++ b/packages/trace-viewer/src/ui/attachmentsTab.tsx @@ -17,62 +17,79 @@ import * as React from 'react'; import './attachmentsTab.css'; import { ImageDiffView } from '@web/components/imageDiffView'; -import type { TestAttachment } from '@web/components/imageDiffView'; -import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil'; +import type { MultiTraceModel } from './modelUtil'; import { PlaceholderPanel } from './placeholderPanel'; +import type { AfterActionTraceEventAttachment } from '@trace/trace'; + +type Attachment = AfterActionTraceEventAttachment & { traceUrl: string }; export const AttachmentsTab: React.FunctionComponent<{ model: MultiTraceModel | undefined, }> = ({ model }) => { - const attachments = model?.actions.map(a => a.attachments || []).flat() || []; - if (!model || !attachments.length) + const { diffMap, screenshots, attachments } = React.useMemo(() => { + const attachments = new Set(); + const screenshots = new Set(); + + for (const action of model?.actions || []) { + const traceUrl = action.context.traceUrl; + for (const attachment of action.attachments || []) + attachments.add({ ...attachment, traceUrl }); + } + const diffMap = new Map(); + + for (const attachment of attachments) { + if (!attachment.path && !attachment.sha1) + continue; + const match = attachment.name.match(/^(.*)-(expected|actual|diff)\.png$/); + if (match) { + const name = match[1]; + const type = match[2] as 'expected' | 'actual' | 'diff'; + const entry = diffMap.get(name) || { expected: undefined, actual: undefined, diff: undefined }; + entry[type] = attachment; + diffMap.set(name, entry); + } + if (attachment.contentType.startsWith('image/')) { + screenshots.add(attachment); + attachments.delete(attachment); + } + } + return { diffMap, attachments, screenshots }; + }, [model]); + + if (!diffMap.size && !screenshots.size && !attachments.size) return ; + return
- { model.actions.map((action, index) => ) } -
; -}; - -export const AttachmentsSection: React.FunctionComponent<{ - action: ActionTraceEventInContext | undefined, -}> = ({ action }) => { - if (!action) - return null; - const expected = action.attachments?.find(a => a.name.endsWith('-expected.png') && (a.path || a.sha1)) as TestAttachment | undefined; - const actual = action.attachments?.find(a => a.name.endsWith('-actual.png') && (a.path || a.sha1)) as TestAttachment | undefined; - const diff = action.attachments?.find(a => a.name.endsWith('-diff.png') && (a.path || a.sha1)) as TestAttachment | undefined; - const screenshots = new Set(action.attachments?.filter(a => a.contentType.startsWith('image/'))); - const otherAttachments = new Set(action.attachments || []); - screenshots.forEach(a => otherAttachments.delete(a)); - - const traceUrl = action.context.traceUrl; - - return <> - {expected && actual &&
Image diff
} - {expected && actual && } + {[...diffMap.values()].map(({ expected, actual, diff }) => { + return <> + {expected && actual &&
Image diff
} + {expected && actual && } + ; + })} {screenshots.size ?
Screenshots
: undefined} - {[...screenshots].map((a, i) => { - const url = attachmentURL(traceUrl, a); + {[...screenshots.values()].map((a, i) => { + const url = attachmentURL(a); return ; })} - {otherAttachments.size ?
Attachments
: undefined} - {[...otherAttachments].map((a, i) => { + {attachments.size ?
Attachments
: undefined} + {[...attachments.values()].map((a, i) => { return ; })} - ; + ; }; -function attachmentURL(traceUrl: string, attachment: NonNullable[0]) { +function attachmentURL(attachment: Attachment) { if (attachment.sha1) - return 'sha1/' + attachment.sha1 + '?trace=' + encodeURIComponent(traceUrl); + return 'sha1/' + attachment.sha1 + '?trace=' + encodeURIComponent(attachment.traceUrl); return 'file?path=' + encodeURIComponent(attachment.path!); }