diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 7715d8b23f..9af7e6ba9d 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -562,7 +562,7 @@ export class CRBrowserContext extends BrowserContext { override async clearCache(): Promise { for (const page of this._crPages()) - await page._mainFrameSession._networkManager.clearCache(); + await page._networkManager.clearCache(); } async cancelDownload(guid: string) { diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index f3952b82fd..9e9eac0477 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -26,70 +26,88 @@ import type * as contexts from '../browserContext'; import type * as frames from '../frames'; import type * as types from '../types'; import type { CRPage } from './crPage'; -import { assert, headersObjectToArray } from '../../utils'; +import { assert, headersArrayToObject, headersObjectToArray } from '../../utils'; import type { CRServiceWorker } from './crServiceWorker'; -import { isProtocolError } from '../protocolError'; +import { isProtocolError, isSessionClosedError } from '../protocolError'; type SessionInfo = { session: CRSession; + isMain?: boolean; workerFrame?: frames.Frame; + eventListeners: RegisteredListener[]; }; export class CRNetworkManager { - private _session: CRSession; private _page: Page | null; private _serviceWorker: CRServiceWorker | null; - private _parentManager: CRNetworkManager | null; private _requestIdToRequest = new Map(); private _requestIdToRequestWillBeSentEvent = new Map(); private _credentials: {origin?: string, username: string, password: string} | null = null; private _attemptedAuthentications = new Set(); private _userRequestInterceptionEnabled = false; private _protocolRequestInterceptionEnabled = false; + private _offline = false; + private _extraHTTPHeaders: types.HeadersArray = []; private _requestIdToRequestPausedEvent = new Map(); - private _eventListeners: RegisteredListener[]; private _responseExtraInfoTracker = new ResponseExtraInfoTracker(); + private _sessions = new Map(); - constructor(session: CRSession, page: Page | null, serviceWorker: CRServiceWorker | null, parentManager: CRNetworkManager | null) { - this._session = session; + constructor(page: Page | null, serviceWorker: CRServiceWorker | null) { this._page = page; this._serviceWorker = serviceWorker; - this._parentManager = parentManager; - this._eventListeners = this.instrumentNetworkEvents({ session }); } - instrumentNetworkEvents(sessionInfo: SessionInfo): RegisteredListener[] { - const listeners = [ - eventsHelper.addEventListener(sessionInfo.session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)), - eventsHelper.addEventListener(sessionInfo.session, 'Fetch.authRequired', this._onAuthRequired.bind(this)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)), + async addSession(session: CRSession, workerFrame?: frames.Frame, isMain?: boolean) { + const sessionInfo: SessionInfo = { session, isMain, workerFrame, eventListeners: [] }; + sessionInfo.eventListeners = [ + eventsHelper.addEventListener(session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)), + eventsHelper.addEventListener(session, 'Fetch.authRequired', this._onAuthRequired.bind(this, sessionInfo)), + eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)), + eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)), + eventsHelper.addEventListener(session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)), + eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)), + eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)), + eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this, sessionInfo)), + eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)), ]; if (this._page) { - listeners.push(...[ - eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)), - eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)), + sessionInfo.eventListeners.push(...[ + eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)), + eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)), + eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), + eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), + eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), + eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)), + eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)), ]); } - return listeners; + this._sessions.set(session, sessionInfo); + await Promise.all([ + session.send('Network.enable'), + this._updateProtocolRequestInterceptionForSession(sessionInfo, true /* initial */), + this._setOfflineForSession(sessionInfo, true /* initial */), + this._setExtraHTTPHeadersForSession(sessionInfo, true /* initial */), + ]); } - async initialize() { - await this._session.send('Network.enable'); + removeSession(session: CRSession) { + const info = this._sessions.get(session); + if (info) + eventsHelper.removeEventListeners(info.eventListeners); + this._sessions.delete(session); } - dispose() { - eventsHelper.removeEventListeners(this._eventListeners); + private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise) { + await Promise.all([...this._sessions.values()].map(info => { + if (info.isMain) + return cb(info); + return cb(info).catch(e => { + // Broadcasting a message to the closed target should be a noop. + if (isSessionClosedError(e)) + return; + throw e; + }); + })); } async authenticate(credentials: types.Credentials | null) { @@ -98,8 +116,20 @@ export class CRNetworkManager { } async setOffline(offline: boolean) { - await this._session.send('Network.emulateNetworkConditions', { - offline, + if (offline === this._offline) + return; + this._offline = offline; + await this._forEachSession(info => this._setOfflineForSession(info)); + } + + private async _setOfflineForSession(info: SessionInfo, initial?: boolean) { + if (initial && !this._offline) + return; + // Workers are affected by the owner frame's Network.emulateNetworkConditions. + if (info.workerFrame) + return; + await info.session.send('Network.emulateNetworkConditions', { + offline: this._offline, // values of 0 remove any active throttling. crbug.com/456324#c9 latency: 0, downloadThroughput: -1, @@ -117,28 +147,46 @@ export class CRNetworkManager { if (enabled === this._protocolRequestInterceptionEnabled) return; this._protocolRequestInterceptionEnabled = enabled; - if (enabled) { - await Promise.all([ - this._session.send('Network.setCacheDisabled', { cacheDisabled: true }), - this._session.send('Fetch.enable', { - handleAuthRequests: true, - patterns: [{ urlPattern: '*', requestStage: 'Request' }], - }), - ]); - } else { - await Promise.all([ - this._session.send('Network.setCacheDisabled', { cacheDisabled: false }), - this._session.send('Fetch.disable') - ]); + await this._forEachSession(info => this._updateProtocolRequestInterceptionForSession(info)); + } + + private async _updateProtocolRequestInterceptionForSession(info: SessionInfo, initial?: boolean) { + const enabled = this._protocolRequestInterceptionEnabled; + if (initial && !enabled) + return; + const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled }); + let fetchPromise = Promise.resolve(undefined); + if (!info.workerFrame) { + if (enabled) + fetchPromise = info.session.send('Fetch.enable', { handleAuthRequests: true, patterns: [{ urlPattern: '*', requestStage: 'Request' }] }); + else + fetchPromise = info.session.send('Fetch.disable'); } + await Promise.all([cachePromise, fetchPromise]); + } + + async setExtraHTTPHeaders(extraHTTPHeaders: types.HeadersArray) { + if (!this._extraHTTPHeaders.length && !extraHTTPHeaders.length) + return; + this._extraHTTPHeaders = extraHTTPHeaders; + await this._forEachSession(info => this._setExtraHTTPHeadersForSession(info)); + } + + private async _setExtraHTTPHeadersForSession(info: SessionInfo, initial?: boolean) { + if (initial && !this._extraHTTPHeaders.length) + return; + await info.session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(this._extraHTTPHeaders, false /* lowerCase */) }); } async clearCache() { - // Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache. - await this._session.send('Network.setCacheDisabled', { cacheDisabled: true }); - if (!this._protocolRequestInterceptionEnabled) - await this._session.send('Network.setCacheDisabled', { cacheDisabled: false }); - await this._session.send('Network.clearBrowserCache'); + await this._forEachSession(async info => { + // Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache. + await info.session.send('Network.setCacheDisabled', { cacheDisabled: true }); + if (!this._protocolRequestInterceptionEnabled) + await info.session.send('Network.setCacheDisabled', { cacheDisabled: false }); + if (!info.workerFrame) + await info.session.send('Network.clearBrowserCache'); + }); } _onRequestWillBeSent(sessionInfo: SessionInfo, event: Protocol.Network.requestWillBeSentPayload) { @@ -165,7 +213,7 @@ export class CRNetworkManager { this._responseExtraInfoTracker.requestWillBeSentExtraInfo(event); } - _onAuthRequired(event: Protocol.Fetch.authRequiredPayload) { + _onAuthRequired(sessionInfo: SessionInfo, event: Protocol.Fetch.authRequiredPayload) { let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default'; const shouldProvideCredentials = this._shouldProvideCredentials(event.request.url); if (this._attemptedAuthentications.has(event.requestId)) { @@ -175,7 +223,7 @@ export class CRNetworkManager { this._attemptedAuthentications.add(event.requestId); } const { username, password } = shouldProvideCredentials && this._credentials ? this._credentials : { username: undefined, password: undefined }; - this._session._sendMayFail('Fetch.continueWithAuth', { + sessionInfo.session._sendMayFail('Fetch.continueWithAuth', { requestId: event.requestId, authChallengeResponse: { response, username, password }, }); @@ -191,7 +239,7 @@ export class CRNetworkManager { if (!event.networkId) { // Fetch without networkId means that request was not recognized by inspector, and // it will never receive Network.requestWillBeSent. Continue the request to not affect it. - this._session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId }); + sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId }); return; } if (event.request.url.startsWith('data:')) @@ -215,7 +263,7 @@ export class CRNetworkManager { // // Note: make sure not to prematurely continue the redirect, which shares the // `networkId` between the original request and the redirect. - this._session._sendMayFail('Fetch.continueRequest', { + sessionInfo.session._sendMayFail('Fetch.continueRequest', { ...alreadyContinuedParams, requestId: event.requestId, }); @@ -266,7 +314,7 @@ export class CRNetworkManager { ]; if (requestHeaders['Access-Control-Request-Headers']) responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] }); - this._session._sendMayFail('Fetch.fulfillRequest', { + sessionInfo.session._sendMayFail('Fetch.fulfillRequest', { requestId: requestPausedEvent.requestId, responseCode: 204, responsePhrase: network.STATUS_TEXTS['204'], @@ -279,7 +327,7 @@ export class CRNetworkManager { // Non-service-worker requests MUST have a frame—if they don't, we pretend there was no request if (!frame && !this._serviceWorker) { if (requestPausedEvent) - this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }); + sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }); return; } @@ -289,9 +337,9 @@ export class CRNetworkManager { if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled)) { // Chromium does not preserve header overrides between redirects, so we have to do it ourselves. const headers = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers; - this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers }); + sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers }); } else { - route = new RouteImpl(this._session, requestPausedEvent.requestId); + route = new RouteImpl(sessionInfo.session, requestPausedEvent.requestId); } } const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document'; @@ -426,16 +474,15 @@ export class CRNetworkManager { (this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response); } - _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) { + _onLoadingFinished(sessionInfo: SessionInfo, event: Protocol.Network.loadingFinishedPayload) { this._responseExtraInfoTracker.loadingFinished(event); - let request = this._requestIdToRequest.get(event.requestId); - if (!request) - request = this._maybeAdoptMainRequest(event.requestId); + const request = this._requestIdToRequest.get(event.requestId); // For certain requestIds we never receive requestWillBeSent event. // @see https://crbug.com/750469 if (!request) return; + this._maybeUpdateOOPIFMainRequest(sessionInfo, request); // Under certain conditions we never get the Network.responseReceived // event from protocol. @see https://crbug.com/883475 @@ -453,8 +500,6 @@ export class CRNetworkManager { this._responseExtraInfoTracker.loadingFailed(event); let request = this._requestIdToRequest.get(event.requestId); - if (!request) - request = this._maybeAdoptMainRequest(event.requestId); if (!request) { const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId); @@ -472,6 +517,7 @@ export class CRNetworkManager { // @see https://crbug.com/750469 if (!request) return; + this._maybeUpdateOOPIFMainRequest(sessionInfo, request); const response = request.request._existingResponse(); if (response) { response.setTransferSize(null); @@ -483,22 +529,12 @@ export class CRNetworkManager { (this._page?._frameManager || this._serviceWorker)!.requestFailed(request.request, !!event.canceled); } - private _maybeAdoptMainRequest(requestId: Protocol.Network.RequestId): InterceptableRequest | undefined { + private _maybeUpdateOOPIFMainRequest(sessionInfo: SessionInfo, request: InterceptableRequest) { // OOPIF has a main request that starts in the parent session but finishes in the child session. - if (!this._parentManager) - return; - const request = this._parentManager._requestIdToRequest.get(requestId); - // Main requests have matching loaderId and requestId. - if (!request || request._documentId !== requestId) - return; - this._requestIdToRequest.set(requestId, request); - request.session = this._session; - this._parentManager._requestIdToRequest.delete(requestId); - if (request._interceptionId && this._parentManager._attemptedAuthentications.has(request._interceptionId)) { - this._parentManager._attemptedAuthentications.delete(request._interceptionId); - this._attemptedAuthentications.add(request._interceptionId); - } - return request; + // We check for the main request by matching loaderId and requestId, and if it now belongs to + // a child session, migrate it there. + if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId) + request.session = sessionInfo.session; } } diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 2c0d3f94b4..6de00fd80d 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -20,7 +20,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper'; import { registry } from '../registry'; import { rewriteErrorMessage } from '../../utils/stackTrace'; -import { assert, createGuid, headersArrayToObject } from '../../utils'; +import { assert, createGuid } from '../../utils'; import * as dialog from '../dialog'; import * as dom from '../dom'; import * as frames from '../frames'; @@ -61,6 +61,7 @@ export class CRPage implements PageDelegate { readonly rawTouchscreen: RawTouchscreenImpl; readonly _targetId: string; readonly _opener: CRPage | null; + readonly _networkManager: CRNetworkManager; private readonly _pdf: CRPDF; private readonly _coverage: CRCoverage; readonly _browserContext: CRBrowserContext; @@ -92,6 +93,13 @@ export class CRPage implements PageDelegate { this._coverage = new CRCoverage(client); this._browserContext = browserContext; this._page = new Page(this, browserContext); + this._networkManager = new CRNetworkManager(this._page, null); + // Sync any browser context state to the network manager. This does not talk over CDP because + // we have not connected any sessions to the network manager yet. + this.updateOffline(); + this.updateExtraHTTPHeaders(); + this.updateHttpCredentials(); + this.updateRequestInterception(); this._mainFrameSession = new FrameSession(this, client, targetId, null); this._sessions.set(targetId, this._mainFrameSession); if (opener && !browserContext._options.noDefaultViewport) { @@ -184,7 +192,11 @@ export class CRPage implements PageDelegate { } async updateExtraHTTPHeaders(): Promise { - await this._forAllFrameSessions(frame => frame._updateExtraHTTPHeaders(false)); + const headers = network.mergeHeaders([ + this._browserContext._options.extraHTTPHeaders, + this._page.extraHTTPHeaders() + ]); + await this._networkManager.setExtraHTTPHeaders(headers); } async updateGeolocation(): Promise { @@ -192,11 +204,11 @@ export class CRPage implements PageDelegate { } async updateOffline(): Promise { - await this._forAllFrameSessions(frame => frame._updateOffline(false)); + await this._networkManager.setOffline(!!this._browserContext._options.offline); } async updateHttpCredentials(): Promise { - await this._forAllFrameSessions(frame => frame._updateHttpCredentials(false)); + await this._networkManager.authenticate(this._browserContext._options.httpCredentials || null); } async updateEmulatedViewportSize(preserveWindowBoundaries?: boolean): Promise { @@ -216,7 +228,7 @@ export class CRPage implements PageDelegate { } async updateRequestInterception(): Promise { - await this._forAllFrameSessions(frame => frame._updateRequestInterception()); + await this._networkManager.setRequestInterception(this._page.needsRequestInterception()); } async updateFileChooserInterception() { @@ -392,7 +404,6 @@ class FrameSession { readonly _client: CRSession; readonly _crPage: CRPage; readonly _page: Page; - readonly _networkManager: CRNetworkManager; private readonly _parentSession: FrameSession | null; private readonly _childSessions = new Set(); private readonly _contextIdToContext = new Map(); @@ -418,7 +429,6 @@ class FrameSession { this._crPage = crPage; this._page = crPage._page; this._targetId = targetId; - this._networkManager = new CRNetworkManager(client, this._page, null, parentSession ? parentSession._networkManager : null); this._parentSession = parentSession; if (parentSession) parentSession._childSessions.add(this); @@ -533,7 +543,7 @@ class FrameSession { source: '', worldName: UTILITY_WORLD_NAME, }), - this._networkManager.initialize(), + this._crPage._networkManager.addSession(this._client, undefined, this._isMainFrame()), this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), ]; if (!isSettingStorageState) { @@ -559,10 +569,6 @@ class FrameSession { if (!this._crPage._browserContext._browser.options.headful) promises.push(this._setDefaultFontFamilies(this._client)); promises.push(this._updateGeolocation(true)); - promises.push(this._updateExtraHTTPHeaders(true)); - promises.push(this._updateRequestInterception()); - promises.push(this._updateOffline(true)); - promises.push(this._updateHttpCredentials(true)); promises.push(this._updateEmulateMedia()); promises.push(this._updateFileChooserInterception(true)); for (const binding of this._crPage._page.allBindings()) @@ -586,7 +592,7 @@ class FrameSession { if (this._parentSession) this._parentSession._childSessions.delete(this); eventsHelper.removeEventListeners(this._eventListeners); - this._networkManager.dispose(); + this._crPage._networkManager.removeSession(this._client); this._crPage._sessions.delete(this._targetId); this._client.dispose(); } @@ -752,7 +758,8 @@ class FrameSession { }); // This might fail if the target is closed before we initialize. session._sendMayFail('Runtime.enable'); - session._sendMayFail('Network.enable'); + // TODO: attribute workers to the right frame. + this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? undefined).catch(() => {}); session._sendMayFail('Runtime.runIfWaitingForDebugger'); session._sendMayFail('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }); session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event)); @@ -762,8 +769,6 @@ class FrameSession { this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); }); session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page)); - // TODO: attribute workers to the right frame. - this._networkManager.instrumentNetworkEvents({ session, workerFrame: this._page._frameManager.frame(this._targetId) ?? undefined }); } _onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) { @@ -981,33 +986,12 @@ class FrameSession { await this._client._sendMayFail('Page.stopScreencast'); } - async _updateExtraHTTPHeaders(initial: boolean): Promise { - const headers = network.mergeHeaders([ - this._crPage._browserContext._options.extraHTTPHeaders, - this._page.extraHTTPHeaders() - ]); - if (!initial || headers.length) - await this._client.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(headers, false /* lowerCase */) }); - } - async _updateGeolocation(initial: boolean): Promise { const geolocation = this._crPage._browserContext._options.geolocation; if (!initial || geolocation) await this._client.send('Emulation.setGeolocationOverride', geolocation || {}); } - async _updateOffline(initial: boolean): Promise { - const offline = !!this._crPage._browserContext._options.offline; - if (!initial || offline) - await this._networkManager.setOffline(offline); - } - - async _updateHttpCredentials(initial: boolean): Promise { - const credentials = this._crPage._browserContext._options.httpCredentials || null; - if (!initial || credentials) - await this._networkManager.authenticate(credentials); - } - async _updateViewport(preserveWindowBoundaries?: boolean): Promise { if (this._crPage._browserContext._browser.isClank()) return; @@ -1106,10 +1090,6 @@ class FrameSession { await session.send('Page.setFontFamilies', fontFamilies); } - async _updateRequestInterception(): Promise { - await this._networkManager.setRequestInterception(this._page.needsRequestInterception()); - } - async _updateFileChooserInterception(initial: boolean) { const enabled = this._page.fileChooserIntercepted(); if (initial && !enabled) diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index 11d6385356..4de63fac55 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -34,13 +34,13 @@ export class CRServiceWorker extends Worker { this._session = session; this._browserContext = browserContext; if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS) - this._networkManager = new CRNetworkManager(session, null, this, null); + this._networkManager = new CRNetworkManager(null, this); session.once('Runtime.executionContextCreated', event => { this._createExecutionContext(new CRExecutionContext(session, event.context)); }); if (this._networkManager && this._isNetworkInspectionEnabled()) { - this._networkManager.initialize().catch(() => {}); + this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {}); this.updateRequestInterception(); this.updateExtraHTTPHeaders(true); this.updateHttpCredentials(true); @@ -56,6 +56,7 @@ export class CRServiceWorker extends Worker { } override didClose() { + this._networkManager?.removeSession(this._session); this._session.dispose(); super.didClose(); } diff --git a/tests/page/workers.spec.ts b/tests/page/workers.spec.ts index 9ad0e32238..3b4d1bf5c8 100644 --- a/tests/page/workers.spec.ts +++ b/tests/page/workers.spec.ts @@ -224,3 +224,32 @@ it('should report and intercept network from nested worker', async function({ pa await expect.poll(() => messages).toEqual(['{"foo":"not bar"}', '{"foo":"not bar"}']); }); + +it('should support extra http headers', async ({ page, server }) => { + await page.setExtraHTTPHeaders({ foo: 'bar' }); + const [worker, request1] = await Promise.all([ + page.waitForEvent('worker'), + server.waitForRequest('/worker/worker.js'), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + const [request2] = await Promise.all([ + server.waitForRequest('/one-style.css'), + worker.evaluate(url => fetch(url), server.PREFIX + '/one-style.css'), + ]); + expect(request1.headers['foo']).toBe('bar'); + expect(request2.headers['foo']).toBe('bar'); +}); + +it('should support offline', async ({ page, server, browserName }) => { + it.fixme(browserName === 'firefox'); + + const [worker] = await Promise.all([ + page.waitForEvent('worker'), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + await page.context().setOffline(true); + expect(await worker.evaluate(() => navigator.onLine)).toBe(false); + expect(await worker.evaluate(() => fetch('/one-style.css').catch(e => 'error'))).toBe('error'); + await page.context().setOffline(false); + expect(await worker.evaluate(() => navigator.onLine)).toBe(true); +});