fix: make network manager multi session (#10793)

This commit is contained in:
Alex Rudenko 2023-08-29 18:12:04 +02:00 committed by GitHub
parent 6f2e3db883
commit 085936bd7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 287 additions and 138 deletions

View File

@ -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<void> {
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<void> {
async initialize(client: CDPSession): Promise<void> {
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()!);
}
/**

View File

@ -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[] = [];

View File

@ -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, 'frame'>;
#frameManager: FrameProvider;
#networkEventManager = new NetworkEventManager();
#extraHTTPHeaders: Record<string, string> = {};
#extraHTTPHeaders?: Record<string, string>;
#credentials?: Credentials;
#attemptedAuthentications = new Set<string>();
#userRequestInterceptionEnabled = false;
#protocolRequestInterceptionEnabled = false;
#userCacheDisabled = false;
#emulatedNetworkConditions: InternalNetworkConditions = {
offline: false,
upload: -1,
download: -1,
latency: 0,
};
#deferredInit?: Deferred<void>;
#userCacheDisabled?: boolean;
#emulatedNetworkConditions?: InternalNetworkConditions;
#userAgent?: string;
#userAgentMetadata?: Protocol.Emulation.UserAgentMetadata;
#handlers = new Map<string, Handler<any>>([
['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<string, Function>([
['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<FrameManager, 'frame'>
) {
#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<void> {
this.#client = client;
async addClient(client: CDPSession): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
await this.#client.send('Network.emulateNetworkConditions', {
async #applyToAllClients(fn: (client: CDPSession) => Promise<unknown>) {
await Promise.all(
Array.from(this.#clients.keys()).map(client => {
return fn(client);
})
);
}
async #applyNetworkConditions(client: CDPSession): Promise<void> {
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<void> {
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<void> {
this.#userCacheDisabled = !enabled;
await this.#updateProtocolCacheDisabled();
await this.#applyToAllClients(this.#applyProtocolCacheDisabled.bind(this));
}
async setRequestInterception(value: boolean): Promise<void> {
this.#userRequestInterceptionEnabled = value;
await this.#updateProtocolRequestInterception();
}
async #updateProtocolRequestInterception(): Promise<void> {
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<void> {
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<void> {
await this.#client.send('Network.setCacheDisabled', {
cacheDisabled: this.#cacheDisabled(),
async #applyProtocolCacheDisabled(client: CDPSession): Promise<void> {
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(

View File

@ -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<void> {
try {
await Promise.all([
this.#frameManager.initialize(),
this.#frameManager.initialize(this.#client),
this.#client.send('Performance.enable'),
this.#client.send('Log.enable'),
]);

View File

@ -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"],

View File

@ -1,2 +1,5 @@
<a id="navigate-within-document" href="#nav">Navigate within document</a>
<a name="nav"></a>
<a name="nav"></a>
<script>
fetch('oopif.html?requestFromOOPIF')
</script>

View File

@ -1,4 +1,5 @@
<!DOCTYPE html>
<head></head>
<head>
<script>fetch('target.html?fromPrerendered')</script>
</head>
<body>target</body>

View File

@ -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;

View File

@ -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();
});
});
});