diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index ee2a8833e6..139a134965 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -27,12 +27,12 @@ import type { Annotation, FullConfigInternal, FullProjectInternal } from '../com import type { FullConfig, Location } from '../../types/testReporter'; import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util'; import { TestTracing } from './testTracing'; -import type { Attachment } from './testTracing'; import type { StackFrame } from '@protocol/channels'; import { testInfoError } from './util'; export interface TestStepInternal { - complete(result: { error?: Error | unknown, attachments?: Attachment[], suggestedRebaseline?: string }): void; + complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void; + attachmentIndexes: number[]; stepId: string; title: string; category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string; @@ -70,6 +70,7 @@ export class TestInfoImpl implements TestInfo { readonly _projectInternal: FullProjectInternal; readonly _configInternal: FullConfigInternal; private readonly _steps: TestStepInternal[] = []; + private readonly _stepMap = new Map(); _onDidFinishTestFunction: (() => Promise) | undefined; _hasNonRetriableError = false; _hasUnhandledError = false; @@ -248,7 +249,7 @@ export class TestInfoImpl implements TestInfo { return zones.zoneData('expectZone')?.stepId; } - _addStep(data: Omit, parentStep?: TestStepInternal): TestStepInternal { + _addStep(data: Omit, parentStep?: TestStepInternal): TestStepInternal { const stepId = `${data.category}@${++this._lastStepId}`; if (data.isStage) { @@ -267,10 +268,12 @@ export class TestInfoImpl implements TestInfo { } data.location = data.location || filteredStack[0]; + const attachmentIndexes: number[] = []; const step: TestStepInternal = { stepId, ...data, steps: [], + attachmentIndexes, complete: result => { if (step.endWallTime) return; @@ -307,11 +310,13 @@ export class TestInfoImpl implements TestInfo { }; this._onStepEnd(payload); const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined; - this._tracing.appendAfterActionForStep(stepId, errorForTrace, result.attachments); + const attachments = attachmentIndexes.map(i => this.attachments[i]); + this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments); } }; const parentStepList = parentStep ? parentStep.steps : this._steps; parentStepList.push(step); + this._stepMap.set(stepId, step); const payload: StepBeginPayload = { testId: this.testId, stepId, @@ -410,13 +415,14 @@ export class TestInfoImpl implements TestInfo { title: `attach "${name}"`, category: 'attach', }); - const attachment = await normalizeAndSaveAttachment(this.outputPath(), name, options); - this._attach(attachment, step.stepId); - step.complete({ attachments: [attachment] }); + this._attach(await normalizeAndSaveAttachment(this.outputPath(), name, options), step.stepId); + step.complete({}); } private _attach(attachment: TestInfo['attachments'][0], stepId: string | undefined) { - this._attachmentsPush(attachment); + const index = this._attachmentsPush(attachment) - 1; + if (stepId) + this._stepMap.get(stepId)!.attachmentIndexes.push(index); this._onAttach({ testId: this.testId, name: attachment.name, diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 3e25747c47..f3f586c57f 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -392,7 +392,7 @@ test('should show custom fixture titles in actions tree', async ({ runUITest }) ]); }); -test('all attachments are shown in attachments tab', async ({ runUITest }) => { +test('attachments tab shows all but top-level .push attachments', async ({ runUITest }) => { const { page } = await runUITest({ 'a.test.ts': ` import { test, expect } from '@playwright/test'; @@ -429,11 +429,10 @@ test('all attachments are shown in attachments tab', async ({ runUITest }) => { - treeitem /attach \\"bar-attach\\"/ `); await page.getByRole('tab', { name: 'Attachments' }).click(); - await expect(page.getByRole('tabpanel', { name: 'Attachments' }), 'attachments tab shows all').toMatchAriaSnapshot(` + await expect(page.getByRole('tabpanel', { name: 'Attachments' })).toMatchAriaSnapshot(` - tabpanel: - button /foo-push/ - button /foo-attach/ - - button /bar-push/ - button /bar-attach/ `); });