feat(html): render image diff slider (#13257)

This commit is contained in:
Pavel Feldman 2022-04-01 14:27:51 -08:00 committed by GitHub
parent f9ae423eab
commit 55ee41c848
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 35 deletions

View File

@ -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,
};

View File

@ -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;

View File

@ -25,6 +25,7 @@
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.test-result > div {

View File

@ -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]);