Skip to content

Commit 78a84a2

Browse files
committed
feat(step.stack): expose step.stack for better current line in IDE
1 parent eff5cd6 commit 78a84a2

File tree

17 files changed

+170
-96
lines changed

17 files changed

+170
-96
lines changed

docs/src/test-reporter-api/class-teststep.md

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ Error thrown during the step execution, if any.
3838

3939
Parent step, if any.
4040

41+
## property: TestStep.stack
42+
* since: v1.51
43+
- type: <[Array]<[Location]>>
44+
45+
Call stack for this step.
46+
4147
## property: TestStep.startTime
4248
* since: v1.10
4349
- type: <[Date]>

packages/playwright-core/src/utils/timeoutRunner.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717
import { monotonicTime } from './';
1818

1919
export async function raceAgainstDeadline<T>(cb: () => Promise<T>, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> {
20+
// Avoid indirections to preserve better stacks.
21+
if (deadline === 0) {
22+
const result = await cb();
23+
return { result, timedOut: false };
24+
}
25+
2026
let timer: NodeJS.Timeout | undefined;
2127
return Promise.race([
2228
cb().then(result => {
2329
return { result, timedOut: false };
2430
}),
2531
new Promise<{ timedOut: true }>(resolve => {
26-
const kMaxDeadline = 2147483647; // 2^31-1
27-
const timeout = (deadline || kMaxDeadline) - monotonicTime();
32+
const timeout = deadline - monotonicTime();
2833
timer = setTimeout(() => resolve({ timedOut: true }), timeout);
2934
}),
3035
]).finally(() => {

packages/playwright/src/common/ipc.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export type StepBeginPayload = {
100100
title: string;
101101
category: string;
102102
wallTime: number; // milliseconds since unix epoch
103-
location?: { file: string, line: number, column: number };
103+
stack: { file: string, line: number, column: number }[];
104104
};
105105

106106
export type StepEndPayload = {

packages/playwright/src/common/testType.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class TestTypeImpl {
6969
this.test = test;
7070
}
7171

72-
private _currentSuite(location: Location, title: string): Suite | undefined {
72+
private _currentSuite(title: string): Suite | undefined {
7373
const suite = currentlyLoadingFileSuite();
7474
if (!suite) {
7575
throw new Error([
@@ -86,7 +86,7 @@ export class TestTypeImpl {
8686

8787
private _createTest(type: 'default' | 'only' | 'skip' | 'fixme' | 'fail' | 'fail.only', location: Location, title: string, fnOrDetails: Function | TestDetails, fn?: Function) {
8888
throwIfRunningInsideJest();
89-
const suite = this._currentSuite(location, 'test()');
89+
const suite = this._currentSuite('test()');
9090
if (!suite)
9191
return;
9292

@@ -117,7 +117,7 @@ export class TestTypeImpl {
117117

118118
private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, titleOrFn: string | Function, fnOrDetails?: TestDetails | Function, fn?: Function) {
119119
throwIfRunningInsideJest();
120-
const suite = this._currentSuite(location, 'test.describe()');
120+
const suite = this._currentSuite('test.describe()');
121121
if (!suite)
122122
return;
123123

@@ -169,7 +169,7 @@ export class TestTypeImpl {
169169
}
170170

171171
private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', location: Location, title: string | Function, fn?: Function) {
172-
const suite = this._currentSuite(location, `test.${name}()`);
172+
const suite = this._currentSuite(`test.${name}()`);
173173
if (!suite)
174174
return;
175175
if (typeof title === 'function') {
@@ -182,7 +182,7 @@ export class TestTypeImpl {
182182

183183
private _configure(location: Location, options: { mode?: 'default' | 'parallel' | 'serial', retries?: number, timeout?: number }) {
184184
throwIfRunningInsideJest();
185-
const suite = this._currentSuite(location, `test.describe.configure()`);
185+
const suite = this._currentSuite(`test.describe.configure()`);
186186
if (!suite)
187187
return;
188188

@@ -252,7 +252,7 @@ export class TestTypeImpl {
252252
}
253253

254254
private _use(location: Location, fixtures: Fixtures) {
255-
const suite = this._currentSuite(location, `test.use()`);
255+
const suite = this._currentSuite(`test.use()`);
256256
if (!suite)
257257
return;
258258
suite._use.push({ fixtures, location });
@@ -263,11 +263,11 @@ export class TestTypeImpl {
263263
if (!testInfo)
264264
throw new Error(`test.step() can only be called from a test`);
265265
if (expectation === 'skip') {
266-
const step = testInfo._addStep({ category: 'test.step.skip', title, location: options.location, box: options.box });
266+
const step = testInfo._addStep({ category: 'test.step.skip', title, box: options.box }, undefined, options.location ? [options.location] : undefined);
267267
step.complete({});
268268
return undefined as T;
269269
}
270-
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
270+
const step = testInfo._addStep({ category: 'test.step', title, box: options.box }, undefined, options.location ? [options.location] : undefined);
271271
return await zones.run('stepZone', step, async () => {
272272
try {
273273
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;

packages/playwright/src/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,11 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
273273
}
274274
// In the general case, create a step for each api call and connect them through the stepId.
275275
const step = testInfo._addStep({
276-
location: data.frames[0],
277276
category: 'pw:api',
278277
title: renderApiCall(data.apiName, data.params),
279278
apiName: data.apiName,
280279
params: data.params,
281-
}, tracingGroupSteps[tracingGroupSteps.length - 1]);
280+
}, tracingGroupSteps[tracingGroupSteps.length - 1], data.frames);
282281
data.userData = step;
283282
data.stepId = step.stepId;
284283
if (data.apiName === 'tracing.group')

packages/playwright/src/isomorphic/teleReceiver.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ export type JsonTestStepStart = {
101101
title: string;
102102
category: string,
103103
startTime: number;
104-
location?: reporterTypes.Location;
104+
// Best effort to keep step struct small.
105+
stack?: reporterTypes.Location | reporterTypes.Location[];
105106
};
106107

107108
export type JsonTestStepEnd = {
@@ -249,8 +250,8 @@ export class TeleReporterReceiver {
249250
const result = test.results.find(r => r._id === resultId)!;
250251
const parentStep = payload.parentStepId ? result._stepMap.get(payload.parentStepId) : undefined;
251252

252-
const location = this._absoluteLocation(payload.location);
253-
const step = new TeleTestStep(payload, parentStep, location, result);
253+
const stack = Array.isArray(payload.stack) ? payload.stack.map(l => this._absoluteLocation(l)) : this._absoluteLocation(payload.stack);
254+
const step = new TeleTestStep(payload, parentStep, stack, result);
254255
if (parentStep)
255256
parentStep.steps.push(step);
256257
else
@@ -426,8 +427,8 @@ export class TeleSuite implements reporterTypes.Suite {
426427
}
427428

428429
allTests(): reporterTypes.TestCase[] {
429-
const result: reporterTypes.TestCase[] = [];
430-
const visit = (suite: reporterTypes.Suite) => {
430+
const result: TeleTestCase[] = [];
431+
const visit = (suite: TeleSuite) => {
431432
for (const entry of suite.entries()) {
432433
if (entry.type === 'test')
433434
result.push(entry);
@@ -511,6 +512,7 @@ class TeleTestStep implements reporterTypes.TestStep {
511512
title: string;
512513
category: string;
513514
location: reporterTypes.Location | undefined;
515+
stack: reporterTypes.Location[];
514516
parent: reporterTypes.TestStep | undefined;
515517
duration: number = -1;
516518
steps: reporterTypes.TestStep[] = [];
@@ -521,10 +523,11 @@ class TeleTestStep implements reporterTypes.TestStep {
521523

522524
private _startTime: number = 0;
523525

524-
constructor(payload: JsonTestStepStart, parentStep: reporterTypes.TestStep | undefined, location: reporterTypes.Location | undefined, result: TeleTestResult) {
526+
constructor(payload: JsonTestStepStart, parentStep: reporterTypes.TestStep | undefined, stackOrLocation: reporterTypes.Location | reporterTypes.Location[] | undefined, result: TeleTestResult) {
525527
this.title = payload.title;
526528
this.category = payload.category;
527-
this.location = location;
529+
this.stack = Array.isArray(stackOrLocation) ? stackOrLocation : (stackOrLocation ? [stackOrLocation] : []);
530+
this.location = this.stack[0];
528531
this.parent = parentStep;
529532
this._startTime = payload.startTime;
530533
this._result = result;

packages/playwright/src/isomorphic/testServerConnection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class WebSocketTestServerTransport implements TestServerTransport {
3737
}
3838

3939
onmessage(listener: (message: string) => void) {
40-
this._ws.addEventListener('message', event => listener(event.data));
40+
this._ws.addEventListener('message', event => listener(String(event.data)));
4141
}
4242

4343
onopen(listener: () => void) {

packages/playwright/src/reporters/blob.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type BlobReporterOptions = {
3434
_commandHash: string;
3535
};
3636

37-
export const currentBlobReportVersion = 2;
37+
export const currentBlobReportVersion = 3;
3838

3939
export type BlobReportMetadata = {
4040
version: number;

packages/playwright/src/reporters/merge.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export async function createMergedReport(config: FullConfigInternal, dir: string
7575
await dispatchEvents(eventData.prologue);
7676
for (const { reportFile, eventPatchers, metadata } of eventData.reports) {
7777
const reportJsonl = await fs.promises.readFile(reportFile);
78-
const events = parseTestEvents(reportJsonl);
78+
let events = parseTestEvents(reportJsonl);
79+
events = modernizer.modernize(metadata.version, events);
7980
new JsonStringInternalizer(stringPool).traverse(events);
8081
eventPatchers.patchers.push(new AttachmentPathPatcher(dir));
8182
if (metadata.name)
@@ -480,7 +481,8 @@ class PathSeparatorPatcher {
480481
}
481482
if (jsonEvent.method === 'onStepBegin') {
482483
const step = jsonEvent.params.step as JsonTestStepStart;
483-
this._updateLocation(step.location);
484+
for (const stackFrame of Array.isArray(step.stack) ? step.stack : [step.stack])
485+
this._updateLocation(stackFrame);
484486
return;
485487
}
486488
if (jsonEvent.method === 'onStepEnd') {
@@ -589,6 +591,14 @@ class BlobModernizer {
589591
return event;
590592
});
591593
}
594+
595+
_modernize_2_to_3(events: JsonEvent[]): JsonEvent[] {
596+
return events.map(event => {
597+
if (event.method === 'onStepBegin')
598+
(event.params.step as JsonTestStepStart).stack = event.params.step.location;
599+
return event;
600+
});
601+
}
592602
}
593603

594604
const modernizer = new BlobModernizer();

packages/playwright/src/reporters/teleEmitter.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export class TeleReporterEmitter implements ReporterV2 {
247247
title: step.title,
248248
category: step.category,
249249
startTime: +step.startTime,
250-
location: this._relativeLocation(step.location),
250+
stack: this._relativeStack(step.stack),
251251
};
252252
}
253253

@@ -260,6 +260,14 @@ export class TeleReporterEmitter implements ReporterV2 {
260260
};
261261
}
262262

263+
private _relativeStack(stack: reporterTypes.Location[]): undefined | reporterTypes.Location | reporterTypes.Location[] {
264+
if (!stack.length)
265+
return undefined;
266+
if (stack.length === 1)
267+
return this._relativeLocation(stack[0]);
268+
return stack.map(frame => this._relativeLocation(frame));
269+
}
270+
263271
private _relativeLocation(location: reporterTypes.Location): reporterTypes.Location;
264272
private _relativeLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
265273
private _relativeLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {

packages/playwright/src/runner/dispatcher.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,8 @@ class JobDispatcher {
321321
duration: -1,
322322
steps: [],
323323
attachments: [],
324-
location: params.location,
324+
location: params.stack[0],
325+
stack: params.stack,
325326
};
326327
steps.set(params.stepId, step);
327328
(parentStep || result).steps.push(step);

packages/playwright/src/util.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ export function filterStackFile(file: string) {
5151
return true;
5252
}
5353

54-
export function filteredStackTrace(rawStack: RawStack): StackFrame[] {
54+
export function filteredStackTrace(rawStack: RawStack): Location[] {
5555
const frames: StackFrame[] = [];
5656
for (const line of rawStack) {
5757
const frame = parseStackTraceLine(line);
5858
if (!frame || !frame.file)
5959
continue;
6060
if (!filterStackFile(frame.file))
6161
continue;
62-
frames.push(frame);
62+
frames.push({ file: frame.file, line: frame.line, column: frame.column });
6363
}
6464
return frames;
6565
}

packages/playwright/src/worker/testInfo.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import type { Annotation, FullConfigInternal, FullProjectInternal } from '../com
2626
import type { FullConfig, Location } from '../../types/testReporter';
2727
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
2828
import { TestTracing } from './testTracing';
29-
import type { StackFrame } from '@protocol/channels';
3029
import { testInfoError } from './util';
3130

3231
export interface TestStepInternal {
@@ -35,8 +34,8 @@ export interface TestStepInternal {
3534
stepId: string;
3635
title: string;
3736
category: string;
38-
location?: Location;
39-
boxedStack?: StackFrame[];
37+
stack: Location[];
38+
boxedStack?: Location[];
4039
steps: TestStepInternal[];
4140
endWallTime?: number;
4241
apiName?: string;
@@ -244,7 +243,7 @@ export class TestInfoImpl implements TestInfo {
244243
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
245244
}
246245

247-
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices'>, parentStep?: TestStepInternal): TestStepInternal {
246+
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices' | 'stack'>, parentStep?: TestStepInternal, stackOverride?: Location[]): TestStepInternal {
248247
const stepId = `${data.category}@${++this._lastStepId}`;
249248

250249
if (data.isStage) {
@@ -255,18 +254,20 @@ export class TestInfoImpl implements TestInfo {
255254
parentStep = this._parentStep();
256255
}
257256

258-
const filteredStack = filteredStackTrace(captureRawStack());
257+
const filteredStack = stackOverride ? stackOverride : filteredStackTrace(captureRawStack());
259258
data.boxedStack = parentStep?.boxedStack;
259+
let stack = filteredStack;
260260
if (!data.boxedStack && data.box) {
261261
data.boxedStack = filteredStack.slice(1);
262-
data.location = data.location || data.boxedStack[0];
262+
// Only steps with box: true get boxed stack. Inner steps have original stack for better traceability.
263+
stack = data.boxedStack;
263264
}
264-
data.location = data.location || filteredStack[0];
265265

266266
const attachmentIndices: number[] = [];
267267
const step: TestStepInternal = {
268268
stepId,
269269
...data,
270+
stack,
270271
steps: [],
271272
attachmentIndices,
272273
complete: result => {
@@ -319,10 +320,10 @@ export class TestInfoImpl implements TestInfo {
319320
title: data.title,
320321
category: data.category,
321322
wallTime: Date.now(),
322-
location: data.location,
323+
stack,
323324
};
324325
this._onStepBegin(payload);
325-
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.category, data.apiName || data.title, data.params, data.location ? [data.location] : []);
326+
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.category, data.apiName || data.title, data.params, stack);
326327
return step;
327328
}
328329

@@ -351,7 +352,7 @@ export class TestInfoImpl implements TestInfo {
351352
const location = stage.runnable?.location ? ` at "${formatLocation(stage.runnable.location)}"` : ``;
352353
debugTest(`started stage "${stage.title}"${location}`);
353354
}
354-
stage.step = stage.stepInfo ? this._addStep({ ...stage.stepInfo, title: stage.title, isStage: true }) : undefined;
355+
stage.step = stage.stepInfo ? this._addStep({ category: stage.stepInfo.category, title: stage.title, isStage: true }, undefined, stage.stepInfo.location ? [stage.stepInfo.location] : undefined) : undefined;
355356

356357
try {
357358
await this._timeoutManager.withRunnable(stage.runnable, async () => {

packages/playwright/src/worker/workerMain.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ export class WorkerMain extends ProcessRunner {
555555
let firstError: Error | undefined;
556556
const hooks = suites.map(suite => this._collectHooksAndModifiers(suite, type, testInfo)).flat();
557557
for (const hook of hooks) {
558-
const runnable = { type: hook.type, location: hook.location, slot };
558+
const runnable = { type: hook.type, stack: [hook.location], slot };
559559
if (testInfo._timeoutManager.isTimeExhaustedFor(runnable)) {
560560
// Do not run hooks that will timeout right away.
561561
continue;

packages/playwright/types/testReporter.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,11 @@ export interface TestStep {
747747
*/
748748
parent?: TestStep;
749749

750+
/**
751+
* Call stack for this step.
752+
*/
753+
stack: Array<Location>;
754+
750755
/**
751756
* Start time of this particular test step.
752757
*/

tests/playwright-test/reporter-blob.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1432,7 +1432,7 @@ test('blob report should include version', async ({ runInlineTest }) => {
14321432

14331433
const events = await extractReport(test.info().outputPath('blob-report', 'report.zip'), test.info().outputPath('tmp'));
14341434
const metadataEvent = events.find(e => e.method === 'onBlobReportMetadata');
1435-
expect(metadataEvent.params.version).toBe(2);
1435+
expect(metadataEvent.params.version).toBe(3);
14361436
expect(metadataEvent.params.userAgent).toBe(getUserAgent());
14371437
});
14381438

0 commit comments

Comments
 (0)