use Anchor

This commit is contained in:
Simon Knott 2024-11-19 17:18:00 +01:00
parent 579fbd25ba
commit aa2bedae26
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
3 changed files with 24 additions and 14 deletions

View file

@ -20,7 +20,7 @@ import './colors.css';
import './common.css';
import * as icons from './icons';
import { clsx } from '@web/uiUtils';
import { useAnchor } from './links';
import { type AnchorID, useAnchor } from './links';
export const Chip: React.FC<{
header: JSX.Element | string,
@ -53,7 +53,7 @@ export const AutoChip: React.FC<{
noInsets?: boolean,
children?: any,
dataTestId?: string,
revealOnAnchorId?: string,
revealOnAnchorId?: AnchorID,
}> = ({ header, initialExpanded, noInsets, children, dataTestId, revealOnAnchorId }) => {
const [expanded, setExpanded] = React.useState(initialExpanded ?? true);
const onReveal = React.useCallback(() => setExpanded(true), []);

View file

@ -114,7 +114,7 @@ export function generateTraceUrl(traces: TestAttachment[]) {
const kMissingContentType = 'x-playwright/missing';
type AnchorID = string | ((id: string | null) => boolean) | undefined;
export type AnchorID = string | ((id: string) => boolean) | undefined;
export function useAnchor(id: AnchorID, onReveal: () => void) {
React.useEffect(() => {
@ -123,8 +123,10 @@ export function useAnchor(id: AnchorID, onReveal: () => void) {
const listener = () => {
const params = new URLSearchParams(window.location.hash.slice(1));
if (!params.has('anchor'))
return;
const anchor = params.get('anchor');
const isRevealed = typeof id === 'function' ? id(anchor) : anchor === id;
const isRevealed = typeof id === 'function' ? id(anchor!) : anchor === id;
if (isRevealed)
onReveal();
};

View file

@ -65,7 +65,7 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
export const TestResultView: React.FC<{
test: TestCase,
result: TestResult,
}> = ({ result }) => {
}> = ({ test, 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/')));
@ -88,7 +88,7 @@ export const TestResultView: React.FC<{
})}
</AutoChip>}
{!!result.steps.length && <AutoChip header='Test Steps'>
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} result={result} test={test} depth={0}/>)}
</AutoChip>}
{diffs.map((diff, index) =>
@ -128,11 +128,17 @@ export const TestResultView: React.FC<{
</div>)}
</AutoChip></Anchor>}
{!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'>
{!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments' revealOnAnchorId={id => id.startsWith('attachment-')}>
{[...htmls].map((a, i) => (
<AttachmentLink key={`html-link-${i}`} attachment={a} openInNewTab />)
<Anchor key={`html-link-${i}`} id={`attachment-${a.name}`}>
<AttachmentLink attachment={a} openInNewTab />
</Anchor>)
)}
{[...otherAttachments].map((a, i) =>
<Anchor key={`attachment-link-${i}`} id={`attachment-${a.name}`}>
<AttachmentLink attachment={a}/>
</Anchor>
)}
{[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
</AutoChip>}
</div>;
};
@ -162,21 +168,23 @@ function classifyErrors(testErrors: string[], diffs: ImageDiff[]) {
}
const StepTreeItem: React.FC<{
test: TestCase;
result: TestResult;
step: TestStep;
depth: number,
}> = ({ step, depth }) => {
}> = ({ test, step, result, depth }) => {
const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1];
return <TreeItem title={<span aria-label={step.title}>
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href='TODO' onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={`#?testId=${test.testId}&anchor=attachment-${attachmentName}&run=${test.results.indexOf(result)}`} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
<span>{step.title}</span>
{step.count > 1 && <> <span className='test-result-counter'>{step.count}</span></>}
{step.location && <span className='test-result-path'> {step.location.file}:{step.location.line}</span>}
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
if (step.snippet)
children.unshift(<TestErrorView testId='test-snippet' key='line' error={step.snippet}></TestErrorView>);
children.unshift(<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>);
return children;
} : undefined} depth={depth}></TreeItem>;
} : undefined} depth={depth}/>;
};