api(route): pass Route object instead of Request to route handlers (#1385)

References #1348.
This commit is contained in:
Dmitry Gozman 2020-03-13 14:30:40 -07:00 committed by GitHub
parent 26479119b6
commit 69be12ae12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 251 additions and 255 deletions

View File

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

View File

@ -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.
<!-- GEN:toc -->
- [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)
<!-- GEN:stop -->
#### 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]> 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.
<!-- GEN:toc -->
- [route.abort([errorCode])](#routeaborterrorcode)
- [route.continue([overrides])](#routecontinueoverrides)
- [route.fulfill(response)](#routefulfillresponse)
- [route.request()](#routerequest)
<!-- GEN:stop -->
#### 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"

View File

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

View File

@ -63,7 +63,7 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement
readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>();
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<Error>;
private _closePromiseFulfill: ((error: Error) => void) | undefined;

View File

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

View File

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

View File

@ -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<Response | null>;
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<platform.BufferType>;
export class Response {
@ -313,7 +326,7 @@ export type FulfillResponse = {
body?: string | platform.BufferType,
};
export interface RequestDelegate {
export interface RouteDelegate {
abort(errorCode: string): Promise<void>;
fulfill(response: FulfillResponse): Promise<void>;
continue(overrides: { method?: string; headers?: Headers; postData?: string; }): Promise<void>;

View File

@ -150,7 +150,7 @@ export class Page extends platform.EventEmitter {
private _workers = new Map<string, Worker>();
readonly pdf: ((options?: types.PDFOptions) => Promise<platform.BufferType>) | 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<platform.BufferType> {

View File

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

View File

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

View File

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

View File

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

View File

@ -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(`
<form action='/rredirect' method='post'>
<input type="hidden" id="foo" name="foo" value="FOOBAR">
@ -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,<div>yo</div>';
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,<div>yo</div>';
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,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`);
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('<iframe></iframe>');
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);
});
});
};

View File

@ -44,8 +44,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel=noopener href="empty.html">link</a>');
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);