diff --git a/packages/playwright-core/src/server/har/harRecorder.ts b/packages/playwright-core/src/server/har/harRecorder.ts index 4165a8ec87..e623587e57 100644 --- a/packages/playwright-core/src/server/har/harRecorder.ts +++ b/packages/playwright-core/src/server/har/harRecorder.ts @@ -20,6 +20,7 @@ import { Artifact } from '../artifact'; import type { BrowserContext } from '../browserContext'; import type * as har from '@trace/har'; import { HarTracer } from './harTracer'; +import type { HarTracerDelegate } from './harTracer'; import type * as channels from '@protocol/channels'; import { yazl } from '../../zipBundle'; import type { ZipFile } from '../../zipBundle'; @@ -28,7 +29,7 @@ import type EventEmitter from 'events'; import { createGuid } from '../../utils'; import type { Page } from '../page'; -export class HarRecorder { +export class HarRecorder implements HarTracerDelegate { private _artifact: Artifact; private _isFlushed: boolean = false; private _tracer: HarTracer; diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 4d26b1a924..152f793d1f 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -182,13 +182,7 @@ export class HarTracer { return null; if (!this._options.waitForContentOnStop) return; - const race = Promise.race([ - new Promise(f => target.on('close', () => { - this._barrierPromises.delete(race); - f(); - })), - promise - ]) as Promise; + const race = target.openScope.safeRace(promise); this._barrierPromises.add(race); race.then(() => this._barrierPromises.delete(race)); } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index a0b98a9d3f..318835f52a 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -141,6 +141,7 @@ export class Page extends SdkObject { private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = []; readonly _disconnectedScope = new LongStandingScope(); readonly _crashedScope = new LongStandingScope(); + readonly openScope = new LongStandingScope(); readonly _browserContext: BrowserContext; readonly keyboard: input.Keyboard; readonly mouse: input.Mouse; @@ -276,6 +277,7 @@ export class Page extends SdkObject { this.emit(Page.Events.Close); this._closedPromise.resolve(); this.instrumentation.onPageClose(this); + this.openScope.close('Page closed'); } _didCrash() { @@ -284,6 +286,7 @@ export class Page extends SdkObject { this.emit(Page.Events.Crash); this._crashedScope.close('Page crashed'); this.instrumentation.onPageClose(this); + this.openScope.close('Page closed'); } _didDisconnect() { @@ -292,6 +295,7 @@ export class Page extends SdkObject { assert(!this._disconnected, 'Page disconnected twice'); this._disconnected = true; this._disconnectedScope.close('Page closed'); + this.openScope.close('Page closed'); } async _onFileChooserOpened(handle: dom.ElementHandle) { @@ -711,6 +715,7 @@ export class Worker extends SdkObject { private _executionContextPromise: Promise; private _executionContextCallback: (value: js.ExecutionContext) => void; _existingExecutionContext: js.ExecutionContext | null = null; + readonly openScope = new LongStandingScope(); constructor(parent: SdkObject, url: string) { super(parent, 'worker'); @@ -732,6 +737,7 @@ export class Worker extends SdkObject { if (this._existingExecutionContext) this._existingExecutionContext.contextDestroyed('Worker was closed'); this.emit(Worker.Events.Close, this); + this.openScope.close('Worker closed'); } async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise { diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 59bbba4e61..ae6bba50ad 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -82,6 +82,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private _tracesTmpDir: string | undefined; private _allResources = new Set(); private _contextCreatedEvent: trace.ContextCreatedTraceEvent; + private _pendingHarEntries = new Set(); constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) { super(context, 'tracing'); @@ -230,6 +231,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps if (this._state.recording) throw new Error(`Must stop trace file before stopping tracing`); this._harTracer.stop(); + this.flushHarEntries(); await this._fs.syncAndGetError(); this._state = undefined; } @@ -272,6 +274,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps if (this._state.options.snapshots) await this._snapshotter?.stop(); + this.flushHarEntries(); + // Network file survives across chunks, make a snapshot before returning the resulting entries. // We should pick a name starting with "traceName" and ending with .network. // Something like someSuffixHere.network. @@ -387,14 +391,28 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps } onEntryStarted(entry: har.Entry) { + this._pendingHarEntries.add(entry); } onEntryFinished(entry: har.Entry) { + this._pendingHarEntries.delete(entry); const event: trace.ResourceSnapshotTraceEvent = { type: 'resource-snapshot', snapshot: entry }; const visited = visitTraceEvent(event, this._state!.networkSha1s); this._fs.appendFile(this._state!.networkFile, JSON.stringify(visited) + '\n', true /* flush */); } + flushHarEntries() { + const harLines: string[] = []; + for (const entry of this._pendingHarEntries) { + const event: trace.ResourceSnapshotTraceEvent = { type: 'resource-snapshot', snapshot: entry }; + const visited = visitTraceEvent(event, this._state!.networkSha1s); + harLines.push(JSON.stringify(visited)); + } + this._pendingHarEntries.clear(); + if (harLines.length) + this._fs.appendFile(this._state!.networkFile, harLines.join('\n') + '\n', true /* flush */); + } + onContentBlob(sha1: string, buffer: Buffer) { this._appendResource(sha1, buffer); }