,
+ targetRef?: React.RefCallback
,
}> = ({ header, initialExpanded, noInsets, children, dataTestId, targetRef }) => {
const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined);
return = ({ attachment, href, linkName, openInNewTab }) => {
- return
+ targetRef?: React.RefCallback,
+}> = ({ attachment, href, targetRef, linkName, openInNewTab }) => {
+ return
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
{attachment.path && {linkName || attachment.name}}
{!attachment.path && (
diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx
index 796c03d6a1..065ced4db1 100644
--- a/packages/html-reporter/src/reportView.tsx
+++ b/packages/html-reporter/src/reportView.tsx
@@ -76,7 +76,7 @@ const TestCaseViewLoader: React.FC<{
const searchParams = new URLSearchParams(window.location.hash.slice(1));
const [test, setTest] = React.useState();
const testId = searchParams.get('testId');
- const anchor = (searchParams.get('anchor') || '') as 'video' | 'diff' | '';
+ const anchor = searchParams.has('anchor') ? Number(searchParams.get('anchor')!) : undefined;
const run = +(searchParams.get('run') || '0');
const testIdToFileIdMap = React.useMemo(() => {
diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx
index 7c9c99eeb3..32b301926c 100644
--- a/packages/html-reporter/src/testCaseView.spec.tsx
+++ b/packages/html-reporter/src/testCaseView.spec.tsx
@@ -63,7 +63,7 @@ const testCase: TestCase = {
};
test('should render test case', async ({ mount }) => {
- const component = await mount();
+ const component = await mount();
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
await expect(component.getByText('Hidden annotation')).toBeHidden();
await component.getByText('Annotations').click();
@@ -79,7 +79,7 @@ test('should render test case', async ({ mount }) => {
test('should render copy buttons for annotations', async ({ mount, page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
- const component = await mount();
+ const component = await mount();
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
await component.getByText('Annotation text', { exact: false }).first().hover();
await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible();
@@ -108,7 +108,7 @@ const annotationLinkRenderingTestCase: TestCase = {
};
test('should correctly render links in annotations', async ({ mount }) => {
- const component = await mount();
+ const component = await mount();
const firstLink = await component.getByText('https://playwright.dev/docs/intro').first();
await expect(firstLink).toBeVisible();
@@ -166,7 +166,7 @@ const attachmentLinkRenderingTestCase: TestCase = {
};
test('should correctly render links in attachments', async ({ mount }) => {
- const component = await mount();
+ const component = await mount();
await component.getByText('first attachment').click();
const body = await component.getByText('The body with https://playwright.dev/docs/intro link');
await expect(body).toBeVisible();
@@ -175,7 +175,7 @@ test('should correctly render links in attachments', async ({ mount }) => {
});
test('should correctly render links in attachment name', async ({ mount }) => {
- const component = await mount();
+ const component = await mount();
const link = component.getByText('attachment with inline link').locator('a');
await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284');
diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx
index e3656bf414..7c27c741a5 100644
--- a/packages/html-reporter/src/testCaseView.tsx
+++ b/packages/html-reporter/src/testCaseView.tsx
@@ -31,7 +31,7 @@ import { CopyToClipboardContainer } from './copyToClipboard';
export const TestCaseView: React.FC<{
projectNames: string[],
test: TestCase | undefined,
- anchor: 'video' | 'diff' | '',
+ anchor?: number,
run: number,
}> = ({ projectNames, test, run, anchor }) => {
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
@@ -69,7 +69,7 @@ export const TestCaseView: React.FC<{
test.results.map((result, index) => ({
id: String(index),
title: {statusIcon(result.status)} {retryLabel(index)}
,
- render: () =>
+ render: () =>
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
;
};
diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx
index bd402a74de..c3bfeeecce 100644
--- a/packages/html-reporter/src/testFileView.tsx
+++ b/packages/html-reporter/src/testFileView.tsx
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
+import type { HTMLReport, TestAttachment, TestCaseSummary, TestFileSummary, TestResultSummary } from './types';
import * as React from 'react';
import { hashStringToInt, msToString } from './utils';
import { Chip } from './chip';
@@ -70,16 +70,25 @@ export const TestFileView: React.FC
;
};
-function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
- const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => {
- return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/);
+function findResultWithAttachment(test: TestCaseSummary, predicate: (attachment: TestAttachment) => boolean): { index: number, anchor: number } | undefined {
+ let anchor = 0;
+ const index = test.results.findIndex(result => result.attachments.some((attachment, i) => {
+ if (predicate(attachment)) {
+ anchor = i;
+ return true;
+ }
}));
- return resultWithImageDiff ? {image()} : undefined;
+ return index === -1 ? undefined : { index, anchor };
+}
+
+function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
+ const resultWithImageDiff = findResultWithAttachment(test, attachment => attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/));
+ return resultWithImageDiff ? {image()} : undefined;
}
function videoBadge(test: TestCaseSummary): JSX.Element | undefined {
- const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video'));
- return resultWithVideo ? {video()} : undefined;
+ const resultWithVideo = findResultWithAttachment(test, attachment => attachment.name === 'video');
+ return resultWithVideo ? {video()} : undefined;
}
function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx
index bb18422dd0..21c1c7c0a6 100644
--- a/packages/html-reporter/src/testResultView.tsx
+++ b/packages/html-reporter/src/testResultView.tsx
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import type { TestAttachment, TestCase, TestResult, TestStep } from './types';
+import type { TestAttachment, TestResult, TestStep } from './types';
import * as React from 'react';
import { TreeItem } from './treeItem';
import { msToString } from './utils';
@@ -61,10 +61,24 @@ function groupImageDiffs(screenshots: Set): ImageDiff[] {
return [...snapshotNameToImageDiff.values()];
}
+function getDiffAttachmentRepresentative(diff: ImageDiff, result: TestResult): TestAttachment {
+ let representative: TestAttachment | undefined = undefined;
+ let min = Number.MAX_VALUE;
+ for (const attachment of [diff.actual?.attachment, diff.diff?.attachment, diff.expected?.attachment]) {
+ if (!attachment)
+ continue;
+ const index = result.attachments.indexOf(attachment);
+ if (index < min) {
+ min = index;
+ representative = attachment;
+ }
+ }
+ return representative!;
+}
+
export const TestResultView: React.FC<{
- test: TestCase,
result: TestResult,
- anchor: 'video' | 'diff' | '',
+ anchor?: number,
}> = ({ result, anchor }) => {
const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => {
@@ -80,19 +94,24 @@ export const TestResultView: React.FC<{
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls };
}, [result]);
- const videoRef = React.useRef(null);
- const imageDiffRef = React.useRef(null);
-
- const [scrolled, setScrolled] = React.useState(false);
+ const attachmentRefs = React.useRef([]);
React.useEffect(() => {
- if (scrolled)
+ if (anchor === undefined)
return;
- setScrolled(true);
- if (anchor === 'video')
- videoRef.current?.scrollIntoView({ block: 'start', inline: 'start' });
- if (anchor === 'diff')
- imageDiffRef.current?.scrollIntoView({ block: 'start', inline: 'start' });
- }, [scrolled, anchor, setScrolled, videoRef]);
+
+ const attachmentRef = attachmentRefs.current[anchor];
+ if (!attachmentRef)
+ return;
+
+ attachmentRef.scrollIntoView({ block: 'start', inline: 'start' });
+ }, [attachmentRefs, anchor]);
+ const targetRef = (attachment: TestAttachment): React.RefCallback => el => {
+ const index = result.attachments.indexOf(attachment);
+ if (el)
+ attachmentRefs.current[index] = el;
+ else
+ delete attachmentRefs.current[index];
+ };
return
{!!errors.length &&
@@ -107,7 +126,7 @@ export const TestResultView: React.FC<{
}
{diffs.map((diff, index) =>
-
+
)}
@@ -118,7 +137,7 @@ export const TestResultView: React.FC<{
-
+
;
})}
}
@@ -132,12 +151,12 @@ export const TestResultView: React.FC<{
}
}
- {!!videos.length &&