mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(html): render image diff slider (#13257)
This commit is contained in:
parent
f9ae423eab
commit
55ee41c848
@ -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<string>('left');
|
||||
const diffElement = React.useRef<HTMLImageElement>(null);
|
||||
const setMinHeight = () => {
|
||||
const [selectedTab, setSelectedTab] = React.useState<string>('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);
|
||||
}
|
||||
};
|
||||
const tabs = [
|
||||
{
|
||||
id: 'left',
|
||||
title: diff.left!.title,
|
||||
render: () => <img src={diff.left!.attachment.path!} onLoad={setMinHeight}/>
|
||||
},
|
||||
{
|
||||
id: 'right',
|
||||
const tabs: TabbedPaneTab[] = [];
|
||||
if (diff.diff) {
|
||||
tabs.push({
|
||||
id: 'actual',
|
||||
title: 'Actual',
|
||||
render: () => <ImageDiffSlider sliderPosition={sliderPosition} setSliderPosition={setSliderPosition}>
|
||||
<img src={diff.left!.attachment.path!} onLoad={() => onImageLoaded('right')} ref={imageElement} />
|
||||
<img src={diff.right!.attachment.path!} style={{ boxShadow: 'none' }} />
|
||||
</ImageDiffSlider>,
|
||||
});
|
||||
tabs.push({
|
||||
id: 'expected',
|
||||
title: diff.right!.title,
|
||||
render: () => <img src={diff.right!.attachment.path!} onLoad={setMinHeight}/>
|
||||
},
|
||||
];
|
||||
render: () => <ImageDiffSlider sliderPosition={sliderPosition} setSliderPosition={setSliderPosition}>
|
||||
<img src={diff.left!.attachment.path!} onLoad={() => onImageLoaded('left')} ref={imageElement} />
|
||||
<img src={diff.right!.attachment.path!} style={{ boxShadow: 'none' }} />
|
||||
</ImageDiffSlider>,
|
||||
});
|
||||
} else {
|
||||
tabs.push({
|
||||
id: 'actual',
|
||||
title: 'Actual',
|
||||
render: () => <img src={diff.left!.attachment.path!} onLoad={() => onImageLoaded()} />
|
||||
});
|
||||
tabs.push({
|
||||
id: 'expected',
|
||||
title: diff.right!.title,
|
||||
render: () => <img src={diff.right!.attachment.path!} onLoad={() => onImageLoaded()} />
|
||||
});
|
||||
}
|
||||
if (diff.diff) {
|
||||
tabs.push({
|
||||
id: 'diff',
|
||||
title: diff.diff.title,
|
||||
render: () => <img src={diff.diff!.attachment.path} onLoad={setMinHeight}/>
|
||||
title: 'Diff',
|
||||
render: () => <img src={diff.diff!.attachment.path} onLoad={() => onImageLoaded()} />
|
||||
});
|
||||
}
|
||||
return <div className='vbox' data-testid='test-result-image-mismatch' ref={diffElement}>
|
||||
@ -63,3 +89,79 @@ export const ImageDiffView: React.FunctionComponent<{
|
||||
{diff.diff && <AttachmentLink attachment={diff.diff.attachment}></AttachmentLink>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
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]}
|
||||
<div style={{ ...absolute }}>
|
||||
<div style={{ ...absolute, display: 'flex', zIndex: 50, clip: `rect(0, ${size}px, auto, 0)` }}>
|
||||
{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 absolute: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.test-result > div {
|
||||
|
||||
@ -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]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user