feat(html): link from attachment step to attachment
This commit is contained in:
parent
67471cb3c5
commit
8afe8f60b2
|
|
@ -22,6 +22,7 @@ import { CopyToClipboard } from './copyToClipboard';
|
||||||
import './links.css';
|
import './links.css';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx } from '@web/uiUtils';
|
||||||
|
import { componentID } from './testResultView';
|
||||||
|
|
||||||
export function navigate(href: string) {
|
export function navigate(href: string) {
|
||||||
window.history.pushState({}, '', href);
|
window.history.pushState({}, '', href);
|
||||||
|
|
@ -77,7 +78,7 @@ export const AttachmentLink: React.FunctionComponent<{
|
||||||
linkName?: string,
|
linkName?: string,
|
||||||
openInNewTab?: boolean,
|
openInNewTab?: boolean,
|
||||||
}> = ({ attachment, href, linkName, openInNewTab }) => {
|
}> = ({ attachment, href, linkName, openInNewTab }) => {
|
||||||
return <TreeItem title={<span>
|
return <TreeItem id={componentID(params => params.set('attachment', attachment.name))} title={<span>
|
||||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||||
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||||
{!attachment.path && (
|
{!attachment.path && (
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,14 @@ const result: TestResult = {
|
||||||
errors: [],
|
errors: [],
|
||||||
steps: [{
|
steps: [{
|
||||||
title: 'Outer step',
|
title: 'Outer step',
|
||||||
|
category: 'test.step',
|
||||||
startTime: new Date(100).toUTCString(),
|
startTime: new Date(100).toUTCString(),
|
||||||
duration: 10,
|
duration: 10,
|
||||||
location: { file: 'test.spec.ts', line: 62, column: 0 },
|
location: { file: 'test.spec.ts', line: 62, column: 0 },
|
||||||
count: 1,
|
count: 1,
|
||||||
steps: [{
|
steps: [{
|
||||||
title: 'Inner step',
|
title: 'Inner step',
|
||||||
|
category: 'test.step',
|
||||||
startTime: new Date(200).toUTCString(),
|
startTime: new Date(200).toUTCString(),
|
||||||
duration: 10,
|
duration: 10,
|
||||||
location: { file: 'test.spec.ts', line: 82, column: 0 },
|
location: { file: 'test.spec.ts', line: 82, column: 0 },
|
||||||
|
|
@ -134,6 +136,7 @@ const resultWithAttachment: TestResult = {
|
||||||
errors: [],
|
errors: [],
|
||||||
steps: [{
|
steps: [{
|
||||||
title: 'Outer step',
|
title: 'Outer step',
|
||||||
|
category: 'test.step',
|
||||||
startTime: new Date(100).toUTCString(),
|
startTime: new Date(100).toUTCString(),
|
||||||
duration: 10,
|
duration: 10,
|
||||||
location: { file: 'test.spec.ts', line: 62, column: 0 },
|
location: { file: 'test.spec.ts', line: 62, column: 0 },
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { statusIcon } from './statusIcon';
|
||||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||||
import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||||
|
import * as icons from './icons';
|
||||||
import './testResultView.css';
|
import './testResultView.css';
|
||||||
|
|
||||||
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
||||||
|
|
@ -173,12 +174,20 @@ function classifyErrors(testErrors: string[], diffs: ImageDiff[]) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function componentID(cb: (params: URLSearchParams) => void) {
|
||||||
|
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||||
|
cb(searchParams);
|
||||||
|
return '?' + searchParams;
|
||||||
|
}
|
||||||
|
|
||||||
const StepTreeItem: React.FC<{
|
const StepTreeItem: React.FC<{
|
||||||
step: TestStep;
|
step: TestStep;
|
||||||
depth: number,
|
depth: number,
|
||||||
}> = ({ step, depth }) => {
|
}> = ({ step, depth }) => {
|
||||||
return <TreeItem title={<span>
|
const attachmentName = step.category === 'attach' ? step.title.match(/^attach "(.*)"$/)?.[1] : undefined;
|
||||||
|
return <TreeItem title={<span aria-label={step.title}>
|
||||||
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
||||||
|
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={'#' + componentID(params => params.set('attachment', attachmentName))} onClick={(evt) => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
|
||||||
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
||||||
<span>{step.title}</span>
|
<span>{step.title}</span>
|
||||||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-item:target > .tree-item-title {
|
||||||
|
text-decoration: underline var(--color-underlinenav-icon);
|
||||||
|
text-decoration-thickness: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-item-body {
|
.tree-item-body {
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@ export const TreeItem: React.FunctionComponent<{
|
||||||
depth: number,
|
depth: number,
|
||||||
selected?: boolean,
|
selected?: boolean,
|
||||||
style?: React.CSSProperties,
|
style?: React.CSSProperties,
|
||||||
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => {
|
id?: string,
|
||||||
|
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style, id }) => {
|
||||||
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
||||||
const className = selected ? 'tree-item-title selected' : 'tree-item-title';
|
const className = selected ? 'tree-item-title selected' : 'tree-item-title';
|
||||||
return <div className={'tree-item'} style={style}>
|
return <div className={'tree-item'} id={id} style={style}>
|
||||||
<span className={className} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
<span className={className} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||||
{loadChildren && !!expanded && icons.downArrow()}
|
{loadChildren && !!expanded && icons.downArrow()}
|
||||||
{loadChildren && !expanded && icons.rightArrow()}
|
{loadChildren && !expanded && icons.rightArrow()}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ export type TestResult = {
|
||||||
|
|
||||||
export type TestStep = {
|
export type TestStep = {
|
||||||
title: string;
|
title: string;
|
||||||
|
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,7 @@ class HtmlBuilder {
|
||||||
const { step, duration, count } = dedupedStep;
|
const { step, duration, count } = dedupedStep;
|
||||||
const result: TestStep = {
|
const result: TestStep = {
|
||||||
title: step.title,
|
title: step.title,
|
||||||
|
category: step.category,
|
||||||
startTime: step.startTime.toISOString(),
|
startTime: step.startTime.toISOString(),
|
||||||
duration,
|
duration,
|
||||||
steps: dedupeSteps(step.steps).map(s => this._createTestStep(s)),
|
steps: dedupeSteps(step.steps).map(s => this._createTestStep(s)),
|
||||||
|
|
|
||||||
|
|
@ -845,7 +845,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
'a.test.js': `
|
'a.test.js': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('passing', async ({ page }, testInfo) => {
|
test('passing', async ({ page }, testInfo) => {
|
||||||
testInfo.attach('axe-report.html', {
|
await testInfo.attach('axe-report.html', {
|
||||||
contentType: 'text/html',
|
contentType: 'text/html',
|
||||||
body: '<h1>Axe Report</h1>',
|
body: '<h1>Axe Report</h1>',
|
||||||
});
|
});
|
||||||
|
|
@ -914,6 +914,27 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should link from attach step to attachment view', async ({ runInlineTest, page, showReport }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.js': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('passing', async ({ page }, testInfo) => {
|
||||||
|
await testInfo.attach('foo', { body: 'bar' });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
|
||||||
|
await showReport();
|
||||||
|
await page.getByRole('link', { name: 'passing' }).click();
|
||||||
|
await page.getByLabel('attach "foo"').getByTitle('link to attachment').click();
|
||||||
|
|
||||||
|
await page.waitForURL(url => {
|
||||||
|
const navState = new URLSearchParams(url.hash.slice(1));
|
||||||
|
return navState.get('attachment') === 'foo';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should strikethrough textual diff', async ({ runInlineTest, showReport, page }) => {
|
test('should strikethrough textual diff', async ({ runInlineTest, showReport, page }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'helper.ts': `
|
'helper.ts': `
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue