diff --git a/packages/html-reporter/src/imageDiffView.tsx b/packages/html-reporter/src/imageDiffView.tsx index 6b4b786d7f..b6e036e42b 100644 --- a/packages/html-reporter/src/imageDiffView.tsx +++ b/packages/html-reporter/src/imageDiffView.tsx @@ -17,8 +17,7 @@ import type { TestAttachment } from '@playwright-test/reporters/html'; import * as React from 'react'; import { AttachmentLink } from './links'; -import { TabbedPane } from './tabbedPane'; -import './testResultView.css'; +import { TabbedPane, TabbedPaneTab } from './tabbedPane'; export type ImageDiff = { name: string, @@ -31,29 +30,56 @@ export const ImageDiffView: React.FunctionComponent<{ imageDiff: ImageDiff, }> = ({ imageDiff: diff }) => { // Pre-select a tab called "actual", if any. - const [selectedTab, setSelectedTab] = React.useState('left'); - const diffElement = React.useRef(null); - const setMinHeight = () => { + const [selectedTab, setSelectedTab] = React.useState('actual'); + const diffElement = React.useRef(null); + const imageElement = React.useRef(null); + const [sliderPosition, setSliderPosition] = React.useState(0); + const onImageLoaded = (side?: 'left' | 'right') => { if (diffElement.current) diffElement.current.style.minHeight = diffElement.current.offsetHeight + 'px'; + if (side && diffElement.current && imageElement.current) { + const gap = Math.max(0, (diffElement.current.offsetWidth - imageElement.current.offsetWidth) / 2 - 20); + if (side === 'left') + setSliderPosition(gap); + else if (side === 'right') + setSliderPosition(diffElement.current.offsetWidth - gap); + } }; - const tabs = [ - { - id: 'left', - title: diff.left!.title, - render: () => - }, - { - id: 'right', + const tabs: TabbedPaneTab[] = []; + if (diff.diff) { + tabs.push({ + id: 'actual', + title: 'Actual', + render: () => + onImageLoaded('right')} ref={imageElement} /> + + , + }); + tabs.push({ + id: 'expected', title: diff.right!.title, - render: () => - }, - ]; + render: () => + onImageLoaded('left')} ref={imageElement} /> + + , + }); + } else { + tabs.push({ + id: 'actual', + title: 'Actual', + render: () => onImageLoaded()} /> + }); + tabs.push({ + id: 'expected', + title: diff.right!.title, + render: () => onImageLoaded()} /> + }); + } if (diff.diff) { tabs.push({ id: 'diff', - title: diff.diff.title, - render: () => + title: 'Diff', + render: () => onImageLoaded()} /> }); } return
@@ -63,3 +89,79 @@ export const ImageDiffView: React.FunctionComponent<{ {diff.diff && }
; }; + +export const ImageDiffSlider: React.FC<{ + sliderPosition: number, + setSliderPosition: (position: number) => void, +}> = ({ children, sliderPosition, setSliderPosition }) => { + const [resizing, setResizing] = React.useState<{ offset: number, size: number } | null>(null); + const size = sliderPosition; + + const childrenArray = React.Children.toArray(children); + document.body.style.userSelect = resizing ? 'none' : 'inherit'; + + const gripStyle: React.CSSProperties = { + ...absolute, + zIndex: 100, + cursor: 'ew-resize', + left: resizing ? 0 : size - 4, + right: resizing ? 0 : undefined, + width: resizing ? 'initial' : 8, + }; + + return <> + {childrenArray[0]} +
+
+ {childrenArray[1]} +
+
setResizing({ offset: event.clientX, size })} + onMouseUp={() => setResizing(null)} + onMouseMove={event => { + if (!event.buttons) { + setResizing(null); + } else if (resizing) { + const offset = event.clientX; + const delta = offset - resizing.offset; + const newSize = resizing.size + delta; + + const splitView = (event.target as HTMLElement).parentElement!; + const rect = splitView.getBoundingClientRect(); + const size = Math.min(Math.max(0, newSize), rect.width); + setSliderPosition(size); + } + }} + >
+
+
+
+ +
+
+ ; +}; + +const absolute: React.CSSProperties = { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, +}; diff --git a/packages/html-reporter/src/tabbedPane.css b/packages/html-reporter/src/tabbedPane.css index 41084fe6fb..7a128da217 100644 --- a/packages/html-reporter/src/tabbedPane.css +++ b/packages/html-reporter/src/tabbedPane.css @@ -20,12 +20,6 @@ overflow: hidden; } -.tabbed-pane-tab-content { - display: flex; - flex: auto; - overflow: hidden; -} - .tabbed-pane-tab-strip { display: flex; align-items: center; diff --git a/packages/html-reporter/src/testResultView.css b/packages/html-reporter/src/testResultView.css index 1d9c39e949..d939c3536d 100644 --- a/packages/html-reporter/src/testResultView.css +++ b/packages/html-reporter/src/testResultView.css @@ -25,6 +25,7 @@ display: flex; align-items: center; justify-content: center; + position: relative; } .test-result > div { diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index bd506ad6c2..2575b729cc 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -145,16 +145,35 @@ test('should include image diff', async ({ runInlineTest, page, showReport }) => await page.click('text=fails'); await expect(page.locator('text=Image mismatch')).toBeVisible(); await expect(page.locator('text=Snapshot mismatch')).toHaveCount(0); + + const set = new Set(); + const imageDiff = page.locator('data-testid=test-result-image-mismatch'); - const image = imageDiff.locator('img'); - await expect(image).toHaveAttribute('src', /.*png/); - const actualSrc = await image.getAttribute('src'); + const expectedImage = imageDiff.locator('img').first(); + const actualImage = imageDiff.locator('img').last(); + await expect(expectedImage).toHaveAttribute('src', /.*png/); + await expect(actualImage).toHaveAttribute('src', /.*png/); + set.add(await expectedImage.getAttribute('src')); + set.add(await actualImage.getAttribute('src')); + expect(set.size, 'Should be two images overlaid').toBe(2); + + const sliderElement = imageDiff.locator('data-testid=test-result-image-mismatch-grip'); + await expect.poll(async () => { + return await sliderElement.evaluate(e => e.style.left); + }, 'Actual slider is on the right').toBe('590px'); + await imageDiff.locator('text="Expected"').click(); - const expectedSrc = await image.getAttribute('src'); + set.add(await expectedImage.getAttribute('src')); + set.add(await actualImage.getAttribute('src')); + expect(set.size).toBe(2); + + await expect.poll(async () => { + return await sliderElement.evaluate(e => e.style.left); + }, 'Actual slider is on the right').toBe('350px'); + await imageDiff.locator('text="Diff"').click(); - const diffSrc = await image.getAttribute('src'); - const set = new Set([expectedSrc, actualSrc, diffSrc]); - expect(set.size).toBe(3); + set.add(await imageDiff.locator('img').getAttribute('src')); + expect(set.size, 'Should be three images altogether').toBe(3); }); test('should include multiple image diffs', async ({ runInlineTest, page, showReport }) => { @@ -193,7 +212,7 @@ test('should include multiple image diffs', async ({ runInlineTest, page, showRe await expect(page.locator('text=Screenshots')).toHaveCount(0); for (let i = 0; i < 2; ++i) { const imageDiff = page.locator('data-testid=test-result-image-mismatch').nth(i); - const image = imageDiff.locator('img'); + const image = imageDiff.locator('img').first(); await expect(image).toHaveAttribute('src', /.*png/); } }); @@ -259,10 +278,11 @@ test('should include image diff when screenshot failed to generate due to animat await expect(page.locator('.chip-header', { hasText: 'Screenshots' })).toHaveCount(0); const imageDiff = page.locator('data-testid=test-result-image-mismatch'); const image = imageDiff.locator('img'); - await expect(image).toHaveAttribute('src', /.*png/); - const actualSrc = await image.getAttribute('src'); + await expect(image.first()).toHaveAttribute('src', /.*png/); + await expect(image.last()).toHaveAttribute('src', /.*png/); + const previousSrc = await image.first().getAttribute('src'); + const actualSrc = await image.last().getAttribute('src'); await imageDiff.locator('text="Previous"').click(); - const previousSrc = await image.getAttribute('src'); await imageDiff.locator('text="Diff"').click(); const diffSrc = await image.getAttribute('src'); const set = new Set([previousSrc, actualSrc, diffSrc]);