diff --git a/docs/src/test-reporter-api/class-teststep.md b/docs/src/test-reporter-api/class-teststep.md index ef16e4849a..3b88b622e1 100644 --- a/docs/src/test-reporter-api/class-teststep.md +++ b/docs/src/test-reporter-api/class-teststep.md @@ -38,6 +38,12 @@ Error thrown during the step execution, if any. Parent step, if any. +## property: TestStep.stack +* since: v1.51 +- type: <[Array]<[Location]>> + +Call stack for this step. + ## property: TestStep.startTime * since: v1.10 - type: <[Date]> diff --git a/packages/playwright-core/src/utils/timeoutRunner.ts b/packages/playwright-core/src/utils/timeoutRunner.ts index 622019565a..f513ab331a 100644 --- a/packages/playwright-core/src/utils/timeoutRunner.ts +++ b/packages/playwright-core/src/utils/timeoutRunner.ts @@ -17,14 +17,19 @@ import { monotonicTime } from './'; export async function raceAgainstDeadline(cb: () => Promise, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> { + // Avoid indirections to preserve better stacks. + if (deadline === 0) { + const result = await cb(); + return { result, timedOut: false }; + } + let timer: NodeJS.Timeout | undefined; return Promise.race([ cb().then(result => { return { result, timedOut: false }; }), new Promise<{ timedOut: true }>(resolve => { - const kMaxDeadline = 2147483647; // 2^31-1 - const timeout = (deadline || kMaxDeadline) - monotonicTime(); + const timeout = deadline - monotonicTime(); timer = setTimeout(() => resolve({ timedOut: true }), timeout); }), ]).finally(() => { diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index 76ee996216..32549b6bda 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -100,7 +100,7 @@ export type StepBeginPayload = { title: string; category: string; wallTime: number; // milliseconds since unix epoch - location?: { file: string, line: number, column: number }; + stack: { file: string, line: number, column: number }[]; }; export type StepEndPayload = { diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 58d813a99e..db84bfd201 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -69,7 +69,7 @@ export class TestTypeImpl { this.test = test; } - private _currentSuite(location: Location, title: string): Suite | undefined { + private _currentSuite(title: string): Suite | undefined { const suite = currentlyLoadingFileSuite(); if (!suite) { throw new Error([ @@ -86,7 +86,7 @@ export class TestTypeImpl { private _createTest(type: 'default' | 'only' | 'skip' | 'fixme' | 'fail' | 'fail.only', location: Location, title: string, fnOrDetails: Function | TestDetails, fn?: Function) { throwIfRunningInsideJest(); - const suite = this._currentSuite(location, 'test()'); + const suite = this._currentSuite('test()'); if (!suite) return; @@ -117,7 +117,7 @@ export class TestTypeImpl { private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, titleOrFn: string | Function, fnOrDetails?: TestDetails | Function, fn?: Function) { throwIfRunningInsideJest(); - const suite = this._currentSuite(location, 'test.describe()'); + const suite = this._currentSuite('test.describe()'); if (!suite) return; @@ -169,7 +169,7 @@ export class TestTypeImpl { } private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', location: Location, title: string | Function, fn?: Function) { - const suite = this._currentSuite(location, `test.${name}()`); + const suite = this._currentSuite(`test.${name}()`); if (!suite) return; if (typeof title === 'function') { @@ -182,7 +182,7 @@ export class TestTypeImpl { private _configure(location: Location, options: { mode?: 'default' | 'parallel' | 'serial', retries?: number, timeout?: number }) { throwIfRunningInsideJest(); - const suite = this._currentSuite(location, `test.describe.configure()`); + const suite = this._currentSuite(`test.describe.configure()`); if (!suite) return; @@ -252,7 +252,7 @@ export class TestTypeImpl { } private _use(location: Location, fixtures: Fixtures) { - const suite = this._currentSuite(location, `test.use()`); + const suite = this._currentSuite(`test.use()`); if (!suite) return; suite._use.push({ fixtures, location }); @@ -263,11 +263,11 @@ export class TestTypeImpl { if (!testInfo) throw new Error(`test.step() can only be called from a test`); if (expectation === 'skip') { - const step = testInfo._addStep({ category: 'test.step.skip', title, location: options.location, box: options.box }); + const step = testInfo._addStep({ category: 'test.step.skip', title, box: options.box }, undefined, options.location ? [options.location] : undefined); step.complete({}); return undefined as T; } - const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); + const step = testInfo._addStep({ category: 'test.step', title, box: options.box }, undefined, options.location ? [options.location] : undefined); return await zones.run('stepZone', step, async () => { try { let result: Awaited>> | undefined = undefined; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 83913c18dc..225bd9ba92 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -273,12 +273,11 @@ const playwrightFixtures: Fixtures = ({ } // In the general case, create a step for each api call and connect them through the stepId. const step = testInfo._addStep({ - location: data.frames[0], category: 'pw:api', title: renderApiCall(data.apiName, data.params), apiName: data.apiName, params: data.params, - }, tracingGroupSteps[tracingGroupSteps.length - 1]); + }, tracingGroupSteps[tracingGroupSteps.length - 1], data.frames); data.userData = step; data.stepId = step.stepId; if (data.apiName === 'tracing.group') diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index 1d41b793cd..222cb6864b 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -101,7 +101,8 @@ export type JsonTestStepStart = { title: string; category: string, startTime: number; - location?: reporterTypes.Location; + // Best effort to keep step struct small. + stack?: reporterTypes.Location | reporterTypes.Location[]; }; export type JsonTestStepEnd = { @@ -249,8 +250,8 @@ export class TeleReporterReceiver { const result = test.results.find(r => r._id === resultId)!; const parentStep = payload.parentStepId ? result._stepMap.get(payload.parentStepId) : undefined; - const location = this._absoluteLocation(payload.location); - const step = new TeleTestStep(payload, parentStep, location, result); + const stack = Array.isArray(payload.stack) ? payload.stack.map(l => this._absoluteLocation(l)) : this._absoluteLocation(payload.stack); + const step = new TeleTestStep(payload, parentStep, stack, result); if (parentStep) parentStep.steps.push(step); else @@ -426,8 +427,8 @@ export class TeleSuite implements reporterTypes.Suite { } allTests(): reporterTypes.TestCase[] { - const result: reporterTypes.TestCase[] = []; - const visit = (suite: reporterTypes.Suite) => { + const result: TeleTestCase[] = []; + const visit = (suite: TeleSuite) => { for (const entry of suite.entries()) { if (entry.type === 'test') result.push(entry); @@ -511,6 +512,7 @@ class TeleTestStep implements reporterTypes.TestStep { title: string; category: string; location: reporterTypes.Location | undefined; + stack: reporterTypes.Location[]; parent: reporterTypes.TestStep | undefined; duration: number = -1; steps: reporterTypes.TestStep[] = []; @@ -521,10 +523,11 @@ class TeleTestStep implements reporterTypes.TestStep { private _startTime: number = 0; - constructor(payload: JsonTestStepStart, parentStep: reporterTypes.TestStep | undefined, location: reporterTypes.Location | undefined, result: TeleTestResult) { + constructor(payload: JsonTestStepStart, parentStep: reporterTypes.TestStep | undefined, stackOrLocation: reporterTypes.Location | reporterTypes.Location[] | undefined, result: TeleTestResult) { this.title = payload.title; this.category = payload.category; - this.location = location; + this.stack = Array.isArray(stackOrLocation) ? stackOrLocation : (stackOrLocation ? [stackOrLocation] : []); + this.location = this.stack[0]; this.parent = parentStep; this._startTime = payload.startTime; this._result = result; diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts index 22bdce30ff..e002ade0d1 100644 --- a/packages/playwright/src/isomorphic/testServerConnection.ts +++ b/packages/playwright/src/isomorphic/testServerConnection.ts @@ -37,7 +37,7 @@ export class WebSocketTestServerTransport implements TestServerTransport { } onmessage(listener: (message: string) => void) { - this._ws.addEventListener('message', event => listener(event.data)); + this._ws.addEventListener('message', event => listener(String(event.data))); } onopen(listener: () => void) { diff --git a/packages/playwright/src/reporters/blob.ts b/packages/playwright/src/reporters/blob.ts index 837fe55be0..cbec1cce28 100644 --- a/packages/playwright/src/reporters/blob.ts +++ b/packages/playwright/src/reporters/blob.ts @@ -34,7 +34,7 @@ type BlobReporterOptions = { _commandHash: string; }; -export const currentBlobReportVersion = 2; +export const currentBlobReportVersion = 3; export type BlobReportMetadata = { version: number; diff --git a/packages/playwright/src/reporters/merge.ts b/packages/playwright/src/reporters/merge.ts index 102335cceb..ce7eb88733 100644 --- a/packages/playwright/src/reporters/merge.ts +++ b/packages/playwright/src/reporters/merge.ts @@ -75,7 +75,8 @@ export async function createMergedReport(config: FullConfigInternal, dir: string await dispatchEvents(eventData.prologue); for (const { reportFile, eventPatchers, metadata } of eventData.reports) { const reportJsonl = await fs.promises.readFile(reportFile); - const events = parseTestEvents(reportJsonl); + let events = parseTestEvents(reportJsonl); + events = modernizer.modernize(metadata.version, events); new JsonStringInternalizer(stringPool).traverse(events); eventPatchers.patchers.push(new AttachmentPathPatcher(dir)); if (metadata.name) @@ -480,7 +481,8 @@ class PathSeparatorPatcher { } if (jsonEvent.method === 'onStepBegin') { const step = jsonEvent.params.step as JsonTestStepStart; - this._updateLocation(step.location); + for (const stackFrame of Array.isArray(step.stack) ? step.stack : [step.stack]) + this._updateLocation(stackFrame); return; } if (jsonEvent.method === 'onStepEnd') { @@ -589,6 +591,14 @@ class BlobModernizer { return event; }); } + + _modernize_2_to_3(events: JsonEvent[]): JsonEvent[] { + return events.map(event => { + if (event.method === 'onStepBegin') + (event.params.step as JsonTestStepStart).stack = event.params.step.location; + return event; + }); + } } const modernizer = new BlobModernizer(); diff --git a/packages/playwright/src/reporters/teleEmitter.ts b/packages/playwright/src/reporters/teleEmitter.ts index 0ec92ae9ac..d2513d8afd 100644 --- a/packages/playwright/src/reporters/teleEmitter.ts +++ b/packages/playwright/src/reporters/teleEmitter.ts @@ -247,7 +247,7 @@ export class TeleReporterEmitter implements ReporterV2 { title: step.title, category: step.category, startTime: +step.startTime, - location: this._relativeLocation(step.location), + stack: this._relativeStack(step.stack), }; } @@ -260,6 +260,14 @@ export class TeleReporterEmitter implements ReporterV2 { }; } + private _relativeStack(stack: reporterTypes.Location[]): undefined | reporterTypes.Location | reporterTypes.Location[] { + if (!stack.length) + return undefined; + if (stack.length === 1) + return this._relativeLocation(stack[0]); + return stack.map(frame => this._relativeLocation(frame)); + } + private _relativeLocation(location: reporterTypes.Location): reporterTypes.Location; private _relativeLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined; private _relativeLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined { diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index 534fe7eb4a..ae35d8461b 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -321,7 +321,8 @@ class JobDispatcher { duration: -1, steps: [], attachments: [], - location: params.location, + location: params.stack[0], + stack: params.stack, }; steps.set(params.stepId, step); (parentStep || result).steps.push(step); diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index f7f91d3198..27aaa0838b 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -51,7 +51,7 @@ export function filterStackFile(file: string) { return true; } -export function filteredStackTrace(rawStack: RawStack): StackFrame[] { +export function filteredStackTrace(rawStack: RawStack): Location[] { const frames: StackFrame[] = []; for (const line of rawStack) { const frame = parseStackTraceLine(line); @@ -59,7 +59,7 @@ export function filteredStackTrace(rawStack: RawStack): StackFrame[] { continue; if (!filterStackFile(frame.file)) continue; - frames.push(frame); + frames.push({ file: frame.file, line: frame.line, column: frame.column }); } return frames; } diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 3efd3b3750..568384731f 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -26,7 +26,6 @@ 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 { StackFrame } from '@protocol/channels'; import { testInfoError } from './util'; export interface TestStepInternal { @@ -35,8 +34,8 @@ export interface TestStepInternal { stepId: string; title: string; category: string; - location?: Location; - boxedStack?: StackFrame[]; + stack: Location[]; + boxedStack?: Location[]; steps: TestStepInternal[]; endWallTime?: number; apiName?: string; @@ -244,7 +243,7 @@ export class TestInfoImpl implements TestInfo { ?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent. } - _addStep(data: Omit, parentStep?: TestStepInternal): TestStepInternal { + _addStep(data: Omit, parentStep?: TestStepInternal, stackOverride?: Location[]): TestStepInternal { const stepId = `${data.category}@${++this._lastStepId}`; if (data.isStage) { @@ -255,18 +254,20 @@ export class TestInfoImpl implements TestInfo { parentStep = this._parentStep(); } - const filteredStack = filteredStackTrace(captureRawStack()); + const filteredStack = stackOverride ? stackOverride : filteredStackTrace(captureRawStack()); data.boxedStack = parentStep?.boxedStack; + let stack = filteredStack; if (!data.boxedStack && data.box) { data.boxedStack = filteredStack.slice(1); - data.location = data.location || data.boxedStack[0]; + // Only steps with box: true get boxed stack. Inner steps have original stack for better traceability. + stack = data.boxedStack; } - data.location = data.location || filteredStack[0]; const attachmentIndices: number[] = []; const step: TestStepInternal = { stepId, ...data, + stack, steps: [], attachmentIndices, complete: result => { @@ -319,10 +320,10 @@ export class TestInfoImpl implements TestInfo { title: data.title, category: data.category, wallTime: Date.now(), - location: data.location, + stack, }; this._onStepBegin(payload); - this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.category, data.apiName || data.title, data.params, data.location ? [data.location] : []); + this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.category, data.apiName || data.title, data.params, stack); return step; } @@ -351,7 +352,7 @@ export class TestInfoImpl implements TestInfo { const location = stage.runnable?.location ? ` at "${formatLocation(stage.runnable.location)}"` : ``; debugTest(`started stage "${stage.title}"${location}`); } - stage.step = stage.stepInfo ? this._addStep({ ...stage.stepInfo, title: stage.title, isStage: true }) : undefined; + stage.step = stage.stepInfo ? this._addStep({ category: stage.stepInfo.category, title: stage.title, isStage: true }, undefined, stage.stepInfo.location ? [stage.stepInfo.location] : undefined) : undefined; try { await this._timeoutManager.withRunnable(stage.runnable, async () => { diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index b4878ff659..7809f3a033 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -555,7 +555,7 @@ export class WorkerMain extends ProcessRunner { let firstError: Error | undefined; const hooks = suites.map(suite => this._collectHooksAndModifiers(suite, type, testInfo)).flat(); for (const hook of hooks) { - const runnable = { type: hook.type, location: hook.location, slot }; + const runnable = { type: hook.type, stack: [hook.location], slot }; if (testInfo._timeoutManager.isTimeExhaustedFor(runnable)) { // Do not run hooks that will timeout right away. continue; diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index 3f3a43984e..7e5e703810 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -747,6 +747,11 @@ export interface TestStep { */ parent?: TestStep; + /** + * Call stack for this step. + */ + stack: Array; + /** * Start time of this particular test step. */ diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index e16fbcc460..a8bc6a0da0 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -1432,7 +1432,7 @@ test('blob report should include version', async ({ runInlineTest }) => { const events = await extractReport(test.info().outputPath('blob-report', 'report.zip'), test.info().outputPath('tmp')); const metadataEvent = events.find(e => e.method === 'onBlobReportMetadata'); - expect(metadataEvent.params.version).toBe(2); + expect(metadataEvent.params.version).toBe(3); expect(metadataEvent.params.userAgent).toBe(getUserAgent()); }); diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index 7d509fda25..5963c7d3dd 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -30,6 +30,12 @@ function formatLocation(location?: Location) { return ' @ ' + path.basename(location.file) + ':' + location.line; } +function formatStackFrames(stack: Location[]) { + if (stack.length < 2) + return ''; + return ' @stack [' + stack.map(l => path.basename(l.file) + ':' + l.line).join(' | ') + ']'; +} + function formatStack(indent: string, rawStack: string) { let stack = rawStack.split('\\n').filter(s => s.startsWith(' at ')); stack = stack.map(s => { @@ -75,7 +81,8 @@ export default class MyReporter implements Reporter { let location = ''; if (step.location) location = formatLocation(step.location); - console.log(formatPrefix(step.category) + indent + step.title + location); + let stack = formatStackFrames(step.stack); + console.log(formatPrefix(step.category) + indent + step.title + location + stack); if (step.error) { const errorLocation = this.printErrorLocation ? formatLocation(step.error.location) : ''; console.log(formatPrefix(step.category) + indent + '↪ error: ' + this.trimError(step.error.message!) + errorLocation); @@ -143,11 +150,11 @@ pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage test.step |outer step 1 @ a.test.ts:4 -test.step | inner step 1.1 @ a.test.ts:5 -test.step | inner step 1.2 @ a.test.ts:6 +test.step | inner step 1.1 @ a.test.ts:5 @stack [a.test.ts:5 | a.test.ts:4] +test.step | inner step 1.2 @ a.test.ts:6 @stack [a.test.ts:6 | a.test.ts:4] test.step |outer step 2 @ a.test.ts:8 -test.step | inner step 2.1 @ a.test.ts:9 -test.step | inner step 2.2 @ a.test.ts:10 +test.step | inner step 2.1 @ a.test.ts:9 @stack [a.test.ts:9 | a.test.ts:8] +test.step | inner step 2.2 @ a.test.ts:10 @stack [a.test.ts:10 | a.test.ts:8] hook |After Hooks fixture | fixture: page fixture | fixture: context @@ -485,9 +492,9 @@ test('should mark step as failed when soft expect fails', async ({ runInlineTest hook |Before Hooks test.step |outer @ a.test.ts:4 test.step |↪ error: Error: expect(received).toBe(expected) // Object.is equality -test.step | inner @ a.test.ts:5 +test.step | inner @ a.test.ts:5 @stack [a.test.ts:5 | a.test.ts:4] test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -expect | expect.soft.toBe @ a.test.ts:6 +expect | expect.soft.toBe @ a.test.ts:6 @stack [a.test.ts:6 | a.test.ts:5 | a.test.ts:4] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality test.step |passing @ a.test.ts:9 hook |After Hooks @@ -555,12 +562,12 @@ pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage test.step |grand @ a.test.ts:20 -test.step | parent1 @ a.test.ts:22 -test.step | child1 @ a.test.ts:23 -pw:api | page.click(body) @ a.test.ts:24 -test.step | parent2 @ a.test.ts:27 -test.step | child2 @ a.test.ts:28 -expect | expect.toBeVisible @ a.test.ts:29 +test.step | parent1 @ a.test.ts:22 @stack [a.test.ts:22 | a.test.ts:20] +test.step | child1 @ a.test.ts:23 @stack [a.test.ts:23 | a.test.ts:22 | a.test.ts:20] +pw:api | page.click(body) @ a.test.ts:24 @stack [a.test.ts:24 | a.test.ts:23 | a.test.ts:22 | a.test.ts:20] +test.step | parent2 @ a.test.ts:27 @stack [a.test.ts:27 | a.test.ts:20] +test.step | child2 @ a.test.ts:28 @stack [a.test.ts:28 | a.test.ts:27 | a.test.ts:20] +expect | expect.toBeVisible @ a.test.ts:29 @stack [a.test.ts:29 | a.test.ts:28 | a.test.ts:27 | a.test.ts:20] hook |After Hooks hook | afterEach hook @ a.test.ts:15 test.step | in afterEach @ a.test.ts:16 @@ -701,15 +708,15 @@ test('should propagate nested soft errors', async ({ runInlineTest }) => { hook |Before Hooks test.step |first outer @ a.test.ts:4 test.step |↪ error: Error: expect(received).toBe(expected) // Object.is equality -test.step | first inner @ a.test.ts:5 +test.step | first inner @ a.test.ts:5 @stack [a.test.ts:5 | a.test.ts:4] test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -expect | expect.soft.toBe @ a.test.ts:6 +expect | expect.soft.toBe @ a.test.ts:6 @stack [a.test.ts:6 | a.test.ts:5 | a.test.ts:4] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality test.step |second outer @ a.test.ts:10 test.step |↪ error: Error: expect(received).toBe(expected) // Object.is equality -test.step | second inner @ a.test.ts:11 +test.step | second inner @ a.test.ts:11 @stack [a.test.ts:11 | a.test.ts:10] test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -expect | expect.toBe @ a.test.ts:12 +expect | expect.toBe @ a.test.ts:12 @stack [a.test.ts:12 | a.test.ts:11 | a.test.ts:10] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality hook |After Hooks hook |Worker Cleanup @@ -747,14 +754,14 @@ test('should not propagate nested hard errors', async ({ runInlineTest }) => { expect(stripAnsi(result.output)).toBe(` hook |Before Hooks test.step |first outer @ a.test.ts:4 -test.step | first inner @ a.test.ts:5 -expect | expect.toBe @ a.test.ts:7 +test.step | first inner @ a.test.ts:5 @stack [a.test.ts:5 | a.test.ts:4] +expect | expect.toBe @ a.test.ts:7 @stack [a.test.ts:7 | a.test.ts:5 | a.test.ts:4] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality test.step |second outer @ a.test.ts:13 test.step |↪ error: Error: expect(received).toBe(expected) // Object.is equality -test.step | second inner @ a.test.ts:14 +test.step | second inner @ a.test.ts:14 @stack [a.test.ts:14 | a.test.ts:13] test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -expect | expect.toBe @ a.test.ts:15 +expect | expect.toBe @ a.test.ts:15 @stack [a.test.ts:15 | a.test.ts:14 | a.test.ts:13] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality hook |After Hooks hook |Worker Cleanup @@ -783,7 +790,7 @@ test.step |boxed step @ a.test.ts:3 test.step |↪ error: Error: expect(received).toBe(expected) // Object.is equality @ a.test.ts:4 test.step | at a.test.ts:4:27 test.step | at a.test.ts:3:26 -expect | expect.toBe @ a.test.ts:4 +expect | expect.toBe @ a.test.ts:4 @stack [a.test.ts:4 | a.test.ts:3] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality @ a.test.ts:4 expect | at a.test.ts:4:27 expect | at a.test.ts:3:26 @@ -818,7 +825,7 @@ hook |Before Hooks test.step |boxed step @ a.test.ts:8 test.step |↪ error: Error: expect(received).toBe(expected) // Object.is equality @ a.test.ts:8 test.step | at a.test.ts:8:21 -expect | expect.toBe @ a.test.ts:5 +expect | expect.toBe @ a.test.ts:5 @stack [a.test.ts:5 | a.test.ts:4 | a.test.ts:8] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality @ a.test.ts:8 expect | at a.test.ts:8:21 hook |After Hooks @@ -851,7 +858,7 @@ hook |Before Hooks test.step |boxed step @ a.test.ts:8 test.step |↪ error: Error: expect(received).toBe(expected) // Object.is equality @ a.test.ts:8 test.step | at a.test.ts:8:21 -expect | expect.soft.toBe @ a.test.ts:5 +expect | expect.soft.toBe @ a.test.ts:5 @stack [a.test.ts:5 | a.test.ts:4 | a.test.ts:8] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality @ a.test.ts:8 expect | at a.test.ts:8:21 hook |After Hooks @@ -930,16 +937,16 @@ test('step inside expect.toPass', async ({ runInlineTest }) => { expect(stripAnsi(result.output)).toBe(` hook |Before Hooks test.step |step 1 @ a.test.ts:4 -step | expect.toPass @ a.test.ts:11 +step | expect.toPass @ a.test.ts:11 @stack [a.test.ts:11 | a.test.ts:4] test.step | step 2, attempt: 0 @ a.test.ts:7 test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -expect | expect.toBe @ a.test.ts:9 +expect | expect.toBe @ a.test.ts:9 @stack [a.test.ts:9 | a.test.ts:7] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality test.step | step 2, attempt: 1 @ a.test.ts:7 -expect | expect.toBe @ a.test.ts:9 -test.step | step 3 @ a.test.ts:12 -test.step | step 4 @ a.test.ts:13 -expect | expect.toBe @ a.test.ts:14 +expect | expect.toBe @ a.test.ts:9 @stack [a.test.ts:9 | a.test.ts:7] +test.step | step 3 @ a.test.ts:12 @stack [a.test.ts:12 | a.test.ts:4] +test.step | step 4 @ a.test.ts:13 @stack [a.test.ts:13 | a.test.ts:12 | a.test.ts:4] +expect | expect.toBe @ a.test.ts:14 @stack [a.test.ts:14 | a.test.ts:13 | a.test.ts:12 | a.test.ts:4] hook |After Hooks `); }); @@ -979,13 +986,13 @@ fixture | fixture: page pw:api | browserContext.newPage step |expect.toPass @ a.test.ts:11 pw:api | page.goto(about:blank) @ a.test.ts:6 -test.step | inner step attempt: 0 @ a.test.ts:7 +test.step | inner step attempt: 0 @ a.test.ts:7 @stack [a.test.ts:7 | a.test.ts:5] test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -expect | expect.toBe @ a.test.ts:9 +expect | expect.toBe @ a.test.ts:9 @stack [a.test.ts:9 | a.test.ts:7 | a.test.ts:5] expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality pw:api | page.goto(about:blank) @ a.test.ts:6 -test.step | inner step attempt: 1 @ a.test.ts:7 -expect | expect.toBe @ a.test.ts:9 +test.step | inner step attempt: 1 @ a.test.ts:7 @stack [a.test.ts:7 | a.test.ts:5] +expect | expect.toBe @ a.test.ts:9 @stack [a.test.ts:9 | a.test.ts:7 | a.test.ts:5] hook |After Hooks fixture | fixture: page fixture | fixture: context @@ -1030,13 +1037,13 @@ fixture | fixture: page pw:api | browserContext.newPage step |expect.poll.toHaveLength @ a.test.ts:14 pw:api | page.goto(about:blank) @ a.test.ts:7 -test.step | inner step attempt: 0 @ a.test.ts:8 -expect | expect.toBe @ a.test.ts:10 +test.step | inner step attempt: 0 @ a.test.ts:8 @stack [a.test.ts:8 | a.test.ts:6] +expect | expect.toBe @ a.test.ts:10 @stack [a.test.ts:10 | a.test.ts:8 | a.test.ts:6] expect | expect.toHaveLength @ a.test.ts:6 expect | ↪ error: Error: expect(received).toHaveLength(expected) pw:api | page.goto(about:blank) @ a.test.ts:7 -test.step | inner step attempt: 1 @ a.test.ts:8 -expect | expect.toBe @ a.test.ts:10 +test.step | inner step attempt: 1 @ a.test.ts:8 @stack [a.test.ts:8 | a.test.ts:6] +expect | expect.toBe @ a.test.ts:10 @stack [a.test.ts:10 | a.test.ts:8 | a.test.ts:6] expect | expect.toHaveLength @ a.test.ts:6 hook |After Hooks fixture | fixture: page @@ -1082,13 +1089,13 @@ pw:api | browserContext.newPage pw:api |page.setContent @ a.test.ts:4 step |expect.poll.toBe @ a.test.ts:13 expect | expect.toHaveText @ a.test.ts:7 -test.step | iteration 1 @ a.test.ts:9 -expect | expect.toBeVisible @ a.test.ts:10 +test.step | iteration 1 @ a.test.ts:9 @stack [a.test.ts:9 | a.test.ts:6] +expect | expect.toBeVisible @ a.test.ts:10 @stack [a.test.ts:10 | a.test.ts:9 | a.test.ts:6] expect | expect.toBe @ a.test.ts:6 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toHaveText @ a.test.ts:7 -test.step | iteration 2 @ a.test.ts:9 -expect | expect.toBeVisible @ a.test.ts:10 +test.step | iteration 2 @ a.test.ts:9 @stack [a.test.ts:9 | a.test.ts:6] +expect | expect.toBeVisible @ a.test.ts:10 @stack [a.test.ts:10 | a.test.ts:9 | a.test.ts:6] expect | expect.toBe @ a.test.ts:6 hook |After Hooks fixture | fixture: page @@ -1374,10 +1381,10 @@ pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage test.step |my step 1 @ a.test.ts:4 -test.step | my step 2 @ a.test.ts:5 -pw:api | my group 1 @ a.test.ts:6 -pw:api | my group 2 @ a.test.ts:7 -pw:api | page.setContent @ a.test.ts:8 +test.step | my step 2 @ a.test.ts:5 @stack [a.test.ts:5 | a.test.ts:4] +pw:api | my group 1 @ a.test.ts:6 @stack [a.test.ts:6 | a.test.ts:5 | a.test.ts:4] +pw:api | my group 2 @ a.test.ts:7 @stack [a.test.ts:7 | a.test.ts:5 | a.test.ts:4] +pw:api | page.setContent @ a.test.ts:8 @stack [a.test.ts:8 | a.test.ts:5 | a.test.ts:4] hook |After Hooks fixture | fixture: page fixture | fixture: context @@ -1426,8 +1433,8 @@ pw:api | browserContext.newPage pw:api |page.goto(${server.EMPTY_PAGE}) @ a.test.ts:4 pw:api |page.setContent @ a.test.ts:5 test.step |custom step @ a.test.ts:6 -pw:api | page.waitForResponse @ a.test.ts:7 -pw:api | page.click(div) @ a.test.ts:14 +pw:api | page.waitForResponse @ a.test.ts:7 @stack [a.test.ts:7 | a.test.ts:6] +pw:api | page.click(div) @ a.test.ts:14 @stack [a.test.ts:14 | a.test.ts:6] pw:api | page.content @ a.test.ts:8 pw:api | page.content @ a.test.ts:9 expect | expect.toContainText @ a.test.ts:10 @@ -1509,8 +1516,8 @@ pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage test.step |custom step @ a.test.ts:4 -pw:api | page.route @ a.test.ts:5 -pw:api | page.goto(${server.EMPTY_PAGE}) @ a.test.ts:12 +pw:api | page.route @ a.test.ts:5 @stack [a.test.ts:5 | a.test.ts:4] +pw:api | page.goto(${server.EMPTY_PAGE}) @ a.test.ts:12 @stack [a.test.ts:12 | a.test.ts:4] pw:api | apiResponse.text @ a.test.ts:7 expect | expect.toBe @ a.test.ts:8 pw:api | apiResponse.text @ a.test.ts:9 @@ -1551,9 +1558,9 @@ test('test.step.skip should work', async ({ runInlineTest }) => { hook |Before Hooks test.step.skip|outer step 1 @ a.test.ts:4 test.step |outer step 2 @ a.test.ts:11 -test.step.skip| inner step 2.1 @ a.test.ts:12 -test.step | inner step 2.2 @ a.test.ts:13 -expect | expect.toBe @ a.test.ts:14 +test.step.skip| inner step 2.1 @ a.test.ts:12 @stack [a.test.ts:12 | a.test.ts:11] +test.step | inner step 2.2 @ a.test.ts:13 @stack [a.test.ts:13 | a.test.ts:11] +expect | expect.toBe @ a.test.ts:14 @stack [a.test.ts:14 | a.test.ts:13 | a.test.ts:11] hook |After Hooks `); }); @@ -1581,7 +1588,7 @@ test('skip test.step.skip body', async ({ runInlineTest }) => { expect(stripAnsi(result.output)).toBe(` hook |Before Hooks test.step |outer step 2 @ a.test.ts:5 -test.step.skip| inner step 2 @ a.test.ts:6 +test.step.skip| inner step 2 @ a.test.ts:6 @stack [a.test.ts:6 | a.test.ts:5] expect |expect.toBe @ a.test.ts:10 hook |After Hooks `); @@ -1627,7 +1634,7 @@ fixture | fixture: page pw:api | browserContext.newPage pw:api |page.setContent @ a.test.ts:16 expect |expect.toBeInvisible @ a.test.ts:17 -step | expect.poll.toBe @ a.test.ts:7 +step | expect.poll.toBe @ a.test.ts:7 @stack [a.test.ts:7 | a.test.ts:17] pw:api | locator.isVisible(div) @ a.test.ts:7 expect | expect.toBe @ a.test.ts:7 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality @@ -1641,7 +1648,7 @@ pw:api | locator.isVisible(div) @ a.test.ts:7 expect | expect.toBe @ a.test.ts:7 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality pw:api | locator.isVisible(div) @ a.test.ts:7 -expect | expect.toBe @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 @stack [a.test.ts:7 | a.test.ts:20] pw:api |page.waitForTimeout @ a.test.ts:18 pw:api |page.setContent @ a.test.ts:19 hook |After Hooks @@ -1649,3 +1656,32 @@ fixture | fixture: page fixture | fixture: context `); }); + +test('should provide stack for steps', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': `module.exports = { reporter: './reporter' };`, + 'reporter.ts': stepIndentReporter, + 'helper.ts': ` + import { test } from '@playwright/test'; + + export async function helperStep() { + await test.step('step', () => {}); + } + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + import { helperStep } from './helper'; + + test('pass', async () => { + await helperStep(); + }); + ` + }, { reporter: '', workers: 1 }); + + expect(result.exitCode).toBe(0); + expect(stripAnsi(result.output)).toBe(` +hook |Before Hooks +test.step |step @ helper.ts:5 @stack [helper.ts:5 | a.test.ts:6] +hook |After Hooks +`); +});