Skip to content

Commit e9e6cf5

Browse files
pavelfeldmanaslushnikov
authored andcommitted
cherry-pick(#24106): fix(trace): do not allow after w/o before
Fixes #24087, #23802
1 parent b7dcc2b commit e9e6cf5

File tree

3 files changed

+82
-18
lines changed

3 files changed

+82
-18
lines changed

packages/playwright-core/src/server/trace/recorder/tracing.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type RecordingState = {
6262
networkSha1s: Set<string>,
6363
traceSha1s: Set<string>,
6464
recording: boolean;
65+
callIds: Set<string>;
6566
};
6667

6768
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
@@ -146,7 +147,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
146147
chunkOrdinal: 0,
147148
traceSha1s: new Set(),
148149
networkSha1s: new Set(),
149-
recording: false
150+
recording: false,
151+
callIds: new Set(),
150152
};
151153
const state = this._state;
152154
this._writeChain = fs.promises.mkdir(state.resourcesDir, { recursive: true }).then(() => fs.promises.writeFile(state.networkFile.file, ''));
@@ -171,6 +173,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
171173
buffer: [],
172174
};
173175
state.recording = true;
176+
state.callIds.clear();
174177

175178
if (options.name && options.name !== this._state.traceName)
176179
this._changeTraceName(this._state, options.name);
@@ -352,11 +355,14 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
352355
return Promise.resolve();
353356
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
354357
event.beforeSnapshot = `before@${metadata.id}`;
358+
this._state?.callIds.add(metadata.id);
355359
this._appendTraceEvent(event);
356360
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
357361
}
358362

359363
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
364+
if (!this._state?.callIds.has(metadata.id))
365+
return Promise.resolve();
360366
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
361367
const event = createInputActionTraceEvent(metadata);
362368
if (!event)
@@ -368,9 +374,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
368374
}
369375

370376
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
377+
if (!this._state?.callIds.has(metadata.id))
378+
return;
379+
this._state?.callIds.delete(metadata.id);
371380
const event = createAfterActionTraceEvent(metadata);
372381
if (!event)
373-
return Promise.resolve();
382+
return;
374383
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
375384
event.afterSnapshot = `after@${metadata.id}`;
376385
this._appendTraceEvent(event);

tests/config/utils.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export function suppressCertificateWarning() {
9898
};
9999
}
100100

101-
export async function parseTraceRaw(file: string): Promise<{ events: any[], resources: Map<string, Buffer>, actions: string[], stacks: Map<string, StackFrame[]> }> {
101+
export async function parseTraceRaw(file: string): Promise<{ events: any[], resources: Map<string, Buffer>, actions: string[], actionObjects: ActionTraceEvent[], stacks: Map<string, StackFrame[]> }> {
102102
const zipFS = new ZipFile(file);
103103
const resources = new Map<string, Buffer>();
104104
for (const entry of await zipFS.entries())
@@ -111,14 +111,15 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
111111
for (const line of resources.get(traceFile)!.toString().split('\n')) {
112112
if (line) {
113113
const event = JSON.parse(line) as TraceEvent;
114+
events.push(event);
115+
114116
if (event.type === 'before') {
115117
const action: ActionTraceEvent = {
116118
...event,
117119
type: 'action',
118120
endTime: 0,
119121
log: []
120122
};
121-
events.push(action);
122123
actionMap.set(event.callId, action);
123124
} else if (event.type === 'input') {
124125
const existing = actionMap.get(event.callId);
@@ -131,8 +132,6 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
131132
existing.log = event.log;
132133
existing.error = event.error;
133134
existing.result = event.result;
134-
} else {
135-
events.push(event);
136135
}
137136
}
138137
}
@@ -151,21 +150,17 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
151150
stacks.set(key, value);
152151
}
153152

153+
const actionObjects = [...actionMap.values()];
154+
actionObjects.sort((a, b) => a.startTime - b.startTime);
154155
return {
155156
events,
156157
resources,
157-
actions: eventsToActions(events),
158+
actions: actionObjects.map(a => a.apiName),
159+
actionObjects,
158160
stacks,
159161
};
160162
}
161163

