diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 5cdaad4a69..98ab54d6a1 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -26,57 +26,45 @@ import { AttachmentLink } from './links'; import { statusIcon } from './statusIcon'; import './testResultView.css'; -type DiffTab = { - id: string, - title: string, - attachment: TestAttachment, +type ImageDiff = { + name: string, + left?: { attachment: TestAttachment, title: string }, + right?: { attachment: TestAttachment, title: string }, + diff?: { attachment: TestAttachment, title: string }, }; -function classifyAttachments(attachments: TestAttachment[]) { - const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); - const videos = attachments.filter(a => a.name === 'video'); - const traces = attachments.filter(a => a.name === 'trace'); - - const otherAttachments = new Set(attachments); - [...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a)); - - const snapshotNameToDiffTabs = new Map(); - let tabId = 0; - for (const attachment of attachments) { - const match = attachment.name.match(/^(.*)-(\w+)(\.[^.]+)?$/); +function groupImageDiffs(screenshots: Set): ImageDiff[] { + const snapshotNameToImageDiff = new Map(); + for (const attachment of screenshots) { + const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/); if (!match) continue; const [, name, category, extension = ''] = match; const snapshotName = name + extension; - let diffTabs = snapshotNameToDiffTabs.get(snapshotName); - if (!diffTabs) { - diffTabs = []; - snapshotNameToDiffTabs.set(snapshotName, diffTabs); + let imageDiff = snapshotNameToImageDiff.get(snapshotName); + if (!imageDiff) { + imageDiff = { name: snapshotName }; + snapshotNameToImageDiff.set(snapshotName, imageDiff); } - diffTabs.push({ - id: 'tab-' + (++tabId), - title: category, - attachment, - }); + if (category === 'actual') + imageDiff.left = { attachment, title: 'Actual' }; + if (category === 'expected') + imageDiff.right = { attachment, title: 'Expected' }; + if (category === 'previous') + imageDiff.right = { attachment, title: 'Previous' }; + if (category === 'diff') + imageDiff.diff = { attachment, title: 'Diff' }; } - const diffs = [...snapshotNameToDiffTabs].map(([snapshotName, diffTabs]) => { - diffTabs.sort((tab1: DiffTab, tab2: DiffTab) => { - if (tab1.title === 'diff' || tab2.title === 'diff') - return tab1.title === 'diff' ? -1 : 1; - if (tab1.title !== tab2.title) - return tab1.title < tab2.title ? -1 : 1; - return 0; - }); - const isImageDiff = diffTabs.some(tab => screenshots.has(tab.attachment)); - for (const tab of diffTabs) - screenshots.delete(tab.attachment); - return { - tabs: diffTabs, - isImageDiff, - snapshotName, - }; - }).filter(diff => diff.tabs.some(tab => ['diff', 'actual', 'expected'].includes(tab.title.toLowerCase()))); - return { diffs, screenshots: [...screenshots], videos, otherAttachments, traces }; + for (const [name, diff] of snapshotNameToImageDiff) { + if (!diff.left || !diff.right) { + snapshotNameToImageDiff.delete(name); + } else { + screenshots.delete(diff.left.attachment); + screenshots.delete(diff.right.attachment); + screenshots.delete(diff.diff?.attachment!); + } + } + return [...snapshotNameToImageDiff.values()]; } export const TestResultView: React.FC<{ @@ -85,7 +73,14 @@ export const TestResultView: React.FC<{ }> = ({ result }) => { const { screenshots, videos, traces, otherAttachments, diffs } = React.useMemo(() => { - return classifyAttachments(result?.attachments || []); + const attachments = result?.attachments || []; + const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); + const videos = attachments.filter(a => a.name === 'video'); + const traces = attachments.filter(a => a.name === 'trace'); + const otherAttachments = new Set(attachments); + [...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a)); + const diffs = groupImageDiffs(screenshots); + return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs }; }, [ result ]); return
@@ -96,10 +91,9 @@ export const TestResultView: React.FC<{ {result.steps.map((step, i) => )} } - {diffs.map(({ tabs, snapshotName, isImageDiff }, index) => - - {isImageDiff && } - {tabs.map((tab: DiffTab) => )} + {diffs.map((diff, index) => + + )} @@ -154,23 +148,37 @@ const StepTreeItem: React.FC<{ } : undefined} depth={depth}>; }; -const ImageDiff: React.FunctionComponent<{ - tabs: DiffTab[], -}> = ({ tabs }) => { +const ImageDiffView: React.FunctionComponent<{ + imageDiff: ImageDiff, +}> = ({ imageDiff: diff }) => { // Pre-select a tab called "actual", if any. - const preselectedTab = tabs.find(tab => tab.title.toLowerCase() === 'actual') || tabs[0]; - const [selectedTab, setSelectedTab] = React.useState(preselectedTab.id); + const [selectedTab, setSelectedTab] = React.useState('left'); const diffElement = React.useRef(null); - const paneTabs = tabs.map(tab => ({ - id: tab.id, - title: tab.title, - render: () => { - if (diffElement.current) - diffElement.current.style.minHeight = diffElement.current.offsetHeight + 'px'; - }}/> - })); + const setMinHeight = () => { + if (diffElement.current) + diffElement.current.style.minHeight = diffElement.current.offsetHeight + 'px'; + }; + const tabs = [ + { + id: 'left', + title: diff.left!.title, + render: () => + }, + { + id: 'right', + title: diff.right!.title, + render: () => + }, + ]; + if (diff.diff) { + tabs.push({ + id: 'diff', + title: diff.diff.title, + render: () => + }); + } return
- +
; }; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 977fd26c40..70541318ec 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -259,9 +259,10 @@ test('should not include image diff with non-images', async ({ runInlineTest, pa await showReport(); await page.click('text=fails'); - await expect(page.locator('text=Snapshot mismatch')).toBeVisible(); await expect(page.locator('text=Image mismatch')).toHaveCount(0); await expect(page.locator('img')).toHaveCount(0); + await expect(page.locator('a', { hasText: 'expected-actual' })).toBeVisible(); + await expect(page.locator('a', { hasText: 'expected-expected' })).toBeVisible(); }); test('should include screenshot on failure', async ({ runInlineTest, page, showReport }) => {