fix(trace): duplicate network requests from beforeAll in serial mode

This commit is contained in:
Dmitry Gozman 2024-12-13 10:37:27 +00:00
parent d029b03d9f
commit 9727bc1e2a
9 changed files with 66 additions and 5 deletions

View file

@ -46,8 +46,12 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
await this._startCollectingStacks(traceName); await this._startCollectingStacks(traceName);
} }
async startChunk(options: { name?: string, title?: string } = {}) { async startChunk(options: { name?: string, title?: string, _resetNetwork?: boolean } = {}) {
const { traceName } = await this._channel.tracingStartChunk(options); const { traceName } = await this._channel.tracingStartChunk({
name: options.name,
title: options.title,
resetNetwork: options._resetNetwork,
});
await this._startCollectingStacks(traceName); await this._startCollectingStacks(traceName);
} }

View file

@ -2294,6 +2294,7 @@ scheme.TracingTracingStartResult = tOptional(tObject({}));
scheme.TracingTracingStartChunkParams = tObject({ scheme.TracingTracingStartChunkParams = tObject({
name: tOptional(tString), name: tOptional(tString),
title: tOptional(tString), title: tOptional(tString),
resetNetwork: tOptional(tBoolean),
}); });
scheme.TracingTracingStartChunkResult = tObject({ scheme.TracingTracingStartChunkResult = tObject({
traceName: tString, traceName: tString,

View file

@ -158,7 +158,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._harTracer.start({ omitScripts: !options.live }); this._harTracer.start({ omitScripts: !options.live });
} }
async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> { async startChunk(options: { name?: string, title?: string, resetNetwork?: boolean } = {}): Promise<{ traceName: string }> {
if (this._state && this._state.recording) if (this._state && this._state.recording)
await this.stopChunk({ mode: 'discard' }); await this.stopChunk({ mode: 'discard' });
@ -184,6 +184,9 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
}; };
this._appendTraceEvent(event); this._appendTraceEvent(event);
if (options.resetNetwork)
this._fs.writeFile(this._state.networkFile, '');
this._context.instrumentation.addListener(this, this._context); this._context.instrumentation.addListener(this, this._context);
this._eventListeners.push( this._eventListeners.push(
eventsHelper.addEventListener(this._context, BrowserContext.Events.Console, this._onConsoleMessage.bind(this)), eventsHelper.addEventListener(this._context, BrowserContext.Events.Console, this._onConsoleMessage.bind(this)),

View file

@ -685,14 +685,17 @@ class ArtifactsRecorder {
private async _startTraceChunkOnContextCreation(tracing: Tracing) { private async _startTraceChunkOnContextCreation(tracing: Tracing) {
const options = this._testInfo._tracing.traceOptions(); const options = this._testInfo._tracing.traceOptions();
const seenInThisTestSymbol = this._testInfo._tracing.uniqueSymbol();
if (options) { if (options) {
const seenInThisTest = !!(tracing as any)[seenInThisTestSymbol];
(tracing as any)[seenInThisTestSymbol] = true;
const title = this._testInfo._tracing.traceTitle(); const title = this._testInfo._tracing.traceTitle();
const name = this._testInfo._tracing.generateNextTraceRecordingName(); const name = this._testInfo._tracing.generateNextTraceRecordingName();
if (!(tracing as any)[kTracingStarted]) { if (!(tracing as any)[kTracingStarted]) {
await tracing.start({ ...options, title, name }); await tracing.start({ ...options, title, name });
(tracing as any)[kTracingStarted] = true; (tracing as any)[kTracingStarted] = true;
} else { } else {
await tracing.startChunk({ title, name }); await tracing.startChunk({ title, name, _resetNetwork: seenInThisTest } as any);
} }
} else { } else {
if ((tracing as any)[kTracingStarted]) { if ((tracing as any)[kTracingStarted]) {

View file

@ -43,6 +43,7 @@ export class TestTracing {
private _artifactsDir: string; private _artifactsDir: string;
private _tracesDir: string; private _tracesDir: string;
private _contextCreatedEvent: trace.ContextCreatedTraceEvent; private _contextCreatedEvent: trace.ContextCreatedTraceEvent;
private _uniqueSymbol: symbol;
constructor(testInfo: TestInfoImpl, artifactsDir: string) { constructor(testInfo: TestInfoImpl, artifactsDir: string) {
this._testInfo = testInfo; this._testInfo = testInfo;
@ -59,6 +60,7 @@ export class TestTracing {
monotonicTime: monotonicTime(), monotonicTime: monotonicTime(),
sdkLanguage: 'javascript', sdkLanguage: 'javascript',
}; };
this._uniqueSymbol = Symbol('unique');
this._appendTraceEvent(this._contextCreatedEvent); this._appendTraceEvent(this._contextCreatedEvent);
} }
@ -140,6 +142,10 @@ export class TestTracing {
return this._options; return this._options;
} }
uniqueSymbol() {
return this._uniqueSymbol;
}
async stopIfNeeded() { async stopIfNeeded() {
if (!this._options) if (!this._options)
return; return;

View file

@ -4109,10 +4109,12 @@ export type TracingTracingStartResult = void;
export type TracingTracingStartChunkParams = { export type TracingTracingStartChunkParams = {
name?: string, name?: string,
title?: string, title?: string,
resetNetwork?: boolean,
}; };
export type TracingTracingStartChunkOptions = { export type TracingTracingStartChunkOptions = {
name?: string, name?: string,
title?: string, title?: string,
resetNetwork?: boolean,
}; };
export type TracingTracingStartChunkResult = { export type TracingTracingStartChunkResult = {
traceName: string, traceName: string,

View file

@ -3196,6 +3196,7 @@ Tracing:
parameters: parameters:
name: string? name: string?
title: string? title: string?
resetNetwork: boolean?
returns: returns:
traceName: string traceName: string

View file

@ -23,6 +23,7 @@ import { TraceModel } from '../../packages/trace-viewer/src/sw/traceModel';
import type { ActionTreeItem } from '../../packages/trace-viewer/src/ui/modelUtil'; import type { ActionTreeItem } from '../../packages/trace-viewer/src/ui/modelUtil';
import { buildActionTree, MultiTraceModel } from '../../packages/trace-viewer/src/ui/modelUtil'; import { buildActionTree, MultiTraceModel } from '../../packages/trace-viewer/src/ui/modelUtil';
import type { ActionTraceEvent, ConsoleMessageTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace'; import type { ActionTraceEvent, ConsoleMessageTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace';
import type { ResourceSnapshot } from '@trace/snapshot';
import style from 'ansi-styles'; import style from 'ansi-styles';
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> { export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
@ -157,7 +158,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
}; };
} }
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> { export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[], network: ResourceSnapshot[] }> {
const backend = new TraceBackend(file); const backend = new TraceBackend(file);
const traceModel = new TraceModel(); const traceModel = new TraceModel();
await traceModel.load(backend, () => {}); await traceModel.load(backend, () => {});
@ -179,6 +180,7 @@ export async function parseTrace(file: string): Promise<{ resources: Map<string,
model, model,
traceModel, traceModel,
actionTree, actionTree,
network: model.resources,
}; };
} }

View file

@ -1271,3 +1271,42 @@ test('should record trace after fixture teardown timeout', {
// Check console events to make sure that library trace is recorded. // Check console events to make sure that library trace is recorded.
expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' })); expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' }));
}); });
test.only('should not duplicate network from beforeAll hook', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33106' },
}, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
let shared;
test.beforeAll(async ({ browser }) => {
shared = await browser.newPage();
await shared.route('**/*', route => route.fulfill({ body: 'hello' }));
await shared.goto('https://playwright.dev/');
});
test('pass1', async ({ page }) => {
await page.route('**/*', route => route.fulfill({ body: 'hello' }));
await page.goto('https://playwright1.dev/');
});
test('pass2', async ({ page }) => {
await page.route('**/*', route => route.fulfill({ body: 'hello' }));
await page.goto('https://playwright2.dev/');
});
test.afterAll(async ({}) => {
await shared.close();
});
`,
}, { trace: 'on' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
const trace1 = await parseTrace(test.info().outputPath('test-results', 'a-pass1', 'trace.zip'));
expect(trace1.network.map(r => r.request.url).sort()).toEqual(['https://playwright.dev/', 'https://playwright1.dev/']);
const trace2 = await parseTrace(test.info().outputPath('test-results', 'a-pass2', 'trace.zip'));
expect(trace2.network.map(r => r.request.url).sort()).toEqual(['https://playwright.dev/', 'https://playwright2.dev/']);
});