mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
chore: reuse image diff component in trace/html (#28727)
Fixes https://github.com/microsoft/playwright/issues/28685
This commit is contained in:
parent
ff99aa33b0
commit
f5c57d0e98
@ -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: [{
|
||||
|
@ -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;
|
||||
}
|
@ -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<string>('diff');
|
||||
const diffElement = React.useRef<HTMLDivElement>(null);
|
||||
const imageElement = React.useRef<HTMLImageElement>(null);
|
||||
const [sliderPosition, setSliderPosition] = React.useState<number>(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: () => <ImageWithSize src={diff.diff!.attachment.path!} onLoad={() => onImageLoaded()} />
|
||||
});
|
||||
tabs.push({
|
||||
id: 'actual',
|
||||
title: 'Actual',
|
||||
render: () => <ImageDiffSlider sliderPosition={sliderPosition} setSliderPosition={setSliderPosition}>
|
||||
<ImageWithSize src={diff.expected!.attachment.path!} onLoad={() => onImageLoaded('right')} imageRef={imageElement} style={{ boxShadow: 'none' }} />
|
||||
<ImageWithSize src={diff.actual!.attachment.path!} />
|
||||
</ImageDiffSlider>,
|
||||
});
|
||||
tabs.push({
|
||||
id: 'expected',
|
||||
title: diff.expected!.title,
|
||||
render: () => <ImageDiffSlider sliderPosition={sliderPosition} setSliderPosition={setSliderPosition}>
|
||||
<ImageWithSize src={diff.expected!.attachment.path!} onLoad={() => onImageLoaded('left')} imageRef={imageElement} />
|
||||
<ImageWithSize src={diff.actual!.attachment.path!} style={{ boxShadow: 'none' }} />
|
||||
</ImageDiffSlider>,
|
||||
});
|
||||
} else {
|
||||
tabs.push({
|
||||
id: 'actual',
|
||||
title: 'Actual',
|
||||
render: () => <ImageWithSize src={diff.actual!.attachment.path!} onLoad={() => onImageLoaded()} />
|
||||
});
|
||||
tabs.push({
|
||||
id: 'expected',
|
||||
title: diff.expected!.title,
|
||||
render: () => <ImageWithSize src={diff.expected!.attachment.path!} onLoad={() => onImageLoaded()} />
|
||||
});
|
||||
}
|
||||
return <div className='vbox image-diff-view' data-testid='test-result-image-mismatch' ref={diffElement}>
|
||||
<TabbedPane tabs={tabs} selectedTab={selectedTab} setSelectedTab={setSelectedTab} />
|
||||
{diff.diff && <AttachmentLink attachment={diff.diff.attachment}></AttachmentLink>}
|
||||
<AttachmentLink attachment={diff.actual!.attachment}></AttachmentLink>
|
||||
<AttachmentLink attachment={diff.expected!.attachment}></AttachmentLink>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const ImageDiffSlider: React.FC<React.PropsWithChildren<{
|
||||
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]}
|
||||
<div style={{ ...absolute }}>
|
||||
<div style={{
|
||||
...absolute,
|
||||
display: 'flex',
|
||||
zIndex: 50,
|
||||
clip: `rect(0, ${size}px, auto, 0)`,
|
||||
backgroundColor: 'var(--color-canvas-default)',
|
||||
}}>
|
||||
{childrenArray[1]}
|
||||
</div>
|
||||
<div
|
||||
style={gripStyle}
|
||||
onMouseDown={event => 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);
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
<div data-testid='test-result-image-mismatch-grip' style={{
|
||||
...absolute,
|
||||
left: size - 1,
|
||||
width: 20,
|
||||
zIndex: 80,
|
||||
margin: '10px -10px',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 9,
|
||||
width: 2,
|
||||
backgroundColor: 'var(--color-diff-blob-expander-icon)',
|
||||
}}>
|
||||
</div>
|
||||
<svg style={{ fill: 'var(--color-diff-blob-expander-icon)' }} viewBox="0 0 27 20"><path d="M9.6 0L0 9.6l9.6 9.6z"></path><path d="M17 19.2l9.5-9.6L16.9 0z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
const ImageWithSize: React.FunctionComponent<{
|
||||
src: string,
|
||||
onLoad?: () => void,
|
||||
imageRef?: React.RefObject<HTMLImageElement>,
|
||||
style?: React.CSSProperties,
|
||||
}> = ({ src, onLoad, imageRef, style }) => {
|
||||
const newRef = React.useRef<HTMLImageElement>(null);
|
||||
const ref = imageRef ?? newRef;
|
||||
const [size, setSize] = React.useState<{ width: number, height: number } | null>(null);
|
||||
return <div className='image-wrapper'>
|
||||
<div>
|
||||
<span style={{ flex: '1 1 0', textAlign: 'end' }}>{ size ? size.width : ''}</span>
|
||||
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
||||
<span style={{ flex: '1 1 0', textAlign: 'start' }}>{ size ? size.height : ''}</span>
|
||||
</div>
|
||||
<img src={src} onLoad={() => {
|
||||
onLoad?.();
|
||||
if (ref.current)
|
||||
setSize({ width: ref.current.naturalWidth, height: ref.current.naturalHeight });
|
||||
}} ref={ref} style={style} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const absolute: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
};
|
@ -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;
|
||||
|
@ -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) =>
|
||||
<AutoChip key={`diff-${index}`} header={`Image mismatch: ${diff.name}`} targetRef={imageDiffRef}>
|
||||
<ImageDiffView key='image-diff' imageDiff={diff}></ImageDiffView>
|
||||
<ImageDiffView key='image-diff' diff={diff}></ImageDiffView>
|
||||
</AutoChip>
|
||||
)}
|
||||
|
||||
@ -110,7 +110,7 @@ export const TestResultView: React.FC<{
|
||||
{screenshots.map((a, i) => {
|
||||
return <div key={`screenshot-${i}`}>
|
||||
<a href={a.path}>
|
||||
<img src={a.path} />
|
||||
<img className='screenshot' src={a.path} />
|
||||
</a>
|
||||
<AttachmentLink attachment={a}></AttachmentLink>
|
||||
</div>;
|
||||
@ -120,7 +120,7 @@ export const TestResultView: React.FC<{
|
||||
{!!traces.length && <AutoChip header='Traces'>
|
||||
{<div>
|
||||
<a href={generateTraceUrl(traces)}>
|
||||
<img src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
||||
<img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
||||
</a>
|
||||
{traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)}
|
||||
</div>}
|
||||
|
@ -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 && <div className='attachments-section'>Image diff</div>}
|
||||
{expected && actual && <ImageDiffView imageDiff={{
|
||||
{expected && actual && <ImageDiffView diff={{
|
||||
name: 'Image diff',
|
||||
expected: { attachment: { ...expected, path: attachmentURL(expected) }, title: 'Expected' },
|
||||
actual: { attachment: { ...actual, path: attachmentURL(actual) } },
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { msToString, useMeasure } from '@web/uiUtils';
|
||||
import { GlassPane } from '@web/components/glassPane';
|
||||
import { GlassPane } from '@web/shared/glassPane';
|
||||
import * as React from 'react';
|
||||
import type { Boundaries } from '../geometry';
|
||||
import { FilmStrip } from './filmStrip';
|
||||
|
@ -1,46 +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 .image-wrapper img {
|
||||
flex: auto;
|
||||
box-shadow: var(--box-shadow-thick);
|
||||
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;
|
||||
}
|
||||
|
||||
.image-diff-view .modes > div {
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
@ -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<HTMLDivElement>(null);
|
||||
const imageElement = React.useRef<HTMLImageElement>(null);
|
||||
const [sliderPosition, setSliderPosition] = React.useState<number>(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 <div className='vbox image-diff-view'>
|
||||
<div className='hbox modes'>
|
||||
{diff.diff && <div onClick={() => setMode('diff')}>Diff</div>}
|
||||
<div onClick={() => setMode('actual')}>Actual</div>
|
||||
<div onClick={() => setMode('expected')}>Expected</div>
|
||||
</div>
|
||||
<div style={{ position: 'relative' }} ref={diffElement}>
|
||||
{diff.diff && mode === 'diff' && <ImageWithSize src={diff.diff!.attachment.path!} onLoad={() => onImageLoaded()} />}
|
||||
{diff.diff && mode === 'actual' && <ImageDiffSlider sliderPosition={sliderPosition} setSliderPosition={setSliderPosition}>
|
||||
<ImageWithSize src={diff.expected!.attachment.path!} onLoad={() => onImageLoaded('right')} imageRef={imageElement} style={{ boxShadow: 'none' }} />
|
||||
<ImageWithSize src={diff.actual!.attachment.path!} />
|
||||
</ImageDiffSlider>}
|
||||
{diff.diff && mode === 'expected' && <ImageDiffSlider sliderPosition={sliderPosition} setSliderPosition={setSliderPosition}>
|
||||
<ImageWithSize src={diff.expected!.attachment.path!} onLoad={() => onImageLoaded('left')} imageRef={imageElement} />
|
||||
<ImageWithSize src={diff.actual!.attachment.path!} style={{ boxShadow: 'none' }} />
|
||||
</ImageDiffSlider>}
|
||||
{!diff.diff && mode === 'actual' && <ImageWithSize src={diff.actual!.attachment.path!} onLoad={() => onImageLoaded()} />}
|
||||
{!diff.diff && mode === 'expected' && <ImageWithSize src={diff.expected!.attachment.path!} onLoad={() => onImageLoaded()} />}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const ImageDiffSlider: React.FC<React.PropsWithChildren<{
|
||||
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]}
|
||||
<div style={{ ...absolute }}>
|
||||
<div style={{
|
||||
...absolute,
|
||||
display: 'flex',
|
||||
zIndex: 50,
|
||||
clip: `rect(0, ${size}px, auto, 0)`,
|
||||
backgroundColor: 'var(--vscode-panel-background)',
|
||||
}}>
|
||||
{childrenArray[1]}
|
||||
</div>
|
||||
<div
|
||||
style={gripStyle}
|
||||
onMouseDown={event => 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);
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
<div data-testid='test-result-image-mismatch-grip' style={{
|
||||
...absolute,
|
||||
left: size - 1,
|
||||
width: 20,
|
||||
zIndex: 80,
|
||||
margin: '10px -10px',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 9,
|
||||
width: 2,
|
||||
backgroundColor: 'var(--vscode-panel-border)',
|
||||
}}>
|
||||
</div>
|
||||
<svg style={{ fill: 'var(--vscode-panel-border)' }} viewBox="0 0 27 20"><path d="M9.6 0L0 9.6l9.6 9.6z"></path><path d="M17 19.2l9.5-9.6L16.9 0z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
const ImageWithSize: React.FunctionComponent<{
|
||||
src: string,
|
||||
onLoad?: () => void,
|
||||
imageRef?: React.RefObject<HTMLImageElement>,
|
||||
style?: React.CSSProperties,
|
||||
}> = ({ src, onLoad, imageRef, style }) => {
|
||||
const newRef = React.useRef<HTMLImageElement>(null);
|
||||
const ref = imageRef ?? newRef;
|
||||
const [size, setSize] = React.useState<{ width: number, height: number } | null>(null);
|
||||
return <div className='image-wrapper'>
|
||||
<div>
|
||||
<span style={{ flex: '1 1 0', textAlign: 'end' }}>{ size ? size.width : ''}</span>
|
||||
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
||||
<span style={{ flex: '1 1 0', textAlign: 'start' }}>{ size ? size.height : ''}</span>
|
||||
</div>
|
||||
<img draggable='false' src={src} onLoad={() => {
|
||||
onLoad?.();
|
||||
if (ref.current)
|
||||
setSize({ width: ref.current.naturalWidth, height: ref.current.naturalHeight });
|
||||
}} ref={ref} style={style} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const absolute: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
};
|
5
packages/web/src/shared/DEPS.list
Normal file
5
packages/web/src/shared/DEPS.list
Normal file
@ -0,0 +1,5 @@
|
||||
[*]
|
||||
../uiUtils.ts
|
||||
|
||||
[imageDiffView.spec.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';
|
@ -36,7 +36,7 @@ const imageDiff: ImageDiff = {
|
||||
};
|
||||
|
||||
test('should render links', async ({ mount }) => {
|
||||
const component = await mount(<ImageDiffView key='image-diff' imageDiff={imageDiff}></ImageDiffView>);
|
||||
const component = await mount(<ImageDiffView key='image-diff' diff={imageDiff}></ImageDiffView>);
|
||||
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(<ImageDiffView key='image-diff' imageDiff={imageDiff}></ImageDiffView>);
|
||||
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(<ImageDiffView key='image-diff' imageDiff={imageDiff}></ImageDiffView>);
|
||||
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(<ImageDiffView key='image-diff' imageDiff={imageDiff}></ImageDiffView>);
|
||||
const component = await mount(<ImageDiffView key='image-diff' diff={imageDiff}></ImageDiffView>);
|
||||
|
||||
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 }));
|
||||
});
|
219
packages/web/src/shared/imageDiffView.tsx
Normal file
219
packages/web/src/shared/imageDiffView.tsx
Normal file
@ -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<HTMLImageElement> {
|
||||
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<boolean>(false);
|
||||
|
||||
const [expectedImage, setExpectedImage] = React.useState<HTMLImageElement | null>(null);
|
||||
const [actualImage, setActualImage] = React.useState<HTMLImageElement | null>(null);
|
||||
const [diffImage, setDiffImage] = React.useState<HTMLImageElement | null>(null);
|
||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||
|
||||
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 <div data-testid='test-result-image-mismatch' style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flex: 'auto' }} ref={ref}>
|
||||
{isLoaded && <>
|
||||
<div data-testid='test-result-image-mismatch-tabs' style={{ display: 'flex', margin: '10px 0 20px' }}>
|
||||
{diff.diff && <div style={{ ...modeStyle, fontWeight: mode === 'diff' ? 600 : 'initial' }} onClick={() => setMode('diff')}>Diff</div>}
|
||||
<div style={{ ...modeStyle, fontWeight: mode === 'actual' ? 600 : 'initial' }} onClick={() => setMode('actual')}>Actual</div>
|
||||
<div style={{ ...modeStyle, fontWeight: mode === 'expected' ? 600 : 'initial' }} onClick={() => setMode('expected')}>Expected</div>
|
||||
<div style={{ ...modeStyle, fontWeight: mode === 'sxs' ? 600 : 'initial' }} onClick={() => setMode('sxs')}>Side by side</div>
|
||||
<div style={{ ...modeStyle, fontWeight: mode === 'slider' ? 600 : 'initial' }} onClick={() => setMode('slider')}>Slider</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', flex: 'auto', minHeight: fitHeight + 60 }}>
|
||||
{diff.diff && mode === 'diff' && <ImageWithSize image={diffImage} alt='Diff' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{diff.diff && mode === 'actual' && <ImageWithSize image={actualImage} alt='Actual' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{diff.diff && mode === 'expected' && <ImageWithSize image={expectedImage} alt='Expected' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{diff.diff && mode === 'slider' && <ImageDiffSlider expectedImage={expectedImage} actualImage={actualImage} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale} />}
|
||||
{diff.diff && mode === 'sxs' && <div style={{ display: 'flex' }}>
|
||||
<ImageWithSize image={expectedImage} title='Expected' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||
<ImageWithSize image={showSxsDiff ? diffImage : actualImage} title={showSxsDiff ? 'Diff' : 'Actual'} onClick={() => setShowSxsDiff(!showSxsDiff)} canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||
</div>}
|
||||
{!diff.diff && mode === 'actual' && <ImageWithSize image={actualImage} title='Actual' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{!diff.diff && mode === 'expected' && <ImageWithSize image={expectedImage} title='Expected' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{!diff.diff && mode === 'sxs' && <div style={{ display: 'flex' }}>
|
||||
<ImageWithSize image={expectedImage} title='Expected' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||
<ImageWithSize image={actualImage} title='Actual' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||
</div>}
|
||||
</div>
|
||||
<div style={{ alignSelf: 'start', lineHeight: '18px' }}>
|
||||
<div>{diff.diff && <a target='_blank' href={diff.diff.attachment.path}>{diff.diff.attachment.name}</a>}</div>
|
||||
<div><a target='_blank' href={diff.actual!.attachment.path}>{diff.actual!.attachment.name}</a></div>
|
||||
<div><a target='_blank' href={diff.expected!.attachment.path}>{diff.expected!.attachment.name}</a></div>
|
||||
</div>
|
||||
</>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
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<number>(canvasWidth / 2);
|
||||
const sameSize = expectedImage.naturalWidth === actualImage.naturalWidth && expectedImage.naturalHeight === actualImage.naturalHeight;
|
||||
const [resizing, setResizing] = React.useState<{ offset: number, slider: number } | null>(null);
|
||||
|
||||
return <div style={{ flex: 'none', display: 'flex', alignItems: 'center', flexDirection: 'column', userSelect: 'none' }}>
|
||||
<GlassPane
|
||||
enabled={!!resizing}
|
||||
cursor={'ew-resize'}
|
||||
onPaneMouseUp={() => 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div style={{ margin: 5 }}>
|
||||
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>Expected </span>}
|
||||
<span>{expectedImage.naturalWidth}</span>
|
||||
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
||||
<span>{expectedImage.naturalHeight}</span>
|
||||
{!sameSize && <span style={{ flex: 'none', margin: '0 5px 0 15px' }}>Actual </span>}
|
||||
{!sameSize && <span>{actualImage.naturalWidth}</span>}
|
||||
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>x</span>}
|
||||
{!sameSize && <span>{actualImage.naturalHeight}</span>}
|
||||
</div>
|
||||
<div style={{ position: 'relative', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}
|
||||
onMouseDown={event => setResizing({ offset: event.clientX, slider: slider })}>
|
||||
<img alt='Expected' style={{
|
||||
width: expectedImage.naturalWidth * scale,
|
||||
height: expectedImage.naturalHeight * scale,
|
||||
}} draggable='false' src={expectedImage.src} />
|
||||
<div style={{ ...absoluteStyle, bottom: 0, overflow: 'hidden', width: slider, ...checkerboardStyle }}>
|
||||
<img alt='Actual' style={{
|
||||
width: actualImage.naturalWidth * scale,
|
||||
height: actualImage.naturalHeight * scale,
|
||||
}} draggable='false' src={actualImage.src} />
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 0, bottom: 0, left: slider, width: 6, background: '#57606a80', cursor: 'ew-resize', overflow: 'visible', display: 'flex', alignItems: 'center' }}>
|
||||
<svg style={{ fill: '#57606a80', width: 30, flex: 'none', marginLeft: -12, pointerEvents: 'none' }} viewBox="0 0 27 20"><path d="M9.6 0L0 9.6l9.6 9.6z"></path><path d="M17 19.2l9.5-9.6L16.9 0z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
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 <div style={{ flex: 'none', display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
||||
<div style={{ margin: 5 }}>
|
||||
{title && <span style={{ flex: 'none', margin: '0 5px' }}>{title}</span>}
|
||||
<span>{image.naturalWidth}</span>
|
||||
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
||||
<span>{image.naturalHeight}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flex: 'none', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}>
|
||||
<img
|
||||
width={image.naturalWidth * scale}
|
||||
height={image.naturalHeight * scale}
|
||||
alt={title || alt}
|
||||
style={{ cursor: onClick ? 'pointer' : 'initial' }}
|
||||
draggable='false'
|
||||
src={image.src}
|
||||
onClick={onClick} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
@ -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 }) => {
|
||||
|
@ -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 }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user