diff --git a/packages/html-reporter/src/imageDiffView.css b/packages/html-reporter/src/imageDiffView.css
new file mode 100644
index 0000000000..ab1f467c29
--- /dev/null
+++ b/packages/html-reporter/src/imageDiffView.css
@@ -0,0 +1,30 @@
+/*
+ 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 img {
+ flex: none;
+ box-shadow: var(--box-shadow-thick);
+ margin: 24px auto;
+ min-width: 200px;
+ max-width: 80%;
+}
diff --git a/packages/html-reporter/src/imageDiffView.spec.tsx b/packages/html-reporter/src/imageDiffView.spec.tsx
new file mode 100644
index 0000000000..07cff8de71
--- /dev/null
+++ b/packages/html-reporter/src/imageDiffView.spec.tsx
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *
+ * 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 React from 'react';
+import { test, expect } from '@playwright/experimental-ct-react/test';
+import { ImageDiff, ImageDiffView } from './imageDiffView';
+
+test.use({ viewport: { width: 1000, height: 800 } });
+
+const expectedPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAACMElEQVRYw+1XT0tCQRD/9Qci0Cw7mp1C6BMYnt5niMhPEEFCh07evNk54XnuGkhFehA/QxHkqYMEFWXpscMTipri7fqeu+vbfY+EoBkQ3Zn5zTo7MzsL/NNfoClkUUQNN3jCJ/ETfavRSpYkkSmFQzz8wMr4gaSp8OBJ2HCU4Iwd0kqGgd9GPxCccZ+0jWgWVW1wxlWy0qR51I3hv7lOllq7b4SC/+aGzr+QBadjEKgAykvzJGXwr/Lj4JfRk5hUSLKIa00HPUJRki0xeMWSWxVXmi5sddXKymqTyxdwquXAUVV3WREeLx3gTcNFWQY/jXtB8QIzgt4qTvAR4OCe0ATKCmrnmFMEM0Pp2BvrIisaFUdUjgKKZgYWSjjDLR5J+x13lATHuHSti6JBzQP+gq2QHXjfRaiJojbPgYqbmGFow0VpiyIW0/VIF9QKLzeBWA2MHmwCu8QJQV++Ps/joHQQH4HpuO0uobUeVztgIcr4Vnf4we9orWfUIWKHbEVyYKkPmaVpIVKICuo0ZYXWjHTITXWhsVYxkIDpUoKsla1i2Oz2QjvYG9fshu36GbFQ8DGyHNOuvRdOKZSDUtCFM7wyHeSM4XN8e7bOpd9F2gg+TRYal753bGkbuEjzMg0YW/yDV1czUDm+e43Byz86OnRwsYDMKXlmkYbeAOwffrtU/nGpXpwkXfPhVza+D9AiMAtrtOMYfVr0q8Wr1nh8n8ADZCJPqAk8AifyjP2n36cvkA6/Wln9MokAAAAASUVORK5CYII=';
+const actualPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAACcElEQVRYw+2XQU9TQRDHfxAiYoFE8IaHhhjjN3hw4+SJk0npGU+iCQYv1kQjIQInj6+foh4amhjvEi8EI3yAIk24lBgoBkmqqYfuLvt23763r8QmJszc3sz+/29nZ3Zm4Vr+BxkgoESFPY7o0OGIPSqUCBi4OvgUmzToOLTBJlO9g08QcuEEl3pByEQv8Ascp4JLPWYhG/gQ5QjAGVWWmSXPTUa5xxxLfDJ2V2bIF36ELW3hASuMxfqNsiSOvatbjPj9fU0tabPOjUTvHG/4pfxrPrvQg7PqteNA20c5zbkYiWubgmZbZJcTzvjKGneMZP6m1hST4CdpGvmhU0zzXX0/5VFk5V21iyaTboJQwa7STqH4Y1AE6ixCd9XKxHsHFFIpTo1AvVal56juDeFQZxi8KNaMjJJh2oiDH+RQmFfUtzSKXQPjifh+yGBcsnWNrUhZJVO0DIxxFeTAJigJU9X4nkRxYqF8FL4lm6AiTMuW5bMzaXcs36fCs2IT7AvTjHNvNsVjy3dO+O3bBLLE8jF9oemsblPuq3KzRB7PrZhl8/z2pBhTteAkyMUunI+0HzdFzk0gwzDtvKde8SU2o3TJu0MkD3k28bYtpFDMuA/ZnaZZKJ6707SkuhJXoKi6Cy1QDT7XM8U4LfdVEXfZSXnAMz6wTZ1zzqmzrVWGTvEi6bK7vK4bWqO/zUtF7FJJMcxB0nWtN5y3omje89Nr8OpSrKc1HL1lBjzUGosPxWWTDX2a/o8M4FFNbPrm2NKLFrMMXtk1dfCKjo5ZteY3AEeHX3/1HH7jxne/4HiP7314gPTlCdWHR2BfnrHX8u/lL/ENCdIFFeD3AAAAAElFTkSuQmCC';
+
+const actualAttachment = { name: 'screenshot-actual.png', path: actualPng, contentType: 'image/png', };
+const expectedAttachment = { name: 'screenshot-expected.png', path: expectedPng, contentType: 'image/png', };
+const diffAttachment = { name: 'screenshot-diff.png', path: expectedPng, contentType: 'image/png', };
+
+const imageDiff: ImageDiff = {
+ name: 'log in',
+ actual: { attachment: actualAttachment },
+ expected: { attachment: expectedAttachment, title: 'Expected' },
+ diff: { attachment: diffAttachment },
+};
+
+test('should render links', async ({ mount }) => {
+ const component = await mount();
+ await expect(component.locator('a')).toHaveText([
+ 'screenshot-actual.png',
+ 'screenshot-expected.png',
+ 'screenshot-diff.png',
+ ]);
+});
+
+test('should show actual by default', async ({ mount }) => {
+ const component = await mount();
+ 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: 80, width: 200, height: 200 });
+ }
+});
+
+test('should switch to expected', async ({ mount }) => {
+ const component = await mount();
+ await component.locator('text="Expected"').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: 80, width: 200, height: 200 });
+ }
+});
+
+test('should switch to diff', async ({ mount }) => {
+ const component = await mount();
+ await component.locator('text="Diff"').click();
+
+ const image = component.locator('img');
+ const box = await image.boundingBox();
+ expect(box).toEqual({ x: 400, y: 80, width: 200, height: 200 });
+});
diff --git a/packages/html-reporter/src/imageDiffView.tsx b/packages/html-reporter/src/imageDiffView.tsx
index b6e036e42b..054b15e00e 100644
--- a/packages/html-reporter/src/imageDiffView.tsx
+++ b/packages/html-reporter/src/imageDiffView.tsx
@@ -18,12 +18,14 @@ import type { TestAttachment } from '@playwright-test/reporters/html';
import * as React from 'react';
import { AttachmentLink } from './links';
import { TabbedPane, TabbedPaneTab } from './tabbedPane';
+import './imageDiffView.css';
+import './tabbedPane.css';
export type ImageDiff = {
name: string,
- left?: { attachment: TestAttachment, title: string },
- right?: { attachment: TestAttachment, title: string },
- diff?: { attachment: TestAttachment, title: string },
+ expected?: { attachment: TestAttachment, title: string },
+ actual?: { attachment: TestAttachment },
+ diff?: { attachment: TestAttachment },
};
export const ImageDiffView: React.FunctionComponent<{
@@ -51,28 +53,28 @@ export const ImageDiffView: React.FunctionComponent<{
id: 'actual',
title: 'Actual',
render: () =>
-
onImageLoaded('right')} ref={imageElement} />
-
+
onImageLoaded('right')} ref={imageElement} />
+
,
});
tabs.push({
id: 'expected',
- title: diff.right!.title,
+ title: diff.expected!.title,
render: () =>
-
onImageLoaded('left')} ref={imageElement} />
-
+
onImageLoaded('left')} ref={imageElement} />
+
,
});
} else {
tabs.push({
id: 'actual',
title: 'Actual',
- render: () =>
onImageLoaded()} />
+ render: () =>
onImageLoaded()} />
});
tabs.push({
id: 'expected',
- title: diff.right!.title,
- render: () =>
onImageLoaded()} />
+ title: diff.expected!.title,
+ render: () =>
onImageLoaded()} />
});
}
if (diff.diff) {
@@ -82,10 +84,10 @@ export const ImageDiffView: React.FunctionComponent<{
render: () =>
onImageLoaded()} />
});
}
- return
+ return
;
};
@@ -112,7 +114,13 @@ export const ImageDiffSlider: React.FC<{
return <>
{childrenArray[0]}
-
+
{childrenArray[1]}
div {
flex: none;
}
diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx
index 88fed67144..78cc30508d 100644
--- a/packages/html-reporter/src/testResultView.tsx
+++ b/packages/html-reporter/src/testResultView.tsx
@@ -40,20 +40,20 @@ function groupImageDiffs(screenshots: Set): ImageDiff[] {
snapshotNameToImageDiff.set(snapshotName, imageDiff);
}
if (category === 'actual')
- imageDiff.left = { attachment, title: 'Actual' };
+ imageDiff.actual = { attachment };
if (category === 'expected')
- imageDiff.right = { attachment, title: 'Expected' };
+ imageDiff.expected = { attachment, title: 'Expected' };
if (category === 'previous')
- imageDiff.right = { attachment, title: 'Previous' };
+ imageDiff.expected = { attachment, title: 'Previous' };
if (category === 'diff')
- imageDiff.diff = { attachment, title: 'Diff' };
+ imageDiff.diff = { attachment };
}
for (const [name, diff] of snapshotNameToImageDiff) {
- if (!diff.left || !diff.right) {
+ if (!diff.actual || !diff.expected) {
snapshotNameToImageDiff.delete(name);
} else {
- screenshots.delete(diff.left.attachment);
- screenshots.delete(diff.right.attachment);
+ screenshots.delete(diff.actual.attachment);
+ screenshots.delete(diff.expected.attachment);
screenshots.delete(diff.diff?.attachment!);
}
}
diff --git a/packages/html-reporter/src/tests.ts b/packages/html-reporter/src/tests.ts
index aa05b5c62a..491a438ee2 100644
--- a/packages/html-reporter/src/tests.ts
+++ b/packages/html-reporter/src/tests.ts
@@ -16,6 +16,7 @@
import { AutoChip, Chip } from './chip';
import { HeaderView } from './headerView';
+import { ImageDiffView } from './imageDiffView';
import { TestCaseView } from './testCaseView';
import './theme.css';
@@ -25,5 +26,6 @@ register({
AutoChip,
Chip,
HeaderView,
+ ImageDiffView,
TestCaseView,
});
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index 2575b729cc..4929bcb74b 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -158,18 +158,14 @@ test('should include image diff', async ({ runInlineTest, page, showReport }) =>
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 expect.poll(() => sliderElement.evaluate(e => e.style.left), 'Actual slider is on the right').toBe('590px');
await imageDiff.locator('text="Expected"').click();
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 expect.poll(() => sliderElement.evaluate(e => e.style.left), 'Expected slider is on the left').toBe('350px');
await imageDiff.locator('text="Diff"').click();
set.add(await imageDiff.locator('img').getAttribute('src'));