diff --git a/packages/puppeteer-core/src/common/FrameManager.ts b/packages/puppeteer-core/src/common/FrameManager.ts index a22de8fe..68a9b4b7 100644 --- a/packages/puppeteer-core/src/common/FrameManager.ts +++ b/packages/puppeteer-core/src/common/FrameManager.ts @@ -113,7 +113,7 @@ export class FrameManager extends EventEmitter { super(); this.#client = client; this.#page = page; - this.#networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); + this.#networkManager = new NetworkManager(ignoreHTTPSErrors, this); this.#timeoutSettings = timeoutSettings; this.setupEventListeners(this.#client); client.once(CDPSessionEmittedEvents.Disconnected, () => { @@ -176,12 +176,16 @@ export class FrameManager extends EventEmitter { this.#onClientDisconnect().catch(debugError); }); await this.initialize(client); - await this.#networkManager.updateClient(client); + await this.#networkManager.addClient(client); if (frame) { frame.emit(FrameEmittedEvents.FrameSwappedByActivation); } } + async registerSecondaryPage(client: CDPSessionImpl): Promise { + await this.#networkManager.addClient(client); + } + private setupEventListeners(session: CDPSession) { session.on('Page.frameAttached', event => { this.#onFrameAttached(session, event.frameId, event.parentFrameId); @@ -222,13 +226,13 @@ export class FrameManager extends EventEmitter { }); } - async initialize(client: CDPSession = this.#client): Promise { + async initialize(client: CDPSession): Promise { try { + const networkInit = this.#networkManager.addClient(client); const result = await Promise.all([ client.send('Page.enable'), client.send('Page.getFrameTree'), ]); - const {frameTree} = result[1]; this.#handleFrameTree(client, frameTree); await Promise.all([ @@ -236,10 +240,7 @@ export class FrameManager extends EventEmitter { client.send('Runtime.enable').then(() => { return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME); }), - // TODO: Network manager is not aware of OOP iframes yet. - client === this.#client - ? this.#networkManager.initialize() - : Promise.resolve(), + networkInit, ]); } catch (error) { // The target might have been closed before the initialization finished. @@ -295,7 +296,7 @@ export class FrameManager extends EventEmitter { frame.updateClient(target._session()!); } this.setupEventListeners(target._session()!); - void this.initialize(target._session()); + void this.initialize(target._session()!); } /** diff --git a/packages/puppeteer-core/src/common/NetworkManager.test.ts b/packages/puppeteer-core/src/common/NetworkManager.test.ts index 8537e738..a7e1d2aa 100644 --- a/packages/puppeteer-core/src/common/NetworkManager.test.ts +++ b/packages/puppeteer-core/src/common/NetworkManager.test.ts @@ -45,11 +45,12 @@ class MockCDPSession extends EventEmitter { describe('NetworkManager', () => { it('should process extra info on multiple redirects', async () => { const mockCDPSession = new MockCDPSession(); - new NetworkManager(mockCDPSession, true, { + const manager = new NetworkManager(true, { frame(): Frame | null { return null; }, }); + await manager.addClient(mockCDPSession); mockCDPSession.emit('Network.requestWillBeSent', { requestId: '7760711DEFCFA23132D98ABA6B4E175C', loaderId: '7760711DEFCFA23132D98ABA6B4E175C', @@ -476,11 +477,12 @@ describe('NetworkManager', () => { }); it(`should handle "double pause" (crbug.com/1196004) Fetch.requestPaused events for the same Network.requestWillBeSent event`, async () => { const mockCDPSession = new MockCDPSession(); - const manager = new NetworkManager(mockCDPSession, true, { + const manager = new NetworkManager(true, { frame(): Frame | null { return null; }, }); + await manager.addClient(mockCDPSession); await manager.setRequestInterception(true); const requests: HTTPRequest[] = []; @@ -562,11 +564,12 @@ describe('NetworkManager', () => { }); it(`should handle Network.responseReceivedExtraInfo event after Network.responseReceived event (github.com/puppeteer/puppeteer/issues/8234)`, async () => { const mockCDPSession = new MockCDPSession(); - const manager = new NetworkManager(mockCDPSession, true, { + const manager = new NetworkManager(true, { frame(): Frame | null { return null; }, }); + await manager.addClient(mockCDPSession); const requests: HTTPRequest[] = []; manager.on( @@ -680,11 +683,12 @@ describe('NetworkManager', () => { it(`should resolve the response once the late responseReceivedExtraInfo event arrives`, async () => { const mockCDPSession = new MockCDPSession(); - const manager = new NetworkManager(mockCDPSession, true, { + const manager = new NetworkManager(true, { frame(): Frame | null { return null; }, }); + await manager.addClient(mockCDPSession); const finishedRequests: HTTPRequest[] = []; const pendingRequests: HTTPRequest[] = []; @@ -832,11 +836,12 @@ describe('NetworkManager', () => { it(`should send responses for iframe that don't receive loadingFinished event`, async () => { const mockCDPSession = new MockCDPSession(); - const manager = new NetworkManager(mockCDPSession, true, { + const manager = new NetworkManager(true, { frame(): Frame | null { return null; }, }); + await manager.addClient(mockCDPSession); const responses: HTTPResponse[] = []; const requests: HTTPRequest[] = []; @@ -995,11 +1000,12 @@ describe('NetworkManager', () => { it(`should send responses for iframe that don't receive loadingFinished event`, async () => { const mockCDPSession = new MockCDPSession(); - const manager = new NetworkManager(mockCDPSession, true, { + const manager = new NetworkManager(true, { frame(): Frame | null { return null; }, }); + await manager.addClient(mockCDPSession); const responses: HTTPResponse[] = []; const requests: HTTPRequest[] = []; @@ -1141,11 +1147,12 @@ describe('NetworkManager', () => { it(`should handle cached redirects`, async () => { const mockCDPSession = new MockCDPSession(); - const manager = new NetworkManager(mockCDPSession, true, { + const manager = new NetworkManager(true, { frame(): Frame | null { return null; }, }); + await manager.addClient(mockCDPSession); const responses: HTTPResponse[] = []; const requests: HTTPRequest[] = []; diff --git a/packages/puppeteer-core/src/common/NetworkManager.ts b/packages/puppeteer-core/src/common/NetworkManager.ts index ef71aab4..fa7a17f8 100644 --- a/packages/puppeteer-core/src/common/NetworkManager.ts +++ b/packages/puppeteer-core/src/common/NetworkManager.ts @@ -16,13 +16,11 @@ import {Protocol} from 'devtools-protocol'; +import {Frame} from '../api/Frame.js'; import {assert} from '../util/assert.js'; -import {createDebuggableDeferred} from '../util/DebuggableDeferred.js'; -import {Deferred} from '../util/Deferred.js'; -import {CDPSession} from './Connection.js'; +import {CDPSession, CDPSessionEmittedEvents} from './Connection.js'; import {EventEmitter, Handler} from './EventEmitter.js'; -import {FrameManager} from './FrameManager.js'; import {HTTPRequest} from './HTTPRequest.js'; import {HTTPResponse} from './HTTPResponse.js'; import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js'; @@ -68,102 +66,103 @@ export const NetworkManagerEmittedEvents = { RequestFinished: Symbol('NetworkManager.RequestFinished'), } as const; +/** + * @internal + */ +interface FrameProvider { + frame(id: string): Frame | null; +} + /** * @internal */ export class NetworkManager extends EventEmitter { - #client: CDPSession; #ignoreHTTPSErrors: boolean; - #frameManager: Pick; + #frameManager: FrameProvider; #networkEventManager = new NetworkEventManager(); - #extraHTTPHeaders: Record = {}; + #extraHTTPHeaders?: Record; #credentials?: Credentials; #attemptedAuthentications = new Set(); #userRequestInterceptionEnabled = false; #protocolRequestInterceptionEnabled = false; - #userCacheDisabled = false; - #emulatedNetworkConditions: InternalNetworkConditions = { - offline: false, - upload: -1, - download: -1, - latency: 0, - }; - #deferredInit?: Deferred; + #userCacheDisabled?: boolean; + #emulatedNetworkConditions?: InternalNetworkConditions; + #userAgent?: string; + #userAgentMetadata?: Protocol.Emulation.UserAgentMetadata; - #handlers = new Map>([ - ['Fetch.requestPaused', this.#onRequestPaused.bind(this)], - ['Fetch.authRequired', this.#onAuthRequired.bind(this)], - ['Network.requestWillBeSent', this.#onRequestWillBeSent.bind(this)], - [ - 'Network.requestServedFromCache', - this.#onRequestServedFromCache.bind(this), - ], - ['Network.responseReceived', this.#onResponseReceived.bind(this)], - ['Network.loadingFinished', this.#onLoadingFinished.bind(this)], - ['Network.loadingFailed', this.#onLoadingFailed.bind(this)], - [ - 'Network.responseReceivedExtraInfo', - this.#onResponseReceivedExtraInfo.bind(this), - ], + #handlers = new Map([ + ['Fetch.requestPaused', this.#onRequestPaused], + ['Fetch.authRequired', this.#onAuthRequired], + ['Network.requestWillBeSent', this.#onRequestWillBeSent], + ['Network.requestServedFromCache', this.#onRequestServedFromCache], + ['Network.responseReceived', this.#onResponseReceived], + ['Network.loadingFinished', this.#onLoadingFinished], + ['Network.loadingFailed', this.#onLoadingFailed], + ['Network.responseReceivedExtraInfo', this.#onResponseReceivedExtraInfo], ]); - constructor( - client: CDPSession, - ignoreHTTPSErrors: boolean, - frameManager: Pick - ) { + #clients = new Map< + CDPSession, + Array<{event: string | symbol; handler: Handler}> + >(); + + constructor(ignoreHTTPSErrors: boolean, frameManager: FrameProvider) { super(); - this.#client = client; this.#ignoreHTTPSErrors = ignoreHTTPSErrors; this.#frameManager = frameManager; - - for (const [event, handler] of this.#handlers) { - this.#client.on(event, handler); - } } - async updateClient(client: CDPSession): Promise { - this.#client = client; + async addClient(client: CDPSession): Promise { + if (this.#clients.has(client)) { + return; + } + const listeners: Array<{event: string | symbol; handler: Handler}> = []; + this.#clients.set(client, listeners); for (const [event, handler] of this.#handlers) { - this.#client.on(event, handler); + listeners.push({ + event, + handler: handler.bind(this, client), + }); + client.on(event, listeners.at(-1)!.handler); } - this.#deferredInit = undefined; - await this.initialize(); - } - - /** - * Initialize calls should avoid async dependencies between CDP calls as those - * might not resolve until after the target is resumed causing a deadlock. - */ - initialize(): Promise { - if (this.#deferredInit) { - return this.#deferredInit.valueOrThrow(); - } - this.#deferredInit = createDebuggableDeferred( - 'NetworkManager initialization timed out' - ); - const init = Promise.all([ + listeners.push({ + event: CDPSessionEmittedEvents.Disconnected, + handler: this.#removeClient.bind(this, client), + }); + client.on(CDPSessionEmittedEvents.Disconnected, listeners.at(-1)!.handler); + await Promise.all([ this.#ignoreHTTPSErrors - ? this.#client.send('Security.setIgnoreCertificateErrors', { + ? client.send('Security.setIgnoreCertificateErrors', { ignore: true, }) : null, - this.#client.send('Network.enable'), + client.send('Network.enable'), + this.#applyExtraHTTPHeaders(client), + this.#applyNetworkConditions(client), + this.#applyProtocolCacheDisabled(client), + this.#applyProtocolRequestInterception(client), + this.#applyUserAgent(client), ]); - const deferredInitPromise = this.#deferredInit; - init - .then(() => { - deferredInitPromise.resolve(); - }) - .catch(err => { - deferredInitPromise.reject(err); - }); - return this.#deferredInit.valueOrThrow(); + } + + async #removeClient(client: CDPSession) { + const listeners = this.#clients.get(client); + for (const {event, handler} of listeners || []) { + client.off(event, handler); + } + this.#clients.delete(client); } async authenticate(credentials?: Credentials): Promise { this.#credentials = credentials; - await this.#updateProtocolRequestInterception(); + const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials; + if (enabled === this.#protocolRequestInterceptionEnabled) { + return; + } + this.#protocolRequestInterceptionEnabled = enabled; + await this.#applyToAllClients( + this.#applyProtocolRequestInterception.bind(this) + ); } async setExtraHTTPHeaders( @@ -178,7 +177,15 @@ export class NetworkManager extends EventEmitter { ); this.#extraHTTPHeaders[key.toLowerCase()] = value; } - await this.#client.send('Network.setExtraHTTPHeaders', { + + await this.#applyToAllClients(this.#applyExtraHTTPHeaders.bind(this)); + } + + async #applyExtraHTTPHeaders(client: CDPSession) { + if (this.#extraHTTPHeaders === undefined) { + return; + } + await client.send('Network.setExtraHTTPHeaders', { headers: this.#extraHTTPHeaders, }); } @@ -192,13 +199,29 @@ export class NetworkManager extends EventEmitter { } async setOfflineMode(value: boolean): Promise { + if (!this.#emulatedNetworkConditions) { + this.#emulatedNetworkConditions = { + offline: false, + upload: -1, + download: -1, + latency: 0, + }; + } this.#emulatedNetworkConditions.offline = value; - await this.#updateNetworkConditions(); + await this.#applyToAllClients(this.#applyNetworkConditions.bind(this)); } async emulateNetworkConditions( networkConditions: NetworkConditions | null ): Promise { + if (!this.#emulatedNetworkConditions) { + this.#emulatedNetworkConditions = { + offline: false, + upload: -1, + download: -1, + latency: 0, + }; + } this.#emulatedNetworkConditions.upload = networkConditions ? networkConditions.upload : -1; @@ -209,11 +232,22 @@ export class NetworkManager extends EventEmitter { ? networkConditions.latency : 0; - await this.#updateNetworkConditions(); + await this.#applyToAllClients(this.#applyNetworkConditions.bind(this)); } - async #updateNetworkConditions(): Promise { - await this.#client.send('Network.emulateNetworkConditions', { + async #applyToAllClients(fn: (client: CDPSession) => Promise) { + await Promise.all( + Array.from(this.#clients.keys()).map(client => { + return fn(client); + }) + ); + } + + async #applyNetworkConditions(client: CDPSession): Promise { + if (this.#emulatedNetworkConditions === undefined) { + return; + } + await client.send('Network.emulateNetworkConditions', { offline: this.#emulatedNetworkConditions.offline, latency: this.#emulatedNetworkConditions.latency, uploadThroughput: this.#emulatedNetworkConditions.upload, @@ -225,55 +259,71 @@ export class NetworkManager extends EventEmitter { userAgent: string, userAgentMetadata?: Protocol.Emulation.UserAgentMetadata ): Promise { - await this.#client.send('Network.setUserAgentOverride', { - userAgent: userAgent, - userAgentMetadata: userAgentMetadata, + this.#userAgent = userAgent; + this.#userAgentMetadata = userAgentMetadata; + await this.#applyToAllClients(this.#applyUserAgent.bind(this)); + } + + async #applyUserAgent(client: CDPSession) { + if (this.#userAgent === undefined) { + return; + } + await client.send('Network.setUserAgentOverride', { + userAgent: this.#userAgent, + userAgentMetadata: this.#userAgentMetadata, }); } async setCacheEnabled(enabled: boolean): Promise { this.#userCacheDisabled = !enabled; - await this.#updateProtocolCacheDisabled(); + await this.#applyToAllClients(this.#applyProtocolCacheDisabled.bind(this)); } async setRequestInterception(value: boolean): Promise { this.#userRequestInterceptionEnabled = value; - await this.#updateProtocolRequestInterception(); - } - - async #updateProtocolRequestInterception(): Promise { const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials; if (enabled === this.#protocolRequestInterceptionEnabled) { return; } this.#protocolRequestInterceptionEnabled = enabled; - if (enabled) { + await this.#applyToAllClients( + this.#applyProtocolRequestInterception.bind(this) + ); + } + + async #applyProtocolRequestInterception(client: CDPSession): Promise { + if (this.#userCacheDisabled === undefined) { + this.#userCacheDisabled = false; + } + if (this.#protocolRequestInterceptionEnabled) { await Promise.all([ - this.#updateProtocolCacheDisabled(), - this.#client.send('Fetch.enable', { + this.#applyProtocolCacheDisabled(client), + client.send('Fetch.enable', { handleAuthRequests: true, patterns: [{urlPattern: '*'}], }), ]); } else { await Promise.all([ - this.#updateProtocolCacheDisabled(), - this.#client.send('Fetch.disable'), + this.#applyProtocolCacheDisabled(client), + client.send('Fetch.disable'), ]); } } - #cacheDisabled(): boolean { - return this.#userCacheDisabled; - } - - async #updateProtocolCacheDisabled(): Promise { - await this.#client.send('Network.setCacheDisabled', { - cacheDisabled: this.#cacheDisabled(), + async #applyProtocolCacheDisabled(client: CDPSession): Promise { + if (this.#userCacheDisabled === undefined) { + return; + } + await client.send('Network.setCacheDisabled', { + cacheDisabled: this.#userCacheDisabled, }); } - #onRequestWillBeSent(event: Protocol.Network.RequestWillBeSentEvent): void { + #onRequestWillBeSent( + client: CDPSession, + event: Protocol.Network.RequestWillBeSentEvent + ): void { // Request interception doesn't happen for data URLs with Network Service. if ( this.#userRequestInterceptionEnabled && @@ -291,16 +341,19 @@ export class NetworkManager extends EventEmitter { if (requestPausedEvent) { const {requestId: fetchRequestId} = requestPausedEvent; this.#patchRequestEventHeaders(event, requestPausedEvent); - this.#onRequest(event, fetchRequestId); + this.#onRequest(client, event, fetchRequestId); this.#networkEventManager.forgetRequestPaused(networkRequestId); } return; } - this.#onRequest(event, undefined); + this.#onRequest(client, event, undefined); } - #onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void { + #onAuthRequired( + client: CDPSession, + event: Protocol.Fetch.AuthRequiredEvent + ): void { let response: Protocol.Fetch.AuthChallengeResponse['response'] = 'Default'; if (this.#attemptedAuthentications.has(event.requestId)) { response = 'CancelAuth'; @@ -312,7 +365,7 @@ export class NetworkManager extends EventEmitter { username: undefined, password: undefined, }; - this.#client + client .send('Fetch.continueWithAuth', { requestId: event.requestId, authChallengeResponse: {response, username, password}, @@ -327,12 +380,15 @@ export class NetworkManager extends EventEmitter { * CDP may send multiple Fetch.requestPaused * for the same Network.requestWillBeSent. */ - #onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void { + #onRequestPaused( + client: CDPSession, + event: Protocol.Fetch.RequestPausedEvent + ): void { if ( !this.#userRequestInterceptionEnabled && this.#protocolRequestInterceptionEnabled ) { - this.#client + client .send('Fetch.continueRequest', { requestId: event.requestId, }) @@ -342,7 +398,7 @@ export class NetworkManager extends EventEmitter { const {networkId: networkRequestId, requestId: fetchRequestId} = event; if (!networkRequestId) { - this.#onRequestWithoutNetworkInstrumentation(event); + this.#onRequestWithoutNetworkInstrumentation(client, event); return; } @@ -364,7 +420,7 @@ export class NetworkManager extends EventEmitter { if (requestWillBeSentEvent) { this.#patchRequestEventHeaders(requestWillBeSentEvent, event); - this.#onRequest(requestWillBeSentEvent, fetchRequestId); + this.#onRequest(client, requestWillBeSentEvent, fetchRequestId); } else { this.#networkEventManager.storeRequestPaused(networkRequestId, event); } @@ -382,6 +438,7 @@ export class NetworkManager extends EventEmitter { } #onRequestWithoutNetworkInstrumentation( + client: CDPSession, event: Protocol.Fetch.RequestPausedEvent ): void { // If an event has no networkId it should not have any network events. We @@ -391,7 +448,7 @@ export class NetworkManager extends EventEmitter { : null; const request = new HTTPRequest( - this.#client, + client, frame, event.requestId, this.#userRequestInterceptionEnabled, @@ -403,6 +460,7 @@ export class NetworkManager extends EventEmitter { } #onRequest( + client: CDPSession, event: Protocol.Network.RequestWillBeSentEvent, fetchRequestId?: FetchRequestId ): void { @@ -434,6 +492,7 @@ export class NetworkManager extends EventEmitter { // requestWillBeSent event. if (request) { this.#handleRequestRedirect( + client, request, event.redirectResponse, redirectResponseExtraInfo @@ -446,7 +505,7 @@ export class NetworkManager extends EventEmitter { : null; const request = new HTTPRequest( - this.#client, + client, frame, fetchRequestId, this.#userRequestInterceptionEnabled, @@ -459,6 +518,7 @@ export class NetworkManager extends EventEmitter { } #onRequestServedFromCache( + _client: CDPSession, event: Protocol.Network.RequestServedFromCacheEvent ): void { const request = this.#networkEventManager.getRequest(event.requestId); @@ -469,12 +529,13 @@ export class NetworkManager extends EventEmitter { } #handleRequestRedirect( + client: CDPSession, request: HTTPRequest, responsePayload: Protocol.Network.Response, extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null ): void { const response = new HTTPResponse( - this.#client, + client, request, responsePayload, extraInfo @@ -490,6 +551,7 @@ export class NetworkManager extends EventEmitter { } #emitResponseEvent( + client: CDPSession, responseReceived: Protocol.Network.ResponseReceivedEvent, extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null ): void { @@ -521,7 +583,7 @@ export class NetworkManager extends EventEmitter { } const response = new HTTPResponse( - this.#client, + client, request, responseReceived.response, extraInfo @@ -530,7 +592,10 @@ export class NetworkManager extends EventEmitter { this.emit(NetworkManagerEmittedEvents.Response, response); } - #onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void { + #onResponseReceived( + client: CDPSession, + event: Protocol.Network.ResponseReceivedEvent + ): void { const request = this.#networkEventManager.getRequest(event.requestId); let extraInfo = null; if (request && !request._fromMemoryCache && event.hasExtraInfo) { @@ -545,10 +610,11 @@ export class NetworkManager extends EventEmitter { return; } } - this.#emitResponseEvent(event, extraInfo); + this.#emitResponseEvent(client, event, extraInfo); } #onResponseReceivedExtraInfo( + client: CDPSession, event: Protocol.Network.ResponseReceivedExtraInfoEvent ): void { // We may have skipped a redirect response/request pair due to waiting for @@ -559,7 +625,7 @@ export class NetworkManager extends EventEmitter { ); if (redirectInfo) { this.#networkEventManager.responseExtraInfo(event.requestId).push(event); - this.#onRequest(redirectInfo.event, redirectInfo.fetchRequestId); + this.#onRequest(client, redirectInfo.event, redirectInfo.fetchRequestId); return; } @@ -570,7 +636,11 @@ export class NetworkManager extends EventEmitter { ); if (queuedEvents) { this.#networkEventManager.forgetQueuedEventGroup(event.requestId); - this.#emitResponseEvent(queuedEvents.responseReceivedEvent, event); + this.#emitResponseEvent( + client, + queuedEvents.responseReceivedEvent, + event + ); if (queuedEvents.loadingFinishedEvent) { this.#emitLoadingFinished(queuedEvents.loadingFinishedEvent); } @@ -597,7 +667,10 @@ export class NetworkManager extends EventEmitter { } } - #onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void { + #onLoadingFinished( + _client: CDPSession, + event: Protocol.Network.LoadingFinishedEvent + ): void { // If the response event for this request is still waiting on a // corresponding ExtraInfo event, then wait to emit this event too. const queuedEvents = this.#networkEventManager.getQueuedEventGroup( @@ -627,7 +700,10 @@ export class NetworkManager extends EventEmitter { this.emit(NetworkManagerEmittedEvents.RequestFinished, request); } - #onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void { + #onLoadingFailed( + _client: CDPSession, + event: Protocol.Network.LoadingFailedEvent + ): void { // If the response event for this request is still waiting on a // corresponding ExtraInfo event, then wait to emit this event too. const queuedEvents = this.#networkEventManager.getQueuedEventGroup( diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts index f2131e6b..7b362f14 100644 --- a/packages/puppeteer-core/src/common/Page.ts +++ b/packages/puppeteer-core/src/common/Page.ts @@ -329,6 +329,15 @@ export class CDPPage extends Page { await this.#frameManager.swapFrameTree(newSession); this.#setupEventListeners(); }); + this.#tabSession?.on( + CDPSessionEmittedEvents.Ready, + (session: CDPSessionImpl) => { + if (session._target()._subtype() !== 'prerender') { + return; + } + this.#frameManager.registerSecondaryPage(session).catch(debugError); + } + ); } #setupEventListeners() { @@ -399,7 +408,7 @@ export class CDPPage extends Page { async #initialize(): Promise { try { await Promise.all([ - this.#frameManager.initialize(), + this.#frameManager.initialize(this.#client), this.#client.send('Performance.enable'), this.#client.send('Log.enable'), ]); diff --git a/test/TestExpectations.json b/test/TestExpectations.json index 0b5fbeeb..7c7a675d 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -3911,6 +3911,12 @@ "parameters": ["firefox", "webDriverBiDi"], "expectations": ["SKIP"] }, + { + "testIdPattern": "[prerender.spec] Prerender with network requests can receive requests from the prerendered page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, { "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at browser level", "platforms": ["darwin", "linux", "win32"], diff --git a/test/assets/oopif.html b/test/assets/oopif.html index 0761e8ab..f04b9127 100644 --- a/test/assets/oopif.html +++ b/test/assets/oopif.html @@ -1,2 +1,5 @@ Navigate within document - \ No newline at end of file + + diff --git a/test/assets/prerender/target.html b/test/assets/prerender/target.html index 469f3d87..df78fcc3 100644 --- a/test/assets/prerender/target.html +++ b/test/assets/prerender/target.html @@ -1,4 +1,5 @@ - + + target diff --git a/test/src/oopif.spec.ts b/test/src/oopif.spec.ts index 7fd3a163..e430b5fe 100644 --- a/test/src/oopif.spec.ts +++ b/test/src/oopif.spec.ts @@ -307,17 +307,23 @@ describeWithDebugLogs('OOPIF', function () { it('should load oopif iframes with subresources and request interception', async () => { const {server, page, context} = state; - const frame = page.waitForFrame(frame => { + const framePromise = page.waitForFrame(frame => { return frame.url().endsWith('/oopif.html'); }); - await page.setRequestInterception(true); page.on('request', request => { - return request.continue(); + void request.continue(); + }); + await page.setRequestInterception(true); + const requestPromise = page.waitForRequest(request => { + return request.url().includes('requestFromOOPIF'); }); await page.goto(server.PREFIX + '/dynamic-oopif.html'); - await frame; + const frame = await framePromise; + const request = await requestPromise; expect(oopifs(context)).toHaveLength(1); + expect(request.frame()).toBe(frame); }); + it('should support frames within OOP iframes', async () => { const {server, page} = state; diff --git a/test/src/prerender.spec.ts b/test/src/prerender.spec.ts index 65db7ea1..227fc0f0 100644 --- a/test/src/prerender.spec.ts +++ b/test/src/prerender.spec.ts @@ -89,4 +89,44 @@ describe('Prerender', function () { expect(mainFrame).toBe(page.mainFrame()); }); }); + + describe('with network requests', () => { + it('can receive requests from the prerendered page', async () => { + const {page, server} = await getTestState(); + + const urls: string[] = []; + page.on('request', request => { + urls.push(request.url()); + }); + + await page.goto(server.PREFIX + '/prerender/index.html'); + const button = await page.waitForSelector('button'); + await button?.click(); + const mainFrame = page.mainFrame(); + const link = await mainFrame.waitForSelector('a'); + await Promise.all([mainFrame.waitForNavigation(), link?.click()]); + expect(mainFrame).toBe(page.mainFrame()); + expect( + await mainFrame.evaluate(() => { + return document.body.innerText; + }) + ).toBe('target'); + expect(mainFrame).toBe(page.mainFrame()); + expect( + urls.find(url => { + return url.endsWith('prerender/target.html'); + }) + ).toBeTruthy(); + expect( + urls.find(url => { + return url.includes('prerender/index.html'); + }) + ).toBeTruthy(); + expect( + urls.find(url => { + return url.includes('prerender/target.html?fromPrerendered'); + }) + ).toBeTruthy(); + }); + }); });