diff --git a/docs/api.md b/docs/api.md index 6c30b9c2961c7..892ea053e42ea 100644 --- a/docs/api.md +++ b/docs/api.md @@ -166,7 +166,7 @@ * [page.setGeolocation(options)](#pagesetgeolocationoptions) * [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) * [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled) - * [page.setRequestInterception(value)](#pagesetrequestinterceptionvalue) + * [page.setRequestInterception(value[, cacheSafe])](#pagesetrequestinterceptionvalue-cachesafe) * [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) * [page.setViewport(viewport)](#pagesetviewportviewport) * [page.tap(selector)](#pagetapselector) @@ -2039,8 +2039,9 @@ await page.setGeolocation({latitude: 59.95, longitude: 30.31667}); - `enabled` <[boolean]> When `true`, enables offline mode for the page. - returns: <[Promise]> -#### page.setRequestInterception(value) +#### page.setRequestInterception(value[, cacheSafe]) - `value` <[boolean]> Whether to enable request interception. +- `cacheSafe` <[boolean]> Whether to trust browser caching. If set to false, enabling request interception disables page caching. Defaults to false. - returns: <[Promise]> Activating request interception enables `request.abort`, `request.continue` and diff --git a/src/common/NetworkManager.ts b/src/common/NetworkManager.ts index 6c82d60fa491b..f0eb65f0ddc20 100644 --- a/src/common/NetworkManager.ts +++ b/src/common/NetworkManager.ts @@ -54,6 +54,7 @@ export interface InternalNetworkConditions extends NetworkConditions { */ export const NetworkManagerEmittedEvents = { Request: Symbol('NetworkManager.Request'), + RequestServedFromCache: Symbol('NetworkManager.RequestServedFromCache'), Response: Symbol('NetworkManager.Response'), RequestFailed: Symbol('NetworkManager.RequestFailed'), RequestFinished: Symbol('NetworkManager.RequestFinished'), @@ -75,6 +76,7 @@ export class NetworkManager extends EventEmitter { _credentials?: Credentials = null; _attemptedAuthentications = new Set(); _userRequestInterceptionEnabled = false; + _userRequestInterceptionCacheSafe = false; _protocolRequestInterceptionEnabled = false; _userCacheDisabled = false; _requestIdToInterceptionId = new Map(); @@ -189,8 +191,12 @@ export class NetworkManager extends EventEmitter { await this._updateProtocolCacheDisabled(); } - async setRequestInterception(value: boolean): Promise { + async setRequestInterception( + value: boolean, + cacheSafe = false + ): Promise { this._userRequestInterceptionEnabled = value; + this._userRequestInterceptionCacheSafe = cacheSafe; await this._updateProtocolRequestInterception(); } @@ -217,7 +223,9 @@ export class NetworkManager extends EventEmitter { async _updateProtocolCacheDisabled(): Promise { await this._client.send('Network.setCacheDisabled', { cacheDisabled: - this._userCacheDisabled || this._userRequestInterceptionEnabled, + this._userCacheDisabled || + (this._userRequestInterceptionEnabled && + !this._userRequestInterceptionCacheSafe), }); } @@ -323,6 +331,7 @@ export class NetworkManager extends EventEmitter { ): void { const request = this._requestIdToRequest.get(event.requestId); if (request) request._fromMemoryCache = true; + this.emit(NetworkManagerEmittedEvents.RequestServedFromCache, request); } _handleRequestRedirect( diff --git a/src/common/Page.ts b/src/common/Page.ts index 92c39fa660bea..b708c23ba9b7c 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -287,6 +287,14 @@ export const enum PageEmittedEvents { * and mutating requests. */ Request = 'request', + /** + * Emitted when a request ended up loading from cache. Contains a {@link HTTPRequest}. + * + * @remarks + * For certain requests, might contain undefined. + * @see https://crbug.com/750469 + */ + RequestServedFromCache = 'requestservedfromcache', /** * Emitted when a request fails, for example by timing out. * @@ -483,6 +491,10 @@ export class Page extends EventEmitter { networkManager.on(NetworkManagerEmittedEvents.Request, (event) => this.emit(PageEmittedEvents.Request, event) ); + networkManager.on( + NetworkManagerEmittedEvents.RequestServedFromCache, + (event) => this.emit(PageEmittedEvents.RequestServedFromCache, event) + ); networkManager.on(NetworkManagerEmittedEvents.Response, (event) => this.emit(PageEmittedEvents.Response, event) ); @@ -688,6 +700,8 @@ export class Page extends EventEmitter { /** * @param value - Whether to enable request interception. + * @param cacheSafe - Whether to trust browser caching. If set to false, + * enabling request interception disables page caching. Defaults to false. * * @remarks * Activating request interception enables {@link HTTPRequest.abort}, @@ -695,9 +709,7 @@ export class Page extends EventEmitter { * 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. - * - * **NOTE** Enabling request interception disables page caching. + * continued, responded or aborted; or completed using the browser cache. * * @example * An example of a naïve request interceptor that aborts all image requests: @@ -719,8 +731,13 @@ export class Page extends EventEmitter { * })(); * ``` */ - async setRequestInterception(value: boolean): Promise { - return this._frameManager.networkManager().setRequestInterception(value); + async setRequestInterception( + value: boolean, + cacheSafe = false + ): Promise { + return this._frameManager + .networkManager() + .setRequestInterception(value, cacheSafe); } /** diff --git a/test/network.spec.ts b/test/network.spec.ts index 630771d1d90aa..787fee18f43ef 100644 --- a/test/network.spec.ts +++ b/test/network.spec.ts @@ -355,6 +355,20 @@ describe('network', function () { expect(requests[0].frame() === page.mainFrame()).toBe(true); expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE); }); + it('Page.Events.RequestServedFromCache', async () => { + const { page, server } = getTestState(); + + const cached = []; + page.on('requestservedfromcache', (r) => + cached.push(r.url().split('/').pop()) + ); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + expect(cached).toEqual([]); + + await page.reload(); + expect(cached).toEqual(['one-style.css']); + }); it('Page.Events.Response', async () => { const { page, server } = getTestState(); diff --git a/test/requestinterception.spec.ts b/test/requestinterception.spec.ts index de3c9b24ab870..35e225bb41d8f 100644 --- a/test/requestinterception.spec.ts +++ b/test/requestinterception.spec.ts @@ -495,6 +495,36 @@ describe('request interception', function () { expect(urls.has('one-style.html')).toBe(true); expect(urls.has('one-style.css')).toBe(true); }); + it('should not cache if not cache-safe', async () => { + const { page, server } = getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true, false); + page.on('request', (request) => request.continue()); + + const cached = []; + page.on('requestservedfromcache', (r) => cached.push(r)); + + await page.reload(); + expect(cached.length).toBe(0); + }); + it('should cache if cache-safe', async () => { + const { page, server } = getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true, true); + page.on('request', (request) => request.continue()); + + const cached = []; + page.on('requestservedfromcache', (r) => cached.push(r)); + + await page.reload(); + expect(cached.length).toBe(1); + }); }); describeFailsFirefox('Request.continue', function () {