diff --git a/packages/html-reporter/src/icons.tsx b/packages/html-reporter/src/icons.tsx index 9609a2e23f..26515f331c 100644 --- a/packages/html-reporter/src/icons.tsx +++ b/packages/html-reporter/src/icons.tsx @@ -113,3 +113,11 @@ export const copy = () => { ; }; + +export const paperclip = () => { + return ( + + ); +}; diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index c4002cc904..6b44514900 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -40,7 +40,9 @@ const result: TestResult = { location: { file: 'test.spec.ts', line: 82, column: 0 }, steps: [], count: 1, + attachments: [], }], + attachments: [], }], attachments: [], status: 'passed', @@ -142,6 +144,7 @@ const resultWithAttachment: TestResult = { location: { file: 'test.spec.ts', line: 62, column: 0 }, count: 1, steps: [], + attachments: [1], }], attachments: [{ name: 'first attachment', @@ -183,3 +186,11 @@ test('should correctly render links in attachment name', async ({ mount }) => { await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284'); await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284'); }); + +test('should render attachments in step view', async ({ mount }) => { + const component = await mount(); + const steps = component.getByTestId('test-steps-chip'); + await expect(steps).not.toContainText('attachment with inline link'); + await steps.getByTitle('1 attachment').click(); + await expect(steps).toContainText('attachment with inline link'); +}); diff --git a/packages/html-reporter/src/testResultView.css b/packages/html-reporter/src/testResultView.css index 81fd61453d..67e2840294 100644 --- a/packages/html-reporter/src/testResultView.css +++ b/packages/html-reporter/src/testResultView.css @@ -62,3 +62,8 @@ padding: 0 !important; } } + +.attachments-icon { + color: var(--color-fg-muted); + margin-left: 4px; +} diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 73137e3ddf..01cb14540f 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -19,6 +19,7 @@ import * as React from 'react'; import { TreeItem } from './treeItem'; import { msToString } from './utils'; import { AutoChip } from './chip'; +import * as icons from './icons'; import { traceImage } from './images'; import { AttachmentLink, generateTraceUrl } from './links'; import { statusIcon } from './statusIcon'; @@ -188,23 +189,19 @@ const StepTreeItem: React.FC<{ depth: number, attachments: TestAttachment[], }> = ({ step, depth, attachments }) => { - if (step.category === 'attach') { - const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1]; - const matchingAttachments = attachments.filter(a => a.name === attachmentName); - if (matchingAttachments.length === 1) { - const [attachment] = matchingAttachments; - return ; - } - } - return {msToString(step.duration)} {statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')} {step.title} {step.count > 1 && <> ✕ {step.count}} {step.location && — {step.location.file}:{step.location.line}} - } loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => { + {step.attachments.length > 0 && 1 ? 's' : ''}`}>{icons.paperclip()}} + } loadChildren={step.steps.length + step.attachments.length + (step.snippet ? 1 : 0) ? () => { const children = step.steps.map((s, i) => ); + children.unshift(...step.attachments.map(a => { + const attachment = attachments[a]; + return ; + })); if (step.snippet) children.unshift(); return children; diff --git a/packages/html-reporter/src/types.ts b/packages/html-reporter/src/types.ts index ea2cf453bc..7322106753 100644 --- a/packages/html-reporter/src/types.ts +++ b/packages/html-reporter/src/types.ts @@ -109,5 +109,6 @@ export type TestStep = { snippet?: string; error?: string; steps: TestStep[]; + attachments: number[]; count: number; }; diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index cd6b4aae4f..7403985bcb 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -484,7 +484,7 @@ class HtmlBuilder { duration: result.duration, startTime: result.startTime.toISOString(), retry: result.retry, - steps: dedupeSteps(result.steps).map(s => this._createTestStep(s)), + steps: dedupeSteps(result.steps).map(s => this._createTestStep(s, result)), errors: formatResultFailure(test, result, '', true).map(error => error.message), status: result.status, attachments: this._serializeAttachments([ @@ -494,21 +494,27 @@ class HtmlBuilder { }; } - private _createTestStep(dedupedStep: DedupedStep): TestStep { + private _createTestStep(dedupedStep: DedupedStep, result: TestResultPublic): TestStep { const { step, duration, count } = dedupedStep; - const result: TestStep = { + const testStep: TestStep = { title: step.title, category: step.category, startTime: step.startTime.toISOString(), duration, - steps: dedupeSteps(step.steps).map(s => this._createTestStep(s)), + steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)), + attachments: step.attachments.map(s => { + const index = result.attachments.indexOf(s); + if (index === -1) + throw new Error('Unexpected, attachment not found'); + return index; + }), location: this._relativeLocation(step.location), error: step.error?.message, count }; - if (result.location) - this._stepsInFile.set(result.location.file, result); - return result; + if (testStep.location) + this._stepsInFile.set(testStep.location.file, testStep); + return testStep; } private _relativeLocation(location: Location | undefined): Location | undefined {