diff --git a/docs/api.md b/docs/api.md index 3151985245..45816e00e5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -212,9 +212,6 @@ Indicates that the browser is connected. - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`. - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`. - `userAgent` Specific user agent to use in this context. - - `cacheEnabled` Toggles HTTP cache in the context. By default HTTP cache is enabled. - - `interceptNetwork` Toggles network interception in the context. Defaults to false. - - `offlineMode` Toggles offline network mode in the context. Defaults to false. - `javaScriptEnabled` Whether or not to enable or disable JavaScript in the context. Defaults to true. - `timezoneId` Changes the timezone of the context. See [ICU’s `metaZones.txt`](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) for a list of supported timezone IDs. - `geolocation` <[Object]> @@ -264,12 +261,9 @@ await context.close(); - [browserContext.cookies([...urls])](#browsercontextcookiesurls) - [browserContext.newPage(url)](#browsercontextnewpageurl) - [browserContext.pages()](#browsercontextpages) -- [browserContext.setCacheEnabled([enabled])](#browsercontextsetcacheenabledenabled) - [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) - [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation) -- [browserContext.setOfflineMode(enabled)](#browsercontextsetofflinemodeenabled) - [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) -- [browserContext.setRequestInterception(enabled)](#browsercontextsetrequestinterceptionenabled) #### browserContext.clearCookies() @@ -327,12 +321,6 @@ Creates a new page in the browser context and optionally navigates it to the spe An array of all pages inside the browser context. -#### browserContext.setCacheEnabled([enabled]) -- `enabled` <[boolean]> sets the `enabled` state of the HTTP cache. -- returns: <[Promise]> - -Toggles ignoring HTTP cache for each request based on the enabled state. By default, HTTP cache is enabled. - #### browserContext.setCookies(cookies) - `cookies` <[Array]<[Object]>> - `name` <[string]> **required** @@ -365,10 +353,6 @@ await browserContext.setGeolocation({latitude: 59.95, longitude: 30.31667}); > **NOTE** Consider using [browserContext.setPermissions](#browsercontextsetpermissions-permissions) to grant permissions for the page to read its geolocation. -#### browserContext.setOfflineMode(enabled) -- `enabled` <[boolean]> When `true`, enables offline mode for the context. -- returns: <[Promise]> - #### browserContext.setPermissions(origin, permissions[]) - `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". - `permissions` <[Array]<[string]>> An array of permissions to grant. All permissions that are not listed here will be automatically denied. Permissions can be one of the following values: @@ -396,32 +380,6 @@ const context = browser.defaultContext(); await context.setPermissions('https://html5demos.com', ['geolocation']); ``` -#### browserContext.setRequestInterception(enabled) -- `enabled` <[boolean]> Whether to enable request interception. -- returns: <[Promise]> - -Activating request interception enables `request.abort`, `request.continue` and -`request.respond` methods. This provides the capability to modify network requests that are made by all pages in the context. - -Once request interception is enabled, every request will stall unless it's continued, responded or aborted. -An example of a naïve request interceptor that aborts all image requests: - -```js -const context = await browser.newContext(); -const page = await context.newPage(); -page.on('request', interceptedRequest => { - if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg')) - interceptedRequest.abort(); - else - interceptedRequest.continue(); -}); -await context.setRequestInterception(true); -await page.goto('https://example.com'); -await browser.close(); -``` - -> **NOTE** Enabling request interception disables HTTP cache. - ### class: Page * extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) @@ -513,10 +471,13 @@ page.removeListener('request', logRequest); - [page.reload([options])](#pagereloadoptions) - [page.screenshot([options])](#pagescreenshotoptions) - [page.select(selector, value, options)](#pageselectselector-value-options) +- [page.setCacheEnabled([enabled])](#pagesetcacheenabledenabled) - [page.setContent(html[, options])](#pagesetcontenthtml-options) - [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) - [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) - [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) +- [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled) +- [page.setRequestInterception(enabled)](#pagesetrequestinterceptionenabled) - [page.setViewport(viewport)](#pagesetviewportviewport) - [page.title()](#pagetitle) - [page.tripleclick(selector[, options])](#pagetripleclickselector-options) @@ -1261,6 +1222,12 @@ page.select('select#colors', { value: 'blue' }, { index: 2 }, 'red'); Shortcut for [page.mainFrame().select()](#frameselectselector-values) +#### page.setCacheEnabled([enabled]) +- `enabled` <[boolean]> sets the `enabled` state of the cache. +- returns: <[Promise]> + +Toggles ignoring cache for each request based on the enabled state. By default, caching is enabled. + #### page.setContent(html[, options]) - `html` <[string]> HTML markup to assign to the page. - `options` <[Object]> Parameters which might have the following properties: @@ -1312,6 +1279,35 @@ The extra HTTP headers will be sent with every request the page initiates. > **NOTE** page.setExtraHTTPHeaders does not guarantee the order of headers in the outgoing requests. +#### page.setOfflineMode(enabled) +- `enabled` <[boolean]> When `true`, enables offline mode for the page. +- returns: <[Promise]> + +#### page.setRequestInterception(enabled) +- `enabled` <[boolean]> Whether to enable request interception. +- returns: <[Promise]> + +Activating request interception enables `request.abort`, `request.continue` and +`request.respond` methods. This provides the capability to modify network requests that are made by a page. + +Once request interception is enabled, every request will stall unless it's continued, responded or aborted. +An example of a naïve request interceptor that aborts all image requests: + +```js +const page = await browser.newPage(); +await page.setRequestInterception(true); +page.on('request', interceptedRequest => { + if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg')) + interceptedRequest.abort(); + else + interceptedRequest.continue(); +}); +await page.goto('https://example.com'); +await browser.close(); +``` + +> **NOTE** Enabling request interception disables page caching. + #### page.setViewport(viewport) - `viewport` <[Object]> - `width` <[number]> page width in pixels. **required** diff --git a/src/browserContext.ts b/src/browserContext.ts index a1c48c4202..07824d5eac 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -42,9 +42,6 @@ export type BrowserContextOptions = { javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, - cacheEnabled?: boolean; - interceptNetwork?: boolean; - offlineMode?: boolean; timezoneId?: string, geolocation?: types.Geolocation, permissions?: { [key: string]: string[] }; @@ -115,27 +112,6 @@ export class BrowserContext { await this._delegate.setGeolocation(geolocation); } - async setCacheEnabled(enabled: boolean = true) { - if (this._options.cacheEnabled === enabled) - return; - this._options.cacheEnabled = enabled; - await Promise.all(this._existingPages().map(page => page._delegate.setCacheEnabled(enabled))); - } - - async setRequestInterception(enabled: boolean) { - if (this._options.interceptNetwork === enabled) - return; - this._options.interceptNetwork = enabled; - await Promise.all(this._existingPages().map(page => page._delegate.setRequestInterception(enabled))); - } - - async setOfflineMode(enabled: boolean) { - if (this._options.offlineMode === enabled) - return; - this._options.offlineMode = enabled; - await Promise.all(this._existingPages().map(page => page._delegate.setOfflineMode(enabled))); - } - async close() { if (this._closed) return; diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index b08a68013f..fad536e1ab 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -29,9 +29,12 @@ export class CRNetworkManager { private _page: Page; private _requestIdToRequest = new Map(); private _requestIdToRequestWillBeSentEvent = new Map(); + private _offline = false; private _credentials: {username: string, password: string} | null = null; private _attemptedAuthentications = new Set(); + private _userRequestInterceptionEnabled = false; private _protocolRequestInterceptionEnabled = false; + private _userCacheDisabled = false; private _requestIdToInterceptionId = new Map(); private _eventListeners: RegisteredListener[]; @@ -60,17 +63,7 @@ export class CRNetworkManager { } async initialize() { - const promises: Promise[] = [ - this._client.send('Network.enable') - ]; - const options = this._page.browserContext()._options; - if (options.offlineMode) - promises.push(this.setOfflineMode(options.offlineMode)); - if (this._userRequestInterceptionEnabled()) - promises.push(this._updateProtocolRequestInterception()); - else if (options.cacheEnabled === false) - promises.push(this._updateProtocolCacheDisabled()); - await Promise.all(promises); + await this._client.send('Network.enable'); } dispose() { @@ -82,9 +75,10 @@ export class CRNetworkManager { await this._updateProtocolRequestInterception(); } - async setOfflineMode(offline: boolean) { + async setOfflineMode(value: boolean) { + this._offline = value; await this._client.send('Network.emulateNetworkConditions', { - offline, + offline: this._offline, // values of 0 remove any active throttling. crbug.com/456324#c9 latency: 0, downloadThroughput: -1, @@ -97,19 +91,17 @@ export class CRNetworkManager { } async setCacheEnabled(enabled: boolean) { + this._userCacheDisabled = !enabled; await this._updateProtocolCacheDisabled(); } async setRequestInterception(value: boolean) { + this._userRequestInterceptionEnabled = value; await this._updateProtocolRequestInterception(); } - private _userRequestInterceptionEnabled() : boolean { - return !!this._page.browserContext()._options.interceptNetwork; - } - - private async _updateProtocolRequestInterception() { - const enabled = this._userRequestInterceptionEnabled() || !!this._credentials; + async _updateProtocolRequestInterception() { + const enabled = this._userRequestInterceptionEnabled || !!this._credentials; if (enabled === this._protocolRequestInterceptionEnabled) return; this._protocolRequestInterceptionEnabled = enabled; @@ -129,15 +121,13 @@ export class CRNetworkManager { } } - private async _updateProtocolCacheDisabled() { - const options = this._page.browserContext()._options; - const cacheDisabled = options.cacheEnabled === false; + async _updateProtocolCacheDisabled() { await this._client.send('Network.setCacheDisabled', { - cacheDisabled: cacheDisabled || this._protocolRequestInterceptionEnabled + cacheDisabled: this._userCacheDisabled || this._protocolRequestInterceptionEnabled }); } - private _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) { + _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) { // Request interception doesn't happen for data URLs with Network Service. if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) { const requestId = event.requestId; @@ -153,7 +143,7 @@ export class CRNetworkManager { this._onRequest(event, null); } - private _onAuthRequired(event: Protocol.Fetch.authRequiredPayload) { + _onAuthRequired(event: Protocol.Fetch.authRequiredPayload) { let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default'; if (this._attemptedAuthentications.has(event.requestId)) { response = 'CancelAuth'; @@ -168,8 +158,8 @@ export class CRNetworkManager { }).catch(debugError); } - private _onRequestPaused(event: Protocol.Fetch.requestPausedPayload) { - if (!this._userRequestInterceptionEnabled() && this._protocolRequestInterceptionEnabled) { + _onRequestPaused(event: Protocol.Fetch.requestPausedPayload) { + if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) { this._client.send('Fetch.continueRequest', { requestId: event.requestId }).catch(debugError); @@ -188,7 +178,7 @@ export class CRNetworkManager { } } - private _onRequest(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) { + _onRequest(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) { if (event.request.url.startsWith('data:')) return; let redirectChain: network.Request[] = []; @@ -204,12 +194,12 @@ export class CRNetworkManager { const frame = event.frameId ? this._page._frameManager.frame(event.frameId) : null; const isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document'; const documentId = isNavigationRequest ? event.loaderId : undefined; - const request = new InterceptableRequest(this._client, frame, interceptionId, documentId, this._userRequestInterceptionEnabled(), event, redirectChain); + const request = new InterceptableRequest(this._client, frame, interceptionId, documentId, this._userRequestInterceptionEnabled, event, redirectChain); this._requestIdToRequest.set(event.requestId, request); this._page._frameManager.requestStarted(request.request); } - private _createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response { + _createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response { const getResponseBody = async () => { const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId }); return platform.Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); @@ -217,7 +207,7 @@ export class CRNetworkManager { return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody); } - private _handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) { + _handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) { const response = this._createResponse(request, responsePayload); request.request._redirectChain.push(request.request); response._requestFinished(new Error('Response body is unavailable for redirect responses')); @@ -228,7 +218,7 @@ export class CRNetworkManager { this._page._frameManager.requestFinished(request.request); } - private _onResponseReceived(event: Protocol.Network.responseReceivedPayload) { + _onResponseReceived(event: Protocol.Network.responseReceivedPayload) { const request = this._requestIdToRequest.get(event.requestId); // FileUpload sends a response without a matching request. if (!request) @@ -237,7 +227,7 @@ export class CRNetworkManager { this._page._frameManager.requestReceivedResponse(response); } - private _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) { + _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) { const request = this._requestIdToRequest.get(event.requestId); // For certain requestIds we never receive requestWillBeSent event. // @see https://crbug.com/750469 @@ -255,7 +245,7 @@ export class CRNetworkManager { this._page._frameManager.requestFinished(request.request); } - private _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) { + _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) { const request = this._requestIdToRequest.get(event.requestId); // For certain requestIds we never receive requestWillBeSent event. // @see https://crbug.com/750469 diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 33ca22aa18..af4b362f7c 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -89,10 +89,6 @@ export class FFPage implements PageDelegate { promises.push(this._session.send('Page.setJavascriptEnabled', { enabled: false })); if (options.userAgent) promises.push(this._session.send('Page.setUserAgent', { userAgent: options.userAgent })); - if (options.cacheEnabled === false) - promises.push(this.setCacheEnabled(false)); - if (options.interceptNetwork) - promises.push(this.setRequestInterception(true)); await Promise.all(promises); } diff --git a/src/page.ts b/src/page.ts index 557bd7d473..da30913364 100644 --- a/src/page.ts +++ b/src/page.ts @@ -77,6 +77,9 @@ type PageState = { mediaType: types.MediaType | null; colorScheme: types.ColorScheme | null; extraHTTPHeaders: network.Headers | null; + cacheEnabled: boolean | null; + interceptNetwork: boolean | null; + offlineMode: boolean | null; credentials: types.Credentials | null; hasTouch: boolean | null; }; @@ -120,6 +123,9 @@ export class Page extends platform.EventEmitter { mediaType: null, colorScheme: null, extraHTTPHeaders: null, + cacheEnabled: null, + interceptNetwork: null, + offlineMode: null, credentials: null, hasTouch: null, }; @@ -397,6 +403,27 @@ export class Page extends platform.EventEmitter { await this._delegate.evaluateOnNewDocument(source); } + async setCacheEnabled(enabled: boolean = true) { + if (this._state.cacheEnabled === enabled) + return; + this._state.cacheEnabled = enabled; + await this._delegate.setCacheEnabled(enabled); + } + + async setRequestInterception(enabled: boolean) { + if (this._state.interceptNetwork === enabled) + return; + this._state.interceptNetwork = enabled; + await this._delegate.setRequestInterception(enabled); + } + + async setOfflineMode(enabled: boolean) { + if (this._state.offlineMode === enabled) + return; + this._state.offlineMode = enabled; + await this._delegate.setOfflineMode(enabled); + } + async authenticate(credentials: types.Credentials | null) { this._state.credentials = credentials; await this._delegate.authenticate(credentials); diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 8417b541d9..8a49093b6a 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -121,13 +121,14 @@ export class WKPage implements PageDelegate { this._workers.initializeSession(session) ]; - const contextOptions = this._page.browserContext()._options; - if (contextOptions.interceptNetwork) + if (this._page._state.interceptNetwork) promises.push(session.send('Network.setInterceptionEnabled', { enabled: true, interceptRequests: true })); - if (contextOptions.offlineMode) + if (this._page._state.offlineMode) promises.push(session.send('Network.setEmulateOfflineState', { offline: true })); - if (contextOptions.cacheEnabled === false) + if (this._page._state.cacheEnabled === false) promises.push(session.send('Network.setResourceCachingDisabled', { disabled: true })); + + const contextOptions = this._page.browserContext()._options; if (contextOptions.userAgent) promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent })); if (this._page._state.mediaType || this._page._state.colorScheme) @@ -584,7 +585,7 @@ export class WKPage implements PageDelegate { // TODO(einbinder) this will fail if we are an XHR document request const isNavigationRequest = event.type === 'Document'; const documentId = isNavigationRequest ? event.loaderId : undefined; - const request = new WKInterceptableRequest(session, !!this._page.browserContext()._options.interceptNetwork, frame, event, redirectChain, documentId); + const request = new WKInterceptableRequest(session, !!this._page._state.interceptNetwork, frame, event, redirectChain, documentId); this._requestIdToRequest.set(event.requestId, request); this._page._frameManager.requestStarted(request.request); } diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 90525a545e..f4c1e4700a 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -17,7 +17,7 @@ const utils = require('./utils'); -module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WEBKIT, FFOX}) { +module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WEBKIT}) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit, dit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; @@ -313,40 +313,4 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE await page.goto(server.EMPTY_PAGE); }); }); - - describe.skip(FFOX)('BrowserContext({offlineMode})', function() { - it('should create offline pages', async({newPage, server}) => { - const page = await newPage({ offlineMode: true }); - expect(await page.evaluate(() => window.navigator.onLine)).toBe(false); - let error = null; - await page.goto(server.EMPTY_PAGE).catch(e => error = e); - expect(error).toBeTruthy(); - }); - }); - - describe('BrowserContext({cacheEnabled})', function() { - it('should create pages with disabled cache', async({newPage, server}) => { - const page = await newPage({ cacheEnabled: false }); - await page.goto(server.PREFIX + '/cached/one-style.html'); - // WebKit does r.setCachePolicy(ResourceRequestCachePolicy::ReloadIgnoringCacheData); - // when navigating to the same url, load empty.html to avoid that. - await page.goto(server.EMPTY_PAGE); - const [cachedRequest] = await Promise.all([ - server.waitForRequest('/cached/one-style.html'), - page.goto(server.PREFIX + '/cached/one-style.html'), - ]); - // Rely on "if-modified-since" caching in our test server. - expect(cachedRequest.headers['if-modified-since']).toBe(undefined); - }); - }); - - describe('BrowserContext({interceptNetwork})', function() { - it('should enable request interception', async({newPage, httpsServer}) => { - const page = await newPage({ ignoreHTTPSErrors: true, interceptNetwork: true }); - page.on('request', request => request.abort()); - let error; - await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); - expect(error).toBeTruthy(); - }); - }); }; diff --git a/test/chromium/chromium.spec.js b/test/chromium/chromium.spec.js index 0cd18d9702..e6c5a3c2fc 100644 --- a/test/chromium/chromium.spec.js +++ b/test/chromium/chromium.spec.js @@ -224,7 +224,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI }); describe('Chromium-Specific Page Tests', function() { - it('BrowserContext.setRequestInterception should work with intervention headers', async({context, server, page}) => { + it('Page.setRequestInterception should work with intervention headers', async({server, page}) => { server.setRoute('/intervention', (req, res) => res.end(`