diff --git a/README.md b/README.md index 4a08b6dc15..3cda23e711 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ const { firefox } = require('playwright'); #### Intercept network requests -This code snippet sets up network interception for a WebKit page to log all network requests. +This code snippet sets up request routing for a WebKit page to log all network requests. ```js const { webkit } = require('playwright'); @@ -112,9 +112,9 @@ const { webkit } = require('playwright'); const page = await context.newPage(); // Log and continue all network requests - page.route('**', request => { - console.log(request.url()); - request.continue(); + page.route('**', route => { + console.log(route.request().url()); + route.continue(); }); await page.goto('http://todomvc.com'); diff --git a/docs/api.md b/docs/api.md index 943e5660d9..0206885e41 100644 --- a/docs/api.md +++ b/docs/api.md @@ -20,6 +20,7 @@ - [class: Request](#class-request) - [class: Response](#class-response) - [class: Selectors](#class-selectors) +- [class: Route](#class-route) - [class: TimeoutError](#class-timeouterror) - [class: Accessibility](#class-accessibility) - [class: Worker](#class-worker) @@ -456,17 +457,17 @@ Creates a new page in the browser context. #### browserContext.route(url, handler) - `url` <[string]|[RegExp]|[function]\([string]\):[boolean]> A glob pattern, regex pattern or predicate receiving [URL] to match while routing. -- `handler` <[function]\([Request]\)> handler function to route the request. -- returns: <[Promise]>. +- `handler` <[function]\([Route], [Request]\)> handler function to route the request. +- returns: <[Promise]> -Routing activates the request interception and enables `request.abort`, `request.continue` and `request.fulfill` methods on the request. This provides the capability to modify network requests that are made by any page in the browser context. +Routing provides the capability to modify network requests that are made by any page in the browser context. +Once route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. -Once request interception is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. -An example of a naïve request interceptor that aborts all image requests: +An example of a naïve handler that aborts all image requests: ```js const context = await browser.newContext(); -await context.route('**/*.{png,jpg,jpeg}', request => request.abort()); +await context.route('**/*.{png,jpg,jpeg}', route => route.abort()); const page = await context.newPage(); await page.goto('https://example.com'); await browser.close(); @@ -476,7 +477,7 @@ or the same snippet using a regex pattern instead: ```js const context = await browser.newContext(); -await context.route(/(\.png$)|(\.jpg$)/, request => request.abort()); +await context.route(/(\.png$)|(\.jpg$)/, route => route.abort()); const page = await context.newPage(); await page.goto('https://example.com'); await browser.close(); @@ -484,7 +485,7 @@ await browser.close(); Page routes (set up with [page.route(url, handler)](#pagerouteurl-handler)) take precedence over browser context routes when request matches both handlers. -> **NOTE** Enabling request interception disables http cache. +> **NOTE** Enabling routing disables http cache. #### browserContext.setDefaultNavigationTimeout(timeout) - `timeout` <[number]> Maximum navigation time in milliseconds @@ -783,7 +784,7 @@ const popup = await event.page(); - <[Request]> Emitted when a page issues a request. The [request] object is read-only. -In order to intercept and mutate requests, see `page.route()`. +In order to intercept and mutate requests, see !!!`page.route()` or `brows. #### event: 'requestfailed' - <[Request]> @@ -1423,18 +1424,18 @@ If `key` is a single character and no modifier keys besides `Shift` are being he #### page.route(url, handler) - `url` <[string]|[RegExp]|[function]\([string]\):[boolean]> A glob pattern, regex pattern or predicate receiving [URL] to match while routing. -- `handler` <[function]\([Request]\)> handler function to route the request. +- `handler` <[function]\([Route], [Request]\)> handler function to route the request. - returns: <[Promise]>. -Routing activates the request interception and enables `request.abort`, `request.continue` and -`request.fulfill` methods on the request. This provides the capability to modify network requests that are made by a page. +Routing provides the capability to modify network requests that are made by a page. -Once request interception is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. -An example of a naïve request interceptor that aborts all image requests: +Once routing is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. + +An example of a naïve handler that aborts all image requests: ```js const page = await browser.newPage(); -await page.route('**/*.{png,jpg,jpeg}', request => request.abort()); +await page.route('**/*.{png,jpg,jpeg}', route => route.abort()); await page.goto('https://example.com'); await browser.close(); ``` @@ -1443,14 +1444,14 @@ or the same snippet using a regex pattern instead: ```js const page = await browser.newPage(); -await page.route(/(\.png$)|(\.jpg$)/, request => request.abort()); +await page.route(/(\.png$)|(\.jpg$)/, route => route.abort()); await page.goto('https://example.com'); await browser.close(); ``` Page routes take precedence over browser context routes (set up with [browserContext.route(url, handler)](#browsercontextrouteurl-handler)) when request matches both handlers. -> **NOTE** Enabling request interception disables http cache. +> **NOTE** Enabling rouing disables http cache. #### page.screenshot([options]) - `options` <[Object]> Options object which might have the following properties: @@ -3131,11 +3132,8 @@ If request fails at some point, then instead of `'requestfinished'` event (and p If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new request is issued to a redirected url. -- [request.abort([errorCode])](#requestaborterrorcode) -- [request.continue([overrides])](#requestcontinueoverrides) - [request.failure()](#requestfailure) - [request.frame()](#requestframe) -- [request.fulfill(response)](#requestfulfillresponse) - [request.headers()](#requestheaders) - [request.isNavigationRequest()](#requestisnavigationrequest) - [request.method()](#requestmethod) @@ -3146,50 +3144,6 @@ If request gets a 'redirect' response, the request is successfully finished with - [request.url()](#requesturl) -#### request.abort([errorCode]) -- `errorCode` <[string]> Optional error code. Defaults to `failed`, could be - one of the following: - - `aborted` - An operation was aborted (due to user action) - - `accessdenied` - Permission to access a resource, other than the network, was denied - - `addressunreachable` - The IP address is unreachable. This usually means - that there is no route to the specified host or network. - - `blockedbyclient` - The client chose to block the request. - - `blockedbyresponse` - The request failed because the response was delivered along with requirements which are not met ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance). - - `connectionaborted` - A connection timed out as a result of not receiving an ACK for data sent. - - `connectionclosed` - A connection was closed (corresponding to a TCP FIN). - - `connectionfailed` - A connection attempt failed. - - `connectionrefused` - A connection attempt was refused. - - `connectionreset` - A connection was reset (corresponding to a TCP RST). - - `internetdisconnected` - The Internet connection has been lost. - - `namenotresolved` - The host name could not be resolved. - - `timedout` - An operation timed out. - - `failed` - A generic failure occurred. -- returns: <[Promise]> - -Aborts request. To use this, request interception should be enabled with `page.route`. -Exception is immediately thrown if the request interception is not enabled. - -#### request.continue([overrides]) -- `overrides` <[Object]> Optional request overrides, which can be one of the following: - - `method` <[string]> If set changes the request method (e.g. GET or POST) - - `postData` <[string]> If set changes the post data of request - - `headers` <[Object]> If set changes the request HTTP headers. Header values will be converted to a string. -- returns: <[Promise]> - -Continues request with optional request overrides. To use this, request interception should be enabled with `page.route`. -Exception is immediately thrown if the request interception is not enabled. - -```js -await page.route('**/*', request => { - // Override headers - const headers = Object.assign({}, request.headers(), { - foo: 'bar', // set "foo" header - origin: undefined, // remove "origin" header - }); - request.continue({headers}); -}); -``` - #### request.failure() - returns: Object describing request failure, if any - `errorText` <[string]> Human-readable error message, e.g. `'net::ERR_FAILED'`. @@ -3208,37 +3162,6 @@ page.on('requestfailed', request => { #### request.frame() - returns: <[Frame]> A [Frame] that initiated this request. -#### request.fulfill(response) -- `response` <[Object]> Response that will fulfill this request - - `status` <[number]> Response status code, defaults to `200`. - - `headers` <[Object]> Optional response headers. Header values will be converted to a string. - - `contentType` <[string]> If set, equals to setting `Content-Type` response header. - - `body` <[string]|[Buffer]> Optional response body. - - `path` <[string]> Optional file path to respond with. The content type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). -- returns: <[Promise]> - -Fulfills request with given response. To use this, request interception should -be enabled with `page.route`. Exception is thrown if -request interception is not enabled. - -An example of fulfilling all requests with 404 responses: - -```js -await page.route('**/*', request => { - request.fulfill({ - status: 404, - contentType: 'text/plain', - body: 'Not Found!' - }); -}); -``` - -An example of serving static file: - -```js -await page.route('**/xhr_endpoint', request => request.fulfill({ path: 'mock_data.json' })); -``` - #### request.headers() - returns: <[Object]> An object with HTTP headers associated with the request. All header names are lower-case. @@ -3410,6 +3333,93 @@ const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webk })(); ``` + +### class: Route + +Whenever a network route is set up with [page.route(url, handler)](#pagerouteurl-handler) or [browserContext.route(url, handler)](#browsercontextrouteurl-handler), the `Route` object allows to handle the route. + + +- [route.abort([errorCode])](#routeaborterrorcode) +- [route.continue([overrides])](#routecontinueoverrides) +- [route.fulfill(response)](#routefulfillresponse) +- [route.request()](#routerequest) + + +#### route.abort([errorCode]) +- `errorCode` <[string]> Optional error code. Defaults to `failed`, could be + one of the following: + - `aborted` - An operation was aborted (due to user action) + - `accessdenied` - Permission to access a resource, other than the network, was denied + - `addressunreachable` - The IP address is unreachable. This usually means + that there is no route to the specified host or network. + - `blockedbyclient` - The client chose to block the request. + - `blockedbyresponse` - The request failed because the response was delivered along with requirements which are not met ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance). + - `connectionaborted` - A connection timed out as a result of not receiving an ACK for data sent. + - `connectionclosed` - A connection was closed (corresponding to a TCP FIN). + - `connectionfailed` - A connection attempt failed. + - `connectionrefused` - A connection attempt was refused. + - `connectionreset` - A connection was reset (corresponding to a TCP RST). + - `internetdisconnected` - The Internet connection has been lost. + - `namenotresolved` - The host name could not be resolved. + - `timedout` - An operation timed out. + - `failed` - A generic failure occurred. +- returns: <[Promise]> + +Aborts the route's request. + +#### route.continue([overrides]) +- `overrides` <[Object]> Optional request overrides, which can be one of the following: + - `method` <[string]> If set changes the request method (e.g. GET or POST) + - `postData` <[string]> If set changes the post data of request + - `headers` <[Object]> If set changes the request HTTP headers. Header values will be converted to a string. +- returns: <[Promise]> + +Continues route's request with optional overrides. + +```js +await page.route('**/*', (route, request) => { + // Override headers + const headers = Object.assign({}, request.headers(), { + foo: 'bar', // set "foo" header + origin: undefined, // remove "origin" header + }); + route.continue({headers}); +}); +``` + +#### route.fulfill(response) +- `response` <[Object]> Response that will fulfill this route's request. + - `status` <[number]> Response status code, defaults to `200`. + - `headers` <[Object]> Optional response headers. Header values will be converted to a string. + - `contentType` <[string]> If set, equals to setting `Content-Type` response header. + - `body` <[string]|[Buffer]> Optional response body. + - `path` <[string]> Optional file path to respond with. The content type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). +- returns: <[Promise]> + +Fulfills route's request with given response. + +An example of fulfilling all requests with 404 responses: + +```js +await page.route('**/*', route => { + route.fulfill({ + status: 404, + contentType: 'text/plain', + body: 'Not Found!' + }); +}); +``` + +An example of serving static file: + +```js +await page.route('**/xhr_endpoint', route => route.fulfill({ path: 'mock_data.json' })); +``` + +#### route.request() +- returns: <[Request]> A request to be routed. + + ### class: TimeoutError * extends: [Error] @@ -4052,6 +4062,7 @@ const { chromium } = require('playwright'); [RegExp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp [Request]: #class-request "Request" [Response]: #class-response "Response" +[Route]: #class-route "Route" [Selectors]: #class-selectors "Selectors" [Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable" [TimeoutError]: #class-timeouterror "TimeoutError" diff --git a/src/api.ts b/src/api.ts index 6fa58c6522..e042dfe12a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -24,7 +24,7 @@ export { TimeoutError } from './errors'; export { Frame } from './frames'; export { Keyboard, Mouse } from './input'; export { JSHandle } from './javascript'; -export { Request, Response } from './network'; +export { Request, Response, Route } from './network'; export { FileChooser, Page, PageEvent, Worker } from './page'; export { Selectors } from './selectors'; diff --git a/src/browserContext.ts b/src/browserContext.ts index 6b690d6280..5fd6db1367 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -63,7 +63,7 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement readonly _timeoutSettings = new TimeoutSettings(); readonly _pageBindings = new Map(); readonly _options: BrowserContextOptions; - readonly _routes: { url: types.URLMatch, handler: (request: network.Request) => any }[] = []; + readonly _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = []; _closed = false; private readonly _closePromise: Promise; private _closePromiseFulfill: ((error: Error) => void) | undefined; diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index ea79d67a03..25de43f687 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -241,7 +241,7 @@ export class CRNetworkManager { } } -class InterceptableRequest implements network.RequestDelegate { +class InterceptableRequest implements network.RouteDelegate { readonly request: network.Request; _requestId: string; _interceptionId: string | null; diff --git a/src/firefox/ffNetworkManager.ts b/src/firefox/ffNetworkManager.ts index 6e5210ef95..536024a6a9 100644 --- a/src/firefox/ffNetworkManager.ts +++ b/src/firefox/ffNetworkManager.ts @@ -141,7 +141,7 @@ const causeToResourceType: {[key: string]: string} = { TYPE_WEB_MANIFEST: 'manifest', }; -class InterceptableRequest implements network.RequestDelegate { +class InterceptableRequest implements network.RouteDelegate { readonly request: network.Request; _id: string; private _session: FFSession; diff --git a/src/network.ts b/src/network.ts index efbfbded80..bf60b91450 100644 --- a/src/network.ts +++ b/src/network.ts @@ -41,8 +41,6 @@ export type SetNetworkCookieParam = { sameSite?: 'Strict' | 'Lax' | 'None' }; -export type RouteHandler = (request: Request) => void; - export function filterCookies(cookies: NetworkCookie[], urls: string | string[] = []): NetworkCookie[] { if (!Array.isArray(urls)) urls = [ urls ]; @@ -95,7 +93,7 @@ function stripFragmentFromUrl(url: string): string { export type Headers = { [key: string]: string }; export class Request { - private _delegate: RequestDelegate | null; + readonly _routeDelegate: RouteDelegate | null; private _response: Response | null = null; _redirectChain: Request[]; _finalRequest: Request; @@ -110,12 +108,11 @@ export class Request { private _frame: frames.Frame; private _waitForResponsePromise: Promise; private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {}; - private _interceptionHandled = false; - constructor(delegate: RequestDelegate | null, frame: frames.Frame, redirectChain: Request[], documentId: string | undefined, + constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectChain: Request[], documentId: string | undefined, url: string, resourceType: string, method: string, postData: string | null, headers: Headers) { assert(!url.startsWith('data:'), 'Data urls should not fire requests'); - this._delegate = delegate; + this._routeDelegate = routeDelegate; this._frame = frame; this._redirectChain = redirectChain; this._finalRequest = this; @@ -189,17 +186,36 @@ export class Request { }; } + _route(): Route | null { + if (!this._routeDelegate) + return null; + return new Route(this, this._routeDelegate); + } +} + +export class Route { + private readonly _request: Request; + private readonly _delegate: RouteDelegate; + private _handled = false; + + constructor(request: Request, delegate: RouteDelegate) { + this._request = request; + this._delegate = delegate; + } + + request(): Request { + return this._request; + } + async abort(errorCode: string = 'failed') { - assert(this._delegate, 'Request Interception is not enabled!'); - assert(!this._interceptionHandled, 'Request is already handled!'); - this._interceptionHandled = true; + assert(!this._handled, 'Route is already handled!'); + this._handled = true; await this._delegate.abort(errorCode); } async fulfill(response: FulfillResponse & { path?: string }) { - assert(this._delegate, 'Request Interception is not enabled!'); - assert(!this._interceptionHandled, 'Request is already handled!'); - this._interceptionHandled = true; + assert(!this._handled, 'Route is already handled!'); + this._handled = true; if (response.path) { response = { status: response.status, @@ -212,16 +228,13 @@ export class Request { } async continue(overrides: { method?: string; headers?: Headers; postData?: string } = {}) { - assert(this._delegate, 'Request Interception is not enabled!'); - assert(!this._interceptionHandled, 'Request is already handled!'); + assert(!this._handled, 'Route is already handled!'); await this._delegate.continue(overrides); } - - _isIntercepted(): boolean { - return !!this._delegate; - } } +export type RouteHandler = (route: Route, request: Request) => void; + type GetResponseBodyCallback = () => Promise; export class Response { @@ -313,7 +326,7 @@ export type FulfillResponse = { body?: string | platform.BufferType, }; -export interface RequestDelegate { +export interface RouteDelegate { abort(errorCode: string): Promise; fulfill(response: FulfillResponse): Promise; continue(overrides: { method?: string; headers?: Headers; postData?: string; }): Promise; diff --git a/src/page.ts b/src/page.ts index 47f7767e6a..160267baa4 100644 --- a/src/page.ts +++ b/src/page.ts @@ -150,7 +150,7 @@ export class Page extends platform.EventEmitter { private _workers = new Map(); readonly pdf: ((options?: types.PDFOptions) => Promise) | undefined; readonly coverage: any; - readonly _routes: { url: types.URLMatch, handler: (request: network.Request) => any }[] = []; + readonly _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = []; _ownedContext: BrowserContext | undefined; constructor(delegate: PageDelegate, browserContext: BrowserContextBase) { @@ -419,21 +419,22 @@ export class Page extends platform.EventEmitter { _requestStarted(request: network.Request) { this.emit(Events.Page.Request, request); - if (!request._isIntercepted()) + const route = request._route(); + if (!route) return; for (const { url, handler } of this._routes) { if (platform.urlMatches(request.url(), url)) { - handler(request); + handler(route, request); return; } } for (const { url, handler } of this._browserContext._routes) { if (platform.urlMatches(request.url(), url)) { - handler(request); + handler(route, request); return; } } - request.continue(); + route.continue(); } async screenshot(options?: types.ScreenshotOptions): Promise { diff --git a/src/webkit/wkInterceptableRequest.ts b/src/webkit/wkInterceptableRequest.ts index afa46ba197..db111533fb 100644 --- a/src/webkit/wkInterceptableRequest.ts +++ b/src/webkit/wkInterceptableRequest.ts @@ -39,7 +39,7 @@ const errorReasons: { [reason: string]: string } = { 'failed': 'General', }; -export class WKInterceptableRequest implements network.RequestDelegate { +export class WKInterceptableRequest implements network.RouteDelegate { private readonly _session: WKSession; readonly request: network.Request; readonly _requestId: string; diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 5d3fe8a4db..27ecc3f7a7 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -371,8 +371,9 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, FF it('should intercept', async({browser, server}) => { const context = await browser.newContext(); let intercepted = false; - await context.route('**/empty.html', request => { + await context.route('**/empty.html', route => { intercepted = true; + const request = route.request(); expect(request.url()).toContain('empty.html'); expect(request.headers()['user-agent']).toBeTruthy(); expect(request.method()).toBe('GET'); @@ -381,7 +382,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, FF expect(request.resourceType()).toBe('document'); expect(request.frame() === page.mainFrame()).toBe(true); expect(request.frame().url()).toBe('about:blank'); - request.continue(); + route.continue(); }); const page = await context.newPage(); const response = await page.goto(server.EMPTY_PAGE); @@ -391,12 +392,12 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, FF }); it('should yield to page.route', async({browser, server}) => { const context = await browser.newContext(); - await context.route('**/empty.html', request => { - request.fulfill({ status: 200, body: 'context' }); + await context.route('**/empty.html', route => { + route.fulfill({ status: 200, body: 'context' }); }); const page = await context.newPage(); - await page.route('**/empty.html', request => { - request.fulfill({ status: 200, body: 'page' }); + await page.route('**/empty.html', route => { + route.fulfill({ status: 200, body: 'page' }); }); const response = await page.goto(server.EMPTY_PAGE); expect(response.ok()).toBe(true); diff --git a/test/chromium/chromium.spec.js b/test/chromium/chromium.spec.js index 1dd75e8325..1f215792f8 100644 --- a/test/chromium/chromium.spec.js +++ b/test/chromium/chromium.spec.js @@ -57,7 +57,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI res.end('console.log(1);'); }); - await page.route('*', request => request.continue()); + await page.route('*', route => route.continue()); await page.goto(server.PREFIX + '/intervention'); // Check for feature URL substring rather than https://www.chromestatus.com to // make it work with Edgium. diff --git a/test/chromium/oopif.spec.js b/test/chromium/oopif.spec.js index 127c7f026c..dc94e79e1c 100644 --- a/test/chromium/oopif.spec.js +++ b/test/chromium/oopif.spec.js @@ -51,7 +51,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, b expect(page.frames().length).toBe(2); }); it('should load oopif iframes with subresources and request interception', async function({browser, page, server, context}) { - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(await countOOPIFs(browser)).toBe(1); }); @@ -68,8 +68,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, b const browser = await browserType.launch(headfulOptions); const page = await browser.newPage(); await page.goto(server.EMPTY_PAGE); - await page.route('**/*', request => { - request.fulfill({body: 'YO, GOOGLE.COM'}); + await page.route('**/*', route => { + route.fulfill({body: 'YO, GOOGLE.COM'}); }); await page.evaluate(() => { const frame = document.createElement('iframe'); diff --git a/test/interception.spec.js b/test/interception.spec.js index 2378254c73..0ca4b2b9df 100644 --- a/test/interception.spec.js +++ b/test/interception.spec.js @@ -32,7 +32,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('Page.route', function() { it('should intercept', async({page, server}) => { let intercepted = false; - await page.route('**/empty.html', request => { + await page.route('**/empty.html', (route, request) => { + expect(route.request()).toBe(request); expect(request.url()).toContain('empty.html'); expect(request.headers()['user-agent']).toBeTruthy(); expect(request.method()).toBe('GET'); @@ -41,7 +42,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(request.resourceType()).toBe('document'); expect(request.frame() === page.mainFrame()).toBe(true); expect(request.frame().url()).toBe('about:blank'); - request.continue(); + route.continue(); intercepted = true; }); const response = await page.goto(server.EMPTY_PAGE); @@ -51,7 +52,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p it('should work when POST is redirected with 302', async({page, server}) => { server.setRedirect('/rredirect', '/empty.html'); await page.goto(server.EMPTY_PAGE); - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); await page.setContent(`
@@ -65,22 +66,22 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p // @see https://github.com/GoogleChrome/puppeteer/issues/3973 it('should work when header manipulation headers with redirect', async({page, server}) => { server.setRedirect('/rrredirect', '/empty.html'); - await page.route('**/*', request => { - const headers = Object.assign({}, request.headers(), { + await page.route('**/*', route => { + const headers = Object.assign({}, route.request().headers(), { foo: 'bar' }); - request.continue({ headers }); + route.continue({ headers }); }); await page.goto(server.PREFIX + '/rrredirect'); }); // @see https://github.com/GoogleChrome/puppeteer/issues/4743 it('should be able to remove headers', async({page, server}) => { - await page.route('**/*', request => { - const headers = Object.assign({}, request.headers(), { + await page.route('**/*', route => { + const headers = Object.assign({}, route.request().headers(), { foo: 'bar', origin: undefined, // remove "origin" header }); - request.continue({ headers }); + route.continue({ headers }); }); const [serverRequest] = await Promise.all([ @@ -92,9 +93,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should contain referer header', async({page, server}) => { const requests = []; - await page.route('**/*', request => { - requests.push(request); - request.continue(); + await page.route('**/*', route => { + requests.push(route.request()); + route.continue(); }); await page.goto(server.PREFIX + '/one-style.html'); expect(requests[1].url()).toContain('/one-style.css'); @@ -106,7 +107,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p await context.addCookies([{ url: server.EMPTY_PAGE, name: 'foo', value: 'bar'}]); // Setup request interception. - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); const response = await page.reload(); expect(response.status()).toBe(200); }); @@ -114,9 +115,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p await page.setExtraHTTPHeaders({ foo: 'bar' }); - await page.route('**/*', request => { - expect(request.headers()['foo']).toBe('bar'); - request.continue(); + await page.route('**/*', route => { + expect(route.request().headers()['foo']).toBe('bar'); + route.continue(); }); const response = await page.goto(server.EMPTY_PAGE); expect(response.ok()).toBe(true); @@ -125,7 +126,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p it('should work with redirect inside sync XHR', async({page, server}) => { await page.goto(server.EMPTY_PAGE); server.setRedirect('/logo.png', '/pptr.png'); - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); const status = await page.evaluate(async() => { const request = new XMLHttpRequest(); request.open('GET', '/logo.png', false); // `false` makes the request synchronous @@ -136,15 +137,15 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should work with custom referer headers', async({page, server}) => { await page.setExtraHTTPHeaders({ 'referer': server.EMPTY_PAGE }); - await page.route('**/*', request => { - expect(request.headers()['referer']).toBe(server.EMPTY_PAGE); - request.continue(); + await page.route('**/*', route => { + expect(route.request().headers()['referer']).toBe(server.EMPTY_PAGE); + route.continue(); }); const response = await page.goto(server.EMPTY_PAGE); expect(response.ok()).toBe(true); }); it('should be abortable', async({page, server}) => { - await page.route(/\.css$/, request => request.abort()); + await page.route(/\.css$/, route => route.abort()); let failedRequests = 0; page.on('requestfailed', event => ++failedRequests); const response = await page.goto(server.PREFIX + '/one-style.html'); @@ -153,7 +154,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(failedRequests).toBe(1); }); it('should be abortable with custom error codes', async({page, server}) => { - await page.route('**/*', request => request.abort('internetdisconnected')); + await page.route('**/*', route => route.abort('internetdisconnected')); let failedRequest = null; page.on('requestfailed', request => failedRequest = request); await page.goto(server.EMPTY_PAGE).catch(e => {}); @@ -169,7 +170,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p await page.setExtraHTTPHeaders({ referer: 'http://google.com/' }); - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); const [request] = await Promise.all([ server.waitForRequest('/grid.html'), page.goto(server.PREFIX + '/grid.html'), @@ -177,7 +178,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(request.headers['referer']).toBe('http://google.com/'); }); it('should fail navigation when aborting main resource', async({page, server}) => { - await page.route('**/*', request => request.abort()); + await page.route('**/*', route => route.abort()); let error = null; await page.goto(server.EMPTY_PAGE).catch(e => error = e); expect(error).toBeTruthy(); @@ -190,9 +191,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should work with redirects', async({page, server}) => { const requests = []; - await page.route('**/*', request => { - request.continue(); - requests.push(request); + await page.route('**/*', route => { + route.continue(); + requests.push(route.request()); }); server.setRedirect('/non-existing-page.html', '/non-existing-page-2.html'); server.setRedirect('/non-existing-page-2.html', '/non-existing-page-3.html'); @@ -216,9 +217,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should work with redirects for subresources', async({page, server}) => { const requests = []; - await page.route('**/*', request => { - request.continue(); - requests.push(request); + await page.route('**/*', route => { + route.continue(); + requests.push(route.request()); }); server.setRedirect('/one-style.css', '/two-style.css'); server.setRedirect('/two-style.css', '/three-style.css'); @@ -244,8 +245,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p let spinner = false; // Cancel 2nd request. - await page.route('**/*', request => { - spinner ? request.abort() : request.continue(); + await page.route('**/*', route => { + spinner ? route.abort() : route.continue(); spinner = !spinner; }); const results = await page.evaluate(() => Promise.all([ @@ -257,9 +258,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should navigate to dataURL and not fire dataURL requests', async({page, server}) => { const requests = []; - await page.route('**/*', request => { - requests.push(request); - request.continue(); + await page.route('**/*', route => { + requests.push(route.request()); + route.continue(); }); const dataURL = 'data:text/html,
yo
'; const response = await page.goto(dataURL); @@ -269,9 +270,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p it('should be able to fetch dataURL and not fire dataURL requests', async({page, server}) => { await page.goto(server.EMPTY_PAGE); const requests = []; - await page.route('**/*', request => { - requests.push(request); - request.continue(); + await page.route('**/*', route => { + requests.push(route.request()); + route.continue(); }); const dataURL = 'data:text/html,
yo
'; const text = await page.evaluate(url => fetch(url).then(r => r.text()), dataURL); @@ -280,9 +281,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should navigate to URL with hash and and fire requests without hash', async({page, server}) => { const requests = []; - await page.route('**/*', request => { - requests.push(request); - request.continue(); + await page.route('**/*', route => { + requests.push(route.request()); + route.continue(); }); const response = await page.goto(server.EMPTY_PAGE + '#hash'); expect(response.status()).toBe(200); @@ -293,13 +294,13 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p it('should work with encoded server', async({page, server}) => { // The requestWillBeSent will report encoded URL, whereas interception will // report URL as-is. @see crbug.com/759388 - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); const response = await page.goto(server.PREFIX + '/some nonexisting page'); expect(response.status()).toBe(404); }); it('should work with badly encoded server', async({page, server}) => { server.setRoute('/malformed?rnd=%911', (req, res) => res.end()); - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); const response = await page.goto(server.PREFIX + '/malformed?rnd=%911'); expect(response.status()).toBe(200); }); @@ -307,9 +308,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p // The requestWillBeSent will report URL as-is, whereas interception will // report encoded URL for stylesheet. @see crbug.com/759388 const requests = []; - await page.route('**/*', request => { - request.continue(); - requests.push(request); + await page.route('**/*', route => { + route.continue(); + requests.push(route.request()); }); const response = await page.goto(`data:text/html,`); expect(response).toBe(null); @@ -318,75 +319,40 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should not throw "Invalid Interception Id" if the request was cancelled', async({page, server}) => { await page.setContent(''); - let request = null; - await page.route('**/*', async r => request = r); + let route = null; + await page.route('**/*', async r => route = r); page.$eval('iframe', (frame, url) => frame.src = url, server.EMPTY_PAGE), // Wait for request interception. await utils.waitEvent(page, 'request'); // Delete frame to cause request to be canceled. await page.$eval('iframe', frame => frame.remove()); let error = null; - await request.continue().catch(e => error = e); + await route.continue().catch(e => error = e); expect(error).toBe(null); }); - it('should throw if interception is not enabled', async({browser, server}) => { - let error = null; - const context = await browser.newContext(); - const page = await context.newPage(); - page.on('request', async request => { - try { - await request.continue(); - } catch (e) { - error = e; - } - }); - await page.goto(server.EMPTY_PAGE); - expect(error.message).toContain('Request Interception is not enabled'); - await context.close(); - }); it('should intercept main resource during cross-process navigation', async({page, server}) => { await page.goto(server.EMPTY_PAGE); let intercepted = false; - await page.route(server.CROSS_PROCESS_PREFIX + '/empty.html', request => { + await page.route(server.CROSS_PROCESS_PREFIX + '/empty.html', route => { intercepted = true; - request.continue(); + route.continue(); }); const response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); expect(response.ok()).toBe(true); expect(intercepted).toBe(true); }); - it('should not throw when continued after navigation', async({page, server}) => { - await page.route(server.PREFIX + '/one-style.css', () => {}); - // For some reason, Firefox issues load event with one outstanding request. - const firstNavigation = page.goto(server.PREFIX + '/one-style.html', { waitUntil: FFOX ? 'networkidle0' : 'load' }).catch(e => e); - const request = await page.waitForRequest(server.PREFIX + '/one-style.css'); - await page.goto(server.PREFIX + '/empty.html'); - await firstNavigation; - const notAnError = await request.continue().then(() => null).catch(e => e); - expect(notAnError).toBe(null); - }); - it('should not throw when continued after cross-process navigation', async({page, server}) => { - await page.route(server.PREFIX + '/one-style.css', () => {}); - // For some reason, Firefox issues load event with one outstanding request. - const firstNavigation = page.goto(server.PREFIX + '/one-style.html', { waitUntil: FFOX ? 'networkidle0' : 'load' }).catch(e => e); - const request = await page.waitForRequest(server.PREFIX + '/one-style.css'); - await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); - await firstNavigation; - const notAnError = await request.continue().then(() => null).catch(e => e); - expect(notAnError).toBe(null); - }); }); describe('Request.continue', function() { it('should work', async({page, server}) => { - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); await page.goto(server.EMPTY_PAGE); }); it('should amend HTTP headers', async({page, server}) => { - await page.route('**/*', request => { - const headers = Object.assign({}, request.headers()); + await page.route('**/*', route => { + const headers = Object.assign({}, route.request().headers()); headers['FOO'] = 'bar'; - request.continue({ headers }); + route.continue({ headers }); }); await page.goto(server.EMPTY_PAGE); const [request] = await Promise.all([ @@ -398,7 +364,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p it('should amend method', async({page, server}) => { const sRequest = server.waitForRequest('/sleep.zzz'); await page.goto(server.EMPTY_PAGE); - await page.route('**/*', request => request.continue({ method: 'POST' })); + await page.route('**/*', route => route.continue({ method: 'POST' })); const [request] = await Promise.all([ server.waitForRequest('/sleep.zzz'), page.evaluate(() => fetch('/sleep.zzz')) @@ -408,14 +374,14 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should amend method on main request', async({page, server}) => { const request = server.waitForRequest('/empty.html'); - await page.route('**/*', request => request.continue({ method: 'POST' })); + await page.route('**/*', route => route.continue({ method: 'POST' })); await page.goto(server.EMPTY_PAGE); expect((await request).method).toBe('POST'); }); it('should amend post data', async({page, server}) => { await page.goto(server.EMPTY_PAGE); - await page.route('**/*', request => { - request.continue({ postData: 'doggo' }); + await page.route('**/*', route => { + route.continue({ postData: 'doggo' }); }); const [serverRequest] = await Promise.all([ server.waitForRequest('/sleep.zzz'), @@ -427,8 +393,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('Request.fulfill', function() { it('should work', async({page, server}) => { - await page.route('**/*', request => { - request.fulfill({ + await page.route('**/*', route => { + route.fulfill({ status: 201, headers: { foo: 'bar' @@ -443,8 +409,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!'); }); it('should work with status code 422', async({page, server}) => { - await page.route('**/*', request => { - request.fulfill({ + await page.route('**/*', route => { + route.fulfill({ status: 422, body: 'Yo, page!' }); @@ -455,9 +421,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!'); }); it('should allow mocking binary responses', async({page, server}) => { - await page.route('**/*', request => { + await page.route('**/*', route => { const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png')); - request.fulfill({ + route.fulfill({ contentType: 'image/png', body: imageBuffer }); @@ -472,7 +438,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); }); it('should work with file path', async({page, server}) => { - await page.route('**/*', request => request.fulfill({ contentType: 'shouldBeIgnored', path: path.join(__dirname, 'assets', 'pptr.png') })); + await page.route('**/*', route => route.fulfill({ contentType: 'shouldBeIgnored', path: path.join(__dirname, 'assets', 'pptr.png') })); await page.evaluate(PREFIX => { const img = document.createElement('img'); img.src = PREFIX + '/does-not-exist.png'; @@ -483,8 +449,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); }); it('should stringify intercepted request response headers', async({page, server}) => { - await page.route('**/*', request => { - request.fulfill({ + await page.route('**/*', route => { + route.fulfill({ status: 200, headers: { 'foo': true @@ -503,9 +469,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('Interception vs isNavigationRequest', () => { it('should work with request interception', async({page, server}) => { const requests = new Map(); - await page.route('**/*', request => { - requests.set(request.url().split('/').pop(), request); - request.continue(); + await page.route('**/*', route => { + requests.set(route.request().url().split('/').pop(), route.request()); + route.continue(); }); server.setRedirect('/rrredirect', '/frames/one-frame.html'); await page.goto(server.PREFIX + '/rrredirect'); @@ -522,7 +488,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const context = await browser.newContext({ ignoreHTTPSErrors: true }); const page = await context.newPage(); - await page.route('**/*', request => request.continue()); + await page.route('**/*', route => route.continue()); const response = await page.goto(httpsServer.EMPTY_PAGE); expect(response.status()).toBe(200); await context.close(); @@ -538,10 +504,10 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const swResponse = await page.evaluate(() => fetchDummy('foo')); expect(swResponse).toBe('responseFromServiceWorker:foo'); - await page.route('**/foo', request => { - const slash = request.url().lastIndexOf('/'); - const name = request.url().substring(slash + 1); - request.fulfill({ + await page.route('**/foo', route => { + const slash = route.request().url().lastIndexOf('/'); + const name = route.request().url().substring(slash + 1); + route.fulfill({ status: 200, contentType: 'text/css', body: 'responseFromInterception:' + name @@ -581,8 +547,10 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p it('should work with regular expression passed from a different context', async({page, server}) => { const ctx = vm.createContext(); const regexp = vm.runInContext('new RegExp("empty\\.html")', ctx); + let intercepted = false; - await page.route(regexp, request => { + await page.route(regexp, (route, request) => { + expect(route.request()).toBe(request); expect(request.url()).toContain('empty.html'); expect(request.headers()['user-agent']).toBeTruthy(); expect(request.method()).toBe('GET'); @@ -591,11 +559,13 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(request.resourceType()).toBe('document'); expect(request.frame() === page.mainFrame()).toBe(true); expect(request.frame().url()).toBe('about:blank'); - request.continue(); + route.continue(); + intercepted = true; }); const response = await page.goto(server.EMPTY_PAGE); expect(response.ok()).toBe(true); + expect(intercepted).toBe(true); }); }); }; diff --git a/test/popup.spec.js b/test/popup.spec.js index b465809039..4b7ab162d8 100644 --- a/test/popup.spec.js +++ b/test/popup.spec.js @@ -44,8 +44,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE await page.goto(server.EMPTY_PAGE); await page.setContent('link'); let intercepted = false; - await context.route('**/empty.html', request => { - request.continue(); + await context.route('**/empty.html', route => { + route.continue(); intercepted = true; }); const [popup] = await Promise.all([ @@ -143,8 +143,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); let intercepted = false; - await context.route('**/empty.html', request => { - request.continue(); + await context.route('**/empty.html', route => { + route.continue(); intercepted = true; }); await page.evaluate(url => window.__popup = window.open(url), server.EMPTY_PAGE);