162-
function eventsToActions(events: ActionTraceEvent[]): string[] {
163-
// Trace viewer only shows non-internal non-tracing actions.
164-
return events.filter(e => e.type === 'action')
165-
.sort((a, b) => a.startTime - b.startTime)
166-
.map(e => e.apiName);
167-
}
168-
169164
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: EventTraceEvent[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[] }> {
170165
const backend = new TraceBackend(file);
171166
const traceModel = new TraceModel();

tests/library/tracing.spec.ts

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ test('should not include buffers in the trace', async ({ context, page, server,
112112
await page.goto(server.PREFIX + '/empty.html');
113113
await page.screenshot();
114114
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
115-
const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
116-
const screenshotEvent = events.find(e => e.type === 'action' && e.apiName === 'page.screenshot');
115+
const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
116+
const screenshotEvent = actionObjects.find(a => a.apiName === 'page.screenshot');
117117
expect(screenshotEvent.beforeSnapshot).toBeTruthy();
118118
expect(screenshotEvent.afterSnapshot).toBeTruthy();
119119
expect(screenshotEvent.result).toEqual({});
@@ -526,7 +526,7 @@ test('should hide internal stack frames', async ({ context, page }, testInfo) =>
526526
await context.tracing.stop({ path: tracePath });
527527

528528
const trace = await parseTraceRaw(tracePath);
529-
const actions = trace.events.filter(e => e.type === 'action' && !e.apiName.startsWith('tracing.'));
529+
const actions = trace.actionObjects.filter(a => !a.apiName.startsWith('tracing.'));
530530
expect(actions).toHaveLength(4);
531531
for (const action of actions)
532532
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
@@ -547,7 +547,7 @@ test('should hide internal stack frames in expect', async ({ context, page }, te
547547
await context.tracing.stop({ path: tracePath });
548548

549549
const trace = await parseTraceRaw(tracePath);
550-
const actions = trace.events.filter(e => e.type === 'action' && !e.apiName.startsWith('tracing.'));
550+
const actions = trace.actionObjects.filter(a => !a.apiName.startsWith('tracing.'));
551551
expect(actions).toHaveLength(5);
552552
for (const action of actions)
553553
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
@@ -703,6 +703,66 @@ test('should flush console events on tracing stop', async ({ context, page }, te
703703
expect(events).toHaveLength(100);
704704
});
705705

706+
test('should not emit after w/o before', async ({ browserType, mode }, testInfo) => {
707+
test.skip(mode === 'service', 'Service ignores tracesDir');
708+
709+
const tracesDir = testInfo.outputPath('traces');
710+
const browser = await browserType.launch({ tracesDir });
711+
const context = await browser.newContext();
712+
const page = await context.newPage();
713+
714+
await context.tracing.start({ name: 'name1', snapshots: true });
715+
const evaluatePromise = page.evaluate(() => new Promise(f => (window as any).callback = f)).catch(() => {});
716+
await context.tracing.stopChunk({ path: testInfo.outputPath('trace1.zip') });
717+
expect(fs.existsSync(path.join(tracesDir, 'name1.trace'))).toBe(true);
718+
719+
await context.tracing.startChunk({ name: 'name2' });
720+
await page.evaluateHandle(() => (window as any).callback());
721+
await evaluatePromise;
722+
await context.tracing.stop({ path: testInfo.outputPath('trace2.zip') });
723+
expect(fs.existsSync(path.join(tracesDir, 'name2.trace'))).toBe(true);
724+
725+
await browser.close();
726+
let minCallId = 100000;
727+
const sanitize = (e: any) => {
728+
if (e.type === 'after' || e.type === 'before') {
729+
minCallId = Math.min(minCallId, +e.callId.split('@')[1]);
730+
return {
731+
type: e.type,
732+
callId: +e.callId.split('@')[1] - minCallId,
733+
apiName: e.apiName,
734+
};
735+
}
736+
};
737+
738+
{
739+
const { events } = await parseTraceRaw(testInfo.outputPath('trace1.zip'));
740+
expect(events.map(sanitize).filter(Boolean)).toEqual([
741+
{
742+
type: 'before',
743+
callId: 0,
744+
apiName: 'page.evaluate'
745+
}
746+
]);
747+
}
748+
749+
{
750+
const { events } = await parseTraceRaw(testInfo.outputPath('trace2.zip'));
751+
expect(events.map(sanitize).filter(Boolean)).toEqual([
752+
{
753+
type: 'before',
754+
callId: 6,
755+
apiName: 'page.evaluateHandle'
756+
},
757+
{
758+
type: 'after',
759+
callId: 6,
760+
apiName: undefined
761+
}
762+
]);
763+
}
764+
});
765+
706766
function expectRed(pixels: Buffer, offset: number) {
707767
const r = pixels.readUInt8(offset);
708768
const g = pixels.readUInt8(offset + 1);

0 commit comments

Comments
 (0)