mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 11:46:42 +03:00
fix(trace): do not allow after w/o before (#24106)
Fixes https://github.com/microsoft/playwright/issues/24087, https://github.com/microsoft/playwright/issues/23802
This commit is contained in:
parent
a9560253f8
commit
50ba25e9a1
@ -62,6 +62,7 @@ type RecordingState = {
|
|||||||
networkSha1s: Set<string>,
|
networkSha1s: Set<string>,
|
||||||
traceSha1s: Set<string>,
|
traceSha1s: Set<string>,
|
||||||
recording: boolean;
|
recording: boolean;
|
||||||
|
callIds: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
|
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
|
||||||
@ -146,7 +147,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||||||
chunkOrdinal: 0,
|
chunkOrdinal: 0,
|
||||||
traceSha1s: new Set(),
|
traceSha1s: new Set(),
|
||||||
networkSha1s: new Set(),
|
networkSha1s: new Set(),
|
||||||
recording: false
|
recording: false,
|
||||||
|
callIds: new Set(),
|
||||||
};
|
};
|
||||||
const state = this._state;
|
const state = this._state;
|
||||||
this._writeChain = fs.promises.mkdir(state.resourcesDir, { recursive: true }).then(() => fs.promises.writeFile(state.networkFile.file, ''));
|
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
|
|||||||
buffer: [],
|
buffer: [],
|
||||||
};
|
};
|
||||||
state.recording = true;
|
state.recording = true;
|
||||||
|
state.callIds.clear();
|
||||||
|
|
||||||
if (options.name && options.name !== this._state.traceName)
|
if (options.name && options.name !== this._state.traceName)
|
||||||
this._changeTraceName(this._state, options.name);
|
this._changeTraceName(this._state, options.name);
|
||||||
@ -352,11 +355,14 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
||||||
event.beforeSnapshot = `before@${metadata.id}`;
|
event.beforeSnapshot = `before@${metadata.id}`;
|
||||||
|
this._state?.callIds.add(metadata.id);
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
|
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
||||||
|
if (!this._state?.callIds.has(metadata.id))
|
||||||
|
return Promise.resolve();
|
||||||
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
|
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
|
||||||
const event = createInputActionTraceEvent(metadata);
|
const event = createInputActionTraceEvent(metadata);
|
||||||
if (!event)
|
if (!event)
|
||||||
@ -368,9 +374,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||||
|
if (!this._state?.callIds.has(metadata.id))
|
||||||
|
return;
|
||||||
|
this._state?.callIds.delete(metadata.id);
|
||||||
const event = createAfterActionTraceEvent(metadata);
|
const event = createAfterActionTraceEvent(metadata);
|
||||||
if (!event)
|
if (!event)
|
||||||
return Promise.resolve();
|
return;
|
||||||
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
||||||
event.afterSnapshot = `after@${metadata.id}`;
|
event.afterSnapshot = `after@${metadata.id}`;
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
|
@ -98,7 +98,7 @@ export function suppressCertificateWarning() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseTraceRaw(file: string): Promise<{ events: any[], resources: Map<string, Buffer>, actions: string[], stacks: Map<string, StackFrame[]> }> {
|
export async function parseTraceRaw(file: string): Promise<{ events: any[], resources: Map<string, Buffer>, actions: string[], actionObjects: ActionTraceEvent[], stacks: Map<string, StackFrame[]> }> {
|
||||||
const zipFS = new ZipFile(file);
|
const zipFS = new ZipFile(file);
|
||||||
const resources = new Map<string, Buffer>();
|
const resources = new Map<string, Buffer>();
|
||||||
for (const entry of await zipFS.entries())
|
for (const entry of await zipFS.entries())
|
||||||
@ -111,6 +111,8 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
|
|||||||
for (const line of resources.get(traceFile)!.toString().split('\n')) {
|
for (const line of resources.get(traceFile)!.toString().split('\n')) {
|
||||||
if (line) {
|
if (line) {
|
||||||
const event = JSON.parse(line) as TraceEvent;
|
const event = JSON.parse(line) as TraceEvent;
|
||||||
|
events.push(event);
|
||||||
|
|
||||||
if (event.type === 'before') {
|
if (event.type === 'before') {
|
||||||
const action: ActionTraceEvent = {
|
const action: ActionTraceEvent = {
|
||||||
...event,
|
...event,
|
||||||
@ -118,7 +120,6 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
|
|||||||
endTime: 0,
|
endTime: 0,
|
||||||
log: []
|
log: []
|
||||||
};
|
};
|
||||||
events.push(action);
|
|
||||||
actionMap.set(event.callId, action);
|
actionMap.set(event.callId, action);
|
||||||
} else if (event.type === 'input') {
|
} else if (event.type === 'input') {
|
||||||
const existing = actionMap.get(event.callId);
|
const existing = actionMap.get(event.callId);
|
||||||
@ -131,8 +132,6 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
|
|||||||
existing.log = event.log;
|
existing.log = event.log;
|
||||||
existing.error = event.error;
|
existing.error = event.error;
|
||||||
existing.result = event.result;
|
existing.result = event.result;
|
||||||
} else {
|
|
||||||
events.push(event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,21 +150,17 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
|
|||||||
stacks.set(key, value);
|
stacks.set(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const actionObjects = [...actionMap.values()];
|
||||||
|
actionObjects.sort((a, b) => a.startTime - b.startTime);
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
resources,
|
resources,
|
||||||
actions: eventsToActions(events),
|
actions: actionObjects.map(a => a.apiName),
|
||||||
|
actionObjects,
|
||||||
stacks,
|
stacks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventsToActions(events: ActionTraceEvent[]): string[] {
|
|
||||||
// Trace viewer only shows non-internal non-tracing actions.
|
|
||||||
return events.filter(e => e.type === 'action')
|
|
||||||
.sort((a, b) => a.startTime - b.startTime)
|
|
||||||
.map(e => e.apiName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: EventTraceEvent[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[] }> {
|
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: EventTraceEvent[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[] }> {
|
||||||
const backend = new TraceBackend(file);
|
const backend = new TraceBackend(file);
|
||||||
const traceModel = new TraceModel();
|
const traceModel = new TraceModel();
|
||||||
|
@ -112,8 +112,8 @@ test('should not include buffers in the trace', async ({ context, page, server,
|
|||||||
await page.goto(server.PREFIX + '/empty.html');
|
await page.goto(server.PREFIX + '/empty.html');
|
||||||
await page.screenshot();
|
await page.screenshot();
|
||||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||||
const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||||
const screenshotEvent = events.find(e => e.type === 'action' && e.apiName === 'page.screenshot');
|
const screenshotEvent = actionObjects.find(a => a.apiName === 'page.screenshot');
|
||||||
expect(screenshotEvent.beforeSnapshot).toBeTruthy();
|
expect(screenshotEvent.beforeSnapshot).toBeTruthy();
|
||||||
expect(screenshotEvent.afterSnapshot).toBeTruthy();
|
expect(screenshotEvent.afterSnapshot).toBeTruthy();
|
||||||
expect(screenshotEvent.result).toEqual({});
|
expect(screenshotEvent.result).toEqual({});
|
||||||
@ -526,7 +526,7 @@ test('should hide internal stack frames', async ({ context, page }, testInfo) =>
|
|||||||
await context.tracing.stop({ path: tracePath });
|
await context.tracing.stop({ path: tracePath });
|
||||||
|
|
||||||
const trace = await parseTraceRaw(tracePath);
|
const trace = await parseTraceRaw(tracePath);
|
||||||
const actions = trace.events.filter(e => e.type === 'action' && !e.apiName.startsWith('tracing.'));
|
const actions = trace.actionObjects.filter(a => !a.apiName.startsWith('tracing.'));
|
||||||
expect(actions).toHaveLength(4);
|
expect(actions).toHaveLength(4);
|
||||||
for (const action of actions)
|
for (const action of actions)
|
||||||
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
|
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
|
|||||||
await context.tracing.stop({ path: tracePath });
|
await context.tracing.stop({ path: tracePath });
|
||||||
|
|
||||||
const trace = await parseTraceRaw(tracePath);
|
const trace = await parseTraceRaw(tracePath);
|
||||||
const actions = trace.events.filter(e => e.type === 'action' && !e.apiName.startsWith('tracing.'));
|
const actions = trace.actionObjects.filter(a => !a.apiName.startsWith('tracing.'));
|
||||||
expect(actions).toHaveLength(5);
|
expect(actions).toHaveLength(5);
|
||||||
for (const action of actions)
|
for (const action of actions)
|
||||||
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
|
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
|
|||||||
expect(events).toHaveLength(100);
|
expect(events).toHaveLength(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not emit after w/o before', async ({ browserType, mode }, testInfo) => {
|
||||||
|
test.skip(mode === 'service', 'Service ignores tracesDir');
|
||||||
|
|
||||||
|
const tracesDir = testInfo.outputPath('traces');
|
||||||
|
const browser = await browserType.launch({ tracesDir });
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await context.tracing.start({ name: 'name1', snapshots: true });
|
||||||
|
const evaluatePromise = page.evaluate(() => new Promise(f => (window as any).callback = f)).catch(() => {});
|
||||||
|
await context.tracing.stopChunk({ path: testInfo.outputPath('trace1.zip') });
|
||||||
|
expect(fs.existsSync(path.join(tracesDir, 'name1.trace'))).toBe(true);
|
||||||
|
|
||||||
|
await context.tracing.startChunk({ name: 'name2' });
|
||||||
|
await page.evaluateHandle(() => (window as any).callback());
|
||||||
|
await evaluatePromise;
|
||||||
|
await context.tracing.stop({ path: testInfo.outputPath('trace2.zip') });
|
||||||
|
expect(fs.existsSync(path.join(tracesDir, 'name2.trace'))).toBe(true);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
let minCallId = 100000;
|
||||||
|
const sanitize = (e: any) => {
|
||||||
|
if (e.type === 'after' || e.type === 'before') {
|
||||||
|
minCallId = Math.min(minCallId, +e.callId.split('@')[1]);
|
||||||
|
return {
|
||||||
|
type: e.type,
|
||||||
|
callId: +e.callId.split('@')[1] - minCallId,
|
||||||
|
apiName: e.apiName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const { events } = await parseTraceRaw(testInfo.outputPath('trace1.zip'));
|
||||||
|
expect(events.map(sanitize).filter(Boolean)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'before',
|
||||||
|
callId: 0,
|
||||||
|
apiName: 'page.evaluate'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { events } = await parseTraceRaw(testInfo.outputPath('trace2.zip'));
|
||||||
|
expect(events.map(sanitize).filter(Boolean)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'before',
|
||||||
|
callId: 6,
|
||||||
|
apiName: 'page.evaluateHandle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'after',
|
||||||
|
callId: 6,
|
||||||
|
apiName: undefined
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function expectRed(pixels: Buffer, offset: number) {
|
function expectRed(pixels: Buffer, offset: number) {
|
||||||
const r = pixels.readUInt8(offset);
|
const r = pixels.readUInt8(offset);
|
||||||
const g = pixels.readUInt8(offset + 1);
|
const g = pixels.readUInt8(offset + 1);
|
||||||
|
Loading…
Reference in New Issue
Block a user