,
-}> = ({ header, initialExpanded, noInsets, children, dataTestId, targetRef }) => {
- const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined);
+ revealOnAnchorId?: string,
+}> = ({ header, initialExpanded, noInsets, children, dataTestId, revealOnAnchorId }) => {
+ const [expanded, setExpanded] = React.useState(initialExpanded ?? true);
+ const onReveal = React.useCallback(() => setExpanded(true), []);
+ useAnchor(revealOnAnchorId, onReveal);
return
{children}
;
diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx
index 4b48090e0a..4beb7b65e0 100644
--- a/packages/html-reporter/src/links.tsx
+++ b/packages/html-reporter/src/links.tsx
@@ -113,3 +113,32 @@ export function generateTraceUrl(traces: TestAttachment[]) {
}
const kMissingContentType = 'x-playwright/missing';
+
+type AnchorID = string | ((id: string | null) => boolean) | undefined;
+
+export function useAnchor(id: AnchorID, onReveal: () => void) {
+ React.useEffect(() => {
+ if (typeof id === 'undefined')
+ return;
+
+ const listener = () => {
+ const params = new URLSearchParams(window.location.hash.slice(1));
+ const anchor = params.get('anchor');
+ const isRevealed = typeof id === 'function' ? id(anchor) : anchor === id;
+ if (isRevealed)
+ onReveal();
+ };
+ window.addEventListener('popstate', listener);
+ return () => window.removeEventListener('popstate', listener);
+ }, [id, onReveal]);
+}
+
+export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
+ const ref = React.useRef
(null);
+ const onAnchorReveal = React.useCallback(() => {
+ requestAnimationFrame(() => ref.current?.scrollIntoView({ block: 'start', inline: 'start' }));
+ }, []);
+ useAnchor(id, onAnchorReveal);
+
+ return {children}
;
+}
diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx
index e8a5c3b250..cf0f5e5e56 100644
--- a/packages/html-reporter/src/reportView.tsx
+++ b/packages/html-reporter/src/reportView.tsx
@@ -101,7 +101,6 @@ const TestCaseViewLoader: React.FC<{
const searchParams = React.useContext(SearchParamsContext);
const [test, setTest] = React.useState();
const testId = searchParams.get('testId');
- const anchor = (searchParams.get('anchor') || '') as 'video' | 'diff' | '';
const run = +(searchParams.get('run') || '0');
const { prev, next } = React.useMemo(() => {
@@ -133,7 +132,6 @@ const TestCaseViewLoader: React.FC<{
next={next}
prev={prev}
test={test}
- anchor={anchor}
run={run}
/>;
};
diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx
index 892ad51b7f..b7a9f9405b 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();
@@ -181,7 +181,7 @@ const testCaseSummary: TestCaseSummary = {
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();
@@ -194,7 +194,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');
@@ -204,7 +204,7 @@ test('should correctly render links in attachment name', async ({ mount }) => {
});
test('should correctly render prev and next', async ({ mount }) => {
- const component = await mount();
+ const component = await mount();
await expect(component).toMatchAriaSnapshot(`
- text: group
- link "« previous"
diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx
index 320722fa9b..4e9785ad8a 100644
--- a/packages/html-reporter/src/testCaseView.tsx
+++ b/packages/html-reporter/src/testCaseView.tsx
@@ -33,9 +33,8 @@ export const TestCaseView: React.FC<{
test: TestCase | undefined,
next: TestCaseSummary | undefined,
prev: TestCaseSummary | undefined,
- anchor: 'video' | 'diff' | '',
run: number,
-}> = ({ projectNames, test, run, anchor, next, prev }) => {
+}> = ({ projectNames, test, run, next, prev }) => {
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
const searchParams = React.useContext(SearchParamsContext);
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
@@ -79,7 +78,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 4d6890ad33..6b31d2ebe2 100644
--- a/packages/html-reporter/src/testFileView.tsx
+++ b/packages/html-reporter/src/testFileView.tsx
@@ -75,12 +75,12 @@ 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)/);
}));
- return resultWithImageDiff ?
{image()} : undefined;
+ 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;
+ 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 3a562f3fcf..9170f2023d 100644
--- a/packages/html-reporter/src/testResultView.tsx
+++ b/packages/html-reporter/src/testResultView.tsx
@@ -20,7 +20,7 @@ import { TreeItem } from './treeItem';
import { msToString } from './utils';
import { AutoChip } from './chip';
import { traceImage } from './images';
-import { AttachmentLink, generateTraceUrl } from './links';
+import { Anchor, AttachmentLink, generateTraceUrl } from './links';
import { statusIcon } from './statusIcon';
import type { ImageDiff } from '@web/shared/imageDiffView';
import { ImageDiffView } from '@web/shared/imageDiffView';
@@ -64,9 +64,7 @@ function groupImageDiffs(screenshots: Set
): ImageDiff[] {
export const TestResultView: React.FC<{
test: TestCase,
result: TestResult,
- anchor: 'video' | 'diff' | '',
-}> = ({ result, anchor }) => {
-
+}> = ({ result }) => {
const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => {
const attachments = result?.attachments || [];
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
@@ -80,20 +78,6 @@ 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);
- React.useEffect(() => {
- if (scrolled)
- 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]);
-
return
{!!errors.length &&
{errors.map((error, index) => {
@@ -107,9 +91,11 @@ export const TestResultView: React.FC<{
}
{diffs.map((diff, index) =>
-
-
-
+
+
+
+
+
)}
{!!screenshots.length &&
@@ -123,23 +109,23 @@ export const TestResultView: React.FC<{
})}
}
- {!!traces.length &&
+ {!!traces.length &&
{
{traces.map((a, i) =>
)}
}
- }
+ }
- {!!videos.length &&
+ {!!videos.length &&
{videos.map((a, i) => )}
- }
+ }
{!!(otherAttachments.size + htmls.length) &&
{[...htmls].map((a, i) => (