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 {