From f5c57d0e98849820b733506d087dd21b32668757 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 22 Dec 2023 10:17:35 -0800 Subject: [PATCH] chore: reuse image diff component in trace/html (#28727) Fixes https://github.com/microsoft/playwright/issues/28685 --- packages/html-reporter/playwright.config.ts | 8 + packages/html-reporter/src/imageDiffView.css | 47 ---- packages/html-reporter/src/imageDiffView.tsx | 197 ---------------- packages/html-reporter/src/testResultView.css | 2 +- packages/html-reporter/src/testResultView.tsx | 10 +- .../trace-viewer/src/ui/attachmentsTab.tsx | 4 +- packages/trace-viewer/src/ui/timeline.tsx | 2 +- packages/web/src/components/imageDiffView.css | 46 ---- packages/web/src/components/imageDiffView.tsx | 178 -------------- packages/web/src/shared/DEPS.list | 5 + .../src/{components => shared}/glassPane.tsx | 2 +- .../src/shared}/imageDiffView.spec.tsx | 36 +-- packages/web/src/shared/imageDiffView.tsx | 219 ++++++++++++++++++ tests/playwright-test/reporter-html.spec.ts | 73 +++--- tests/playwright-test/ui-mode-trace.spec.ts | 2 +- 15 files changed, 286 insertions(+), 545 deletions(-) delete mode 100644 packages/html-reporter/src/imageDiffView.css delete mode 100644 packages/html-reporter/src/imageDiffView.tsx delete mode 100644 packages/web/src/components/imageDiffView.css delete mode 100644 packages/web/src/components/imageDiffView.tsx create mode 100644 packages/web/src/shared/DEPS.list rename packages/web/src/{components => shared}/glassPane.tsx (97%) rename packages/{html-reporter/src => web/src/shared}/imageDiffView.spec.tsx (69%) create mode 100644 packages/web/src/shared/imageDiffView.tsx diff --git a/packages/html-reporter/playwright.config.ts b/packages/html-reporter/playwright.config.ts index aa3c5e0494..ec2ff3c42f 100644 --- a/packages/html-reporter/playwright.config.ts +++ b/packages/html-reporter/playwright.config.ts @@ -15,6 +15,7 @@ */ import { devices, defineConfig } from '@playwright/experimental-ct-react'; +import path from 'path'; export default defineConfig({ testDir: 'src', @@ -24,6 +25,13 @@ export default defineConfig({ reporter: process.env.CI ? 'blob' : 'html', use: { ctPort: 3101, + ctViteConfig: { + resolve: { + alias: { + '@web': path.resolve(__dirname, '../web/src'), + }, + } + }, trace: 'on-first-retry', }, projects: [{ diff --git a/packages/html-reporter/src/imageDiffView.css b/packages/html-reporter/src/imageDiffView.css deleted file mode 100644 index 0f2ffe85d3..0000000000 --- a/packages/html-reporter/src/imageDiffView.css +++ /dev/null @@ -1,47 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -.image-diff-view .tabbed-pane .tab-content { - display: flex; - align-items: center; - justify-content: center; - position: relative; -} - -.image-diff-view .image-wrapper img { - flex: auto; - box-shadow: none; - margin: 24px auto; - min-width: 200px; - max-width: 80%; -} - -.image-diff-view .image-wrapper { - flex: auto; - display: flex; - flex-direction: column; - align-items: center; -} - -.image-diff-view .image-wrapper div { - flex: none; - align-self: stretch; - height: 2em; - font-weight: 500; - padding-top: 1em; - display: flex; - flex-direction: row; -} diff --git a/packages/html-reporter/src/imageDiffView.tsx b/packages/html-reporter/src/imageDiffView.tsx deleted file mode 100644 index 705a827716..0000000000 --- a/packages/html-reporter/src/imageDiffView.tsx +++ /dev/null @@ -1,197 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -import type { TestAttachment } from './types'; -import * as React from 'react'; -import { AttachmentLink } from './links'; -import type { TabbedPaneTab } from './tabbedPane'; -import { TabbedPane } from './tabbedPane'; -import './imageDiffView.css'; -import './tabbedPane.css'; - -export type ImageDiff = { - name: string, - expected?: { attachment: TestAttachment, title: string }, - actual?: { attachment: TestAttachment }, - diff?: { attachment: TestAttachment }, -}; - -export const ImageDiffView: React.FunctionComponent<{ - imageDiff: ImageDiff, -}> = ({ imageDiff: diff }) => { - // Pre-select a tab called "diff", if any. - const [selectedTab, setSelectedTab] = React.useState('diff'); - 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: TabbedPaneTab[] = []; - if (diff.diff) { - tabs.push({ - id: 'diff', - title: 'Diff', - render: () => onImageLoaded()} /> - }); - tabs.push({ - id: 'actual', - title: 'Actual', - render: () => - onImageLoaded('right')} imageRef={imageElement} style={{ boxShadow: 'none' }} /> - - , - }); - tabs.push({ - id: 'expected', - title: diff.expected!.title, - render: () => - onImageLoaded('left')} imageRef={imageElement} /> - - , - }); - } else { - tabs.push({ - id: 'actual', - title: 'Actual', - render: () => onImageLoaded()} /> - }); - tabs.push({ - id: 'expected', - title: diff.expected!.title, - render: () => onImageLoaded()} /> - }); - } - return
- - {diff.diff && } - - -
; -}; - -export const ImageDiffSlider: React.FC 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 ImageWithSize: React.FunctionComponent<{ - src: string, - onLoad?: () => void, - imageRef?: React.RefObject, - style?: React.CSSProperties, -}> = ({ src, onLoad, imageRef, style }) => { - const newRef = React.useRef(null); - const ref = imageRef ?? newRef; - const [size, setSize] = React.useState<{ width: number, height: number } | null>(null); - return
-
- { size ? size.width : ''} - x - { size ? size.height : ''} -
- { - onLoad?.(); - if (ref.current) - setSize({ width: ref.current.naturalWidth, height: ref.current.naturalHeight }); - }} ref={ref} style={style} /> -
; -}; - -const absolute: React.CSSProperties = { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, -}; diff --git a/packages/html-reporter/src/testResultView.css b/packages/html-reporter/src/testResultView.css index 79773b4302..81fd61453d 100644 --- a/packages/html-reporter/src/testResultView.css +++ b/packages/html-reporter/src/testResultView.css @@ -26,7 +26,7 @@ } .test-result video, -.test-result img { +.test-result img.screenshot { flex: none; box-shadow: var(--box-shadow-thick); margin: 24px auto; diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 43842aabcf..53c8d19d92 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -22,8 +22,8 @@ import { AutoChip } from './chip'; import { traceImage } from './images'; import { AttachmentLink, generateTraceUrl } from './links'; import { statusIcon } from './statusIcon'; -import type { ImageDiff } from './imageDiffView'; -import { ImageDiffView } from './imageDiffView'; +import type { ImageDiff } from '@web/shared/imageDiffView'; +import { ImageDiffView } from '@web/shared/imageDiffView'; import { TestErrorView } from './testErrorView'; import './testResultView.css'; @@ -102,7 +102,7 @@ export const TestResultView: React.FC<{ {diffs.map((diff, index) => - + )} @@ -110,7 +110,7 @@ export const TestResultView: React.FC<{ {screenshots.map((a, i) => { return ; @@ -120,7 +120,7 @@ export const TestResultView: React.FC<{ {!!traces.length && {
- + {traces.map((a, i) => )}
} diff --git a/packages/trace-viewer/src/ui/attachmentsTab.tsx b/packages/trace-viewer/src/ui/attachmentsTab.tsx index 481f08fe0a..179e6e5c91 100644 --- a/packages/trace-viewer/src/ui/attachmentsTab.tsx +++ b/packages/trace-viewer/src/ui/attachmentsTab.tsx @@ -16,7 +16,7 @@ import * as React from 'react'; import './attachmentsTab.css'; -import { ImageDiffView } from '@web/components/imageDiffView'; +import { ImageDiffView } from '@web/shared/imageDiffView'; import type { MultiTraceModel } from './modelUtil'; import { PlaceholderPanel } from './placeholderPanel'; import type { AfterActionTraceEventAttachment } from '@trace/trace'; @@ -63,7 +63,7 @@ export const AttachmentsTab: React.FunctionComponent<{ {[...diffMap.values()].map(({ expected, actual, diff }) => { return <> {expected && actual &&
Image diff
} - {expected && actual && div { - margin: 10px; - cursor: pointer; - user-select: none; -} diff --git a/packages/web/src/components/imageDiffView.tsx b/packages/web/src/components/imageDiffView.tsx deleted file mode 100644 index 525338f357..0000000000 --- a/packages/web/src/components/imageDiffView.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -import * as React from 'react'; -import './imageDiffView.css'; - -export type TestAttachment = { - name: string; - contentType: string; - path: string; -}; - -export type ImageDiff = { - name: string, - expected?: { attachment: TestAttachment, title: string }, - actual?: { attachment: TestAttachment }, - diff?: { attachment: TestAttachment }, -}; - -export const ImageDiffView: React.FunctionComponent<{ - imageDiff: ImageDiff, -}> = ({ imageDiff: diff }) => { - // Pre-select a tab called "diff", if any. - const [mode, setMode] = React.useState<'diff' | 'actual' | 'expected'>(diff.diff ? 'diff' : '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); - } - }; - - return
-
- {diff.diff &&
setMode('diff')}>Diff
} -
setMode('actual')}>Actual
-
setMode('expected')}>Expected
-
-
- {diff.diff && mode === 'diff' && onImageLoaded()} />} - {diff.diff && mode === 'actual' && - onImageLoaded('right')} imageRef={imageElement} style={{ boxShadow: 'none' }} /> - - } - {diff.diff && mode === 'expected' && - onImageLoaded('left')} imageRef={imageElement} /> - - } - {!diff.diff && mode === 'actual' && onImageLoaded()} />} - {!diff.diff && mode === 'expected' && onImageLoaded()} />} -
-
; -}; - -export const ImageDiffSlider: React.FC 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 ImageWithSize: React.FunctionComponent<{ - src: string, - onLoad?: () => void, - imageRef?: React.RefObject, - style?: React.CSSProperties, -}> = ({ src, onLoad, imageRef, style }) => { - const newRef = React.useRef(null); - const ref = imageRef ?? newRef; - const [size, setSize] = React.useState<{ width: number, height: number } | null>(null); - return
-
- { size ? size.width : ''} - x - { size ? size.height : ''} -
- { - onLoad?.(); - if (ref.current) - setSize({ width: ref.current.naturalWidth, height: ref.current.naturalHeight }); - }} ref={ref} style={style} /> -
; -}; - -const absolute: React.CSSProperties = { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, -}; diff --git a/packages/web/src/shared/DEPS.list b/packages/web/src/shared/DEPS.list new file mode 100644 index 0000000000..82081a1d2f --- /dev/null +++ b/packages/web/src/shared/DEPS.list @@ -0,0 +1,5 @@ +[*] +../uiUtils.ts + +[imageDiffView.spec.tsx] +*** diff --git a/packages/web/src/components/glassPane.tsx b/packages/web/src/shared/glassPane.tsx similarity index 97% rename from packages/web/src/components/glassPane.tsx rename to packages/web/src/shared/glassPane.tsx index 6919cb30d3..c9430826c2 100644 --- a/packages/web/src/components/glassPane.tsx +++ b/packages/web/src/shared/glassPane.tsx @@ -28,7 +28,7 @@ export const GlassPane: React.FC<{ return; const glassPaneDiv = document.createElement('div'); - glassPaneDiv.style.position = 'absolute'; + glassPaneDiv.style.position = 'fixed'; glassPaneDiv.style.top = '0'; glassPaneDiv.style.right = '0'; glassPaneDiv.style.bottom = '0'; diff --git a/packages/html-reporter/src/imageDiffView.spec.tsx b/packages/web/src/shared/imageDiffView.spec.tsx similarity index 69% rename from packages/html-reporter/src/imageDiffView.spec.tsx rename to packages/web/src/shared/imageDiffView.spec.tsx index d2339919b0..79d256096d 100644 --- a/packages/html-reporter/src/imageDiffView.spec.tsx +++ b/packages/web/src/shared/imageDiffView.spec.tsx @@ -36,7 +36,7 @@ const imageDiff: ImageDiff = { }; test('should render links', async ({ mount }) => { - const component = await mount(); + const component = await mount(); await expect(component.locator('a')).toHaveText([ 'screenshot-diff.png', 'screenshot-actual.png', @@ -44,40 +44,10 @@ test('should render links', async ({ mount }) => { ]); }); -test('should switch to actual', async ({ mount }) => { - const component = await mount(); - await component.getByText('Actual', { exact: true }).click(); - const sliderElement = component.locator('data-testid=test-result-image-mismatch-grip'); - await expect.poll(() => sliderElement.evaluate(e => e.style.left), 'Actual slider is on the right').toBe('611px'); - - const images = component.locator('img'); - const imageCount = await component.locator('img').count(); - for (let i = 0; i < imageCount; ++i) { - const image = images.nth(i); - const box = await image.boundingBox(); - expect(box).toEqual({ x: 400, y: 108, width: 200, height: 200 }); - } -}); - -test('should switch to expected', async ({ mount }) => { - const component = await mount(); - await component.getByText('Expected', { exact: true }).click(); - const sliderElement = component.locator('data-testid=test-result-image-mismatch-grip'); - await expect.poll(() => sliderElement.evaluate(e => e.style.left), 'Expected slider is on the left').toBe('371px'); - - const images = component.locator('img'); - const imageCount = await component.locator('img').count(); - for (let i = 0; i < imageCount; ++i) { - const image = images.nth(i); - const box = await image.boundingBox(); - expect(box).toEqual({ x: 400, y: 108, width: 200, height: 200 }); - } -}); - test('should show diff by default', async ({ mount }) => { - const component = await mount(); + const component = await mount(); const image = component.locator('img'); const box = await image.boundingBox(); - expect(box).toEqual({ x: 400, y: 108, width: 200, height: 200 }); + expect(box).toEqual(expect.objectContaining({ width: 48, height: 48 })); }); diff --git a/packages/web/src/shared/imageDiffView.tsx b/packages/web/src/shared/imageDiffView.tsx new file mode 100644 index 0000000000..52e7afc2fe --- /dev/null +++ b/packages/web/src/shared/imageDiffView.tsx @@ -0,0 +1,219 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as React from 'react'; +import { GlassPane } from './glassPane'; +import { useMeasure } from '../uiUtils'; + +type TestAttachment = { + name: string; + body?: string; + path?: string; + contentType: string; +}; + +export type ImageDiff = { + name: string, + expected?: { attachment: TestAttachment, title: string }, + actual?: { attachment: TestAttachment }, + diff?: { attachment: TestAttachment }, +}; + +async function loadImage(src?: string): Promise { + const image = new Image(); + if (src) { + image.src = src; + await new Promise((f, r) => { + image.onload = f; + image.onerror = f; + }); + } + return image; +} + +const checkerboardStyle: React.CSSProperties = { + backgroundImage: `linear-gradient(45deg, #80808020 25%, transparent 25%), + linear-gradient(-45deg, #80808020 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #80808020 75%), + linear-gradient(-45deg, transparent 75%, #80808020 75%)`, + backgroundSize: '20px 20px', + backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px', + boxShadow: `rgb(0 0 0 / 10%) 0px 1.8px 1.9px, + rgb(0 0 0 / 15%) 0px 6.1px 6.3px, + rgb(0 0 0 / 10%) 0px -2px 4px, + rgb(0 0 0 / 15%) 0px -6.1px 12px, + rgb(0 0 0 / 25%) 0px 6px 12px` +}; + +export const ImageDiffView: React.FC<{ + diff: ImageDiff, +}> = ({ diff }) => { + const [mode, setMode] = React.useState<'diff' | 'actual' | 'expected' | 'slider' | 'sxs'>(diff.diff ? 'diff' : 'actual'); + const [showSxsDiff, setShowSxsDiff] = React.useState(false); + + const [expectedImage, setExpectedImage] = React.useState(null); + const [actualImage, setActualImage] = React.useState(null); + const [diffImage, setDiffImage] = React.useState(null); + const [measure, ref] = useMeasure(); + + React.useEffect(() => { + (async () => { + setExpectedImage(await loadImage(diff.expected?.attachment.path)); + setActualImage(await loadImage(diff.actual?.attachment.path)); + setDiffImage(await loadImage(diff.diff?.attachment.path)); + })(); + }, [diff]); + + const isLoaded = expectedImage && actualImage && diffImage; + + const imageWidth = isLoaded ? Math.max(expectedImage.naturalWidth, actualImage.naturalWidth, 200) : 500; + const imageHeight = isLoaded ? Math.max(expectedImage.naturalHeight, actualImage.naturalHeight, 200) : 500; + const scale = Math.min(1, (measure.width - 30) / imageWidth); + const sxsScale = Math.min(1, (measure.width - 50) / imageWidth / 2); + const fitWidth = imageWidth * scale; + const fitHeight = imageHeight * scale; + + const modeStyle: React.CSSProperties = { + flex: 'none', + margin: '0 10px', + cursor: 'pointer', + userSelect: 'none', + }; + return
+ {isLoaded && <> +
+ {diff.diff &&
setMode('diff')}>Diff
} +
setMode('actual')}>Actual
+
setMode('expected')}>Expected
+
setMode('sxs')}>Side by side
+
setMode('slider')}>Slider
+
+
+ {diff.diff && mode === 'diff' && } + {diff.diff && mode === 'actual' && } + {diff.diff && mode === 'expected' && } + {diff.diff && mode === 'slider' && } + {diff.diff && mode === 'sxs' &&
+ + setShowSxsDiff(!showSxsDiff)} canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} /> +
} + {!diff.diff && mode === 'actual' && } + {!diff.diff && mode === 'expected' && } + {!diff.diff && mode === 'sxs' &&
+ + +
} +
+ + } +
; +}; + +export const ImageDiffSlider: React.FC<{ + expectedImage: HTMLImageElement, + actualImage: HTMLImageElement, + canvasWidth: number, + canvasHeight: number, + scale: number, +}> = ({ expectedImage, actualImage, canvasWidth, canvasHeight, scale }) => { + const absoluteStyle: React.CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + }; + + const [slider, setSlider] = React.useState(canvasWidth / 2); + const sameSize = expectedImage.naturalWidth === actualImage.naturalWidth && expectedImage.naturalHeight === actualImage.naturalHeight; + const [resizing, setResizing] = React.useState<{ offset: number, slider: number } | null>(null); + + return
+ setResizing(null)} + onPaneMouseMove={event => { + if (!event.buttons) { + setResizing(null); + } else if (resizing) { + const offset = event.clientX; + const delta = offset - resizing.offset; + const newSlider = resizing.slider + delta; + const slider = Math.min(Math.max(0, newSlider), canvasWidth); + setSlider(slider); + } + }} + /> +
+ {!sameSize && Expected } + {expectedImage.naturalWidth} + x + {expectedImage.naturalHeight} + {!sameSize && Actual } + {!sameSize && {actualImage.naturalWidth}} + {!sameSize && x} + {!sameSize && {actualImage.naturalHeight}} +
+
setResizing({ offset: event.clientX, slider: slider })}> + Expected +
+ Actual +
+
+ +
+
+
; +}; + +const ImageWithSize: React.FunctionComponent<{ + image: HTMLImageElement, + title?: string, + alt?: string, + canvasWidth: number, + canvasHeight: number, + scale: number, + onClick?: () => void; +}> = ({ image, title, alt, canvasWidth, canvasHeight, scale, onClick }) => { + return
+
+ {title && {title}} + {image.naturalWidth} + x + {image.naturalHeight} +
+
+ {title +
+
; +}; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index aedc1e6eb7..0225b6c0e2 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -152,32 +152,44 @@ for (const useIntermediateMergeReport of [false, true] as const) { await expect(page.locator('text=Image mismatch')).toBeVisible(); await expect(page.locator('text=Snapshot mismatch')).toHaveCount(0); - const set = new Set(); + await expect(page.getByTestId('test-result-image-mismatch-tabs').locator('div')).toHaveText([ + 'Diff', + 'Actual', + 'Expected', + 'Side by side', + 'Slider', + ]); - const imageDiff = page.locator('data-testid=test-result-image-mismatch'); - await imageDiff.locator('text="Actual"').click(); - 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); - await expect(imageDiff).toContainText('200x200'); + const imageDiff = page.getByTestId('test-result-image-mismatch'); + await test.step('Diff', async () => { + await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Diff'); + }); - const sliderElement = imageDiff.locator('data-testid=test-result-image-mismatch-grip'); - await expect.poll(() => sliderElement.evaluate(e => e.style.left), 'Actual slider is on the right').toBe('590px'); + await test.step('Actual', async () => { + await imageDiff.getByText('Actual', { exact: true }).click(); + await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Actual'); + }); - await imageDiff.locator('text="Expected"').click(); - set.add(await expectedImage.getAttribute('src')); - set.add(await actualImage.getAttribute('src')); - expect(set.size).toBe(2); + await test.step('Expected', async () => { + await imageDiff.getByText('Expected', { exact: true }).click(); + await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Expected'); + }); - await expect.poll(() => sliderElement.evaluate(e => e.style.left), 'Expected slider is on the left').toBe('350px'); + await test.step('Side by side', async () => { + await imageDiff.getByText('Side by side').click(); + await expect(imageDiff.locator('img')).toHaveCount(2); + await expect(imageDiff.locator('img').first()).toHaveAttribute('alt', 'Expected'); + await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Actual'); + await imageDiff.locator('img').last().click(); + await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Diff'); + }); - await imageDiff.locator('text="Diff"').click(); - set.add(await imageDiff.locator('img').getAttribute('src')); - expect(set.size, 'Should be three images altogether').toBe(3); + await test.step('Slider', async () => { + await imageDiff.getByText('Slider', { exact: true }).click(); + await expect(imageDiff.locator('img')).toHaveCount(2); + await expect(imageDiff.locator('img').first()).toHaveAttribute('alt', 'Expected'); + await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Actual'); + }); }); test('should include multiple image diffs', async ({ runInlineTest, page, showReport }) => { @@ -280,18 +292,13 @@ for (const useIntermediateMergeReport of [false, true] as const) { await expect(page.locator('text=Image mismatch')).toHaveCount(1); await expect(page.locator('text=Snapshot mismatch')).toHaveCount(0); await expect(page.locator('.chip-header', { hasText: 'Screenshots' })).toHaveCount(0); - const imageDiff = page.locator('data-testid=test-result-image-mismatch'); - await imageDiff.locator('text="Actual"').click(); - const image = imageDiff.locator('img'); - 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(); - await imageDiff.locator('text="Diff"').click(); - const diffSrc = await image.getAttribute('src'); - const set = new Set([previousSrc, actualSrc, diffSrc]); - expect(set.size).toBe(3); + await expect(page.getByTestId('test-result-image-mismatch-tabs').locator('div')).toHaveText([ + 'Diff', + 'Actual', + 'Expected', + 'Side by side', + 'Slider', + ]); }); test('should not include image diff with non-images', async ({ runInlineTest, page, showReport }) => { diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 74b28455b8..4e4031a9dd 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -172,7 +172,7 @@ test('should show image diff', async ({ runUITest }) => { await expect(page.getByText('Diff', { exact: true })).toBeVisible(); await expect(page.getByText('Actual', { exact: true })).toBeVisible(); await expect(page.getByText('Expected', { exact: true })).toBeVisible(); - await expect(page.locator('.image-diff-view .image-wrapper img')).toBeVisible(); + await expect(page.getByTestId('test-result-image-mismatch').locator('img')).toBeVisible(); }); test('should show screenshot', async ({ runUITest }) => {