mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 21:53:35 +03:00
feat(webkit): enable request interception (#343)
This commit is contained in:
parent
20f404cb42
commit
654fa22cc7
216
docs/api.md
216
docs/api.md
@ -173,6 +173,7 @@
|
||||
* [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout)
|
||||
* [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout)
|
||||
* [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders)
|
||||
* [page.setRequestInterception(enabled)](#pagesetrequestinterceptionenabled)
|
||||
* [page.setViewport(viewport)](#pagesetviewportviewport)
|
||||
* [page.title()](#pagetitle)
|
||||
* [page.tripleclick(selector[, options])](#pagetripleclickselector-options)
|
||||
@ -188,8 +189,11 @@
|
||||
* [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options)
|
||||
* [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
|
||||
- [class: Request](#class-request)
|
||||
* [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)
|
||||
@ -230,12 +234,7 @@
|
||||
* [chromiumCoverage.stopCSSCoverage()](#chromiumcoveragestopcsscoverage)
|
||||
* [chromiumCoverage.stopJSCoverage()](#chromiumcoveragestopjscoverage)
|
||||
- [class: ChromiumInterception](#class-chromiuminterception)
|
||||
* [chromiumInterception.abort(request, [errorCode])](#chromiuminterceptionabortrequest-errorcode)
|
||||
* [chromiumInterception.authenticate(credentials)](#chromiuminterceptionauthenticatecredentials)
|
||||
* [chromiumInterception.continue(request, [overrides])](#chromiuminterceptioncontinuerequest-overrides)
|
||||
* [chromiumInterception.disable()](#chromiuminterceptiondisable)
|
||||
* [chromiumInterception.enable()](#chromiuminterceptionenable)
|
||||
* [chromiumInterception.fulfill(request, response)](#chromiuminterceptionfulfillrequest-response)
|
||||
* [chromiumInterception.setOfflineMode(enabled)](#chromiuminterceptionsetofflinemodeenabled)
|
||||
- [class: ChromiumOverrides](#class-chromiumoverrides)
|
||||
* [chromiumOverrides.setGeolocation(options)](#chromiumoverridessetgeolocationoptions)
|
||||
@ -1799,7 +1798,7 @@ const [popup] = await Promise.all([
|
||||
- <[Request]>
|
||||
|
||||
Emitted when a page issues a request. The [request] object is read-only.
|
||||
In order to intercept and mutate requests, see `page.interception.enable()`.
|
||||
In order to intercept and mutate requests, see `page.setRequestInterception(true)`.
|
||||
|
||||
#### event: 'requestfailed'
|
||||
- <[Request]>
|
||||
@ -2403,6 +2402,31 @@ The extra HTTP headers will be sent with every request the page initiates.
|
||||
|
||||
> **NOTE** page.setExtraHTTPHeaders does not guarantee the order of headers in the outgoing requests.
|
||||
|
||||
#### page.setRequestInterception(enabled)
|
||||
- `enabled` <[boolean]> Whether to enable request interception.
|
||||
- returns: <[Promise]>
|
||||
|
||||
Activating request interception enables `request.abort`, `request.continue` and
|
||||
`request.respond` methods. This provides the capability to modify network requests that are made by a page.
|
||||
|
||||
Once request interception is enabled, every request will stall unless it's continued, responded or aborted.
|
||||
An example of a naïve request interceptor that aborts all image requests:
|
||||
|
||||
```js
|
||||
const page = await browser.newPage();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', interceptedRequest => {
|
||||
if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))
|
||||
interceptedRequest.abort();
|
||||
else
|
||||
interceptedRequest.continue();
|
||||
});
|
||||
await page.goto('https://example.com');
|
||||
await browser.close();
|
||||
```
|
||||
|
||||
> **NOTE** Enabling request interception disables page caching.
|
||||
|
||||
#### page.setViewport(viewport)
|
||||
- `viewport` <[Object]>
|
||||
- `width` <[number]> page width in pixels. **required**
|
||||
@ -2689,6 +2713,49 @@ 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])
|
||||
- `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.setRequestInterception`.
|
||||
Exception is immediately thrown if the request interception is not enabled.
|
||||
|
||||
#### request.continue([overrides])
|
||||
- `overrides` <[Object]> Optional request overwrites, which can be one of the following:
|
||||
- `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.setRequestInterception`.
|
||||
Exception is immediately thrown if the request interception is not enabled.
|
||||
|
||||
```js
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', 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'`.
|
||||
@ -2707,6 +2774,34 @@ page.on('requestfailed', request => {
|
||||
#### request.frame()
|
||||
- returns: <?[Frame]> A [Frame] that initiated this request, or `null` if navigating to error pages.
|
||||
|
||||
#### 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
|
||||
- returns: <[Promise]>
|
||||
|
||||
Fulfills request with given response. To use this, request interception should
|
||||
be enabled with `page.setRequestInterception`. Exception is thrown if
|
||||
request interception is not enabled.
|
||||
|
||||
An example of fulfilling all requests with 404 responses:
|
||||
|
||||
```js
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
request.respond({
|
||||
status: 404,
|
||||
contentType: 'text/plain',
|
||||
body: 'Not Found!'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
> **NOTE** Mocking responses for dataURL requests is not supported.
|
||||
> Calling `request.respond` for a dataURL request is a noop.
|
||||
|
||||
#### request.headers()
|
||||
- returns: <[Object]> An object with HTTP headers associated with the request. All header names are lower-case.
|
||||
|
||||
@ -3041,30 +3136,6 @@ reported.
|
||||
|
||||
### class: ChromiumInterception
|
||||
|
||||
#### chromiumInterception.abort(request, [errorCode])
|
||||
- `request` <[Request]>
|
||||
- `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.interception.enable()`.
|
||||
Exception is immediately thrown if the request interception is not enabled.
|
||||
|
||||
#### chromiumInterception.authenticate(credentials)
|
||||
- `credentials` <?[Object]>
|
||||
- `username` <[string]>
|
||||
@ -3075,91 +3146,6 @@ Provide credentials for [HTTP authentication](https://developer.mozilla.org/en-U
|
||||
|
||||
To disable authentication, pass `null`.
|
||||
|
||||
#### chromiumInterception.continue(request, [overrides])
|
||||
- `request` <[Request]>
|
||||
- `overrides` <[Object]> Optional request overwrites, which can be one of the following:
|
||||
- `url` <[string]> If set, the request url will be changed. This is not a redirect. The request will be silently forwarded to the new url. For example, the address bar will show the original url.
|
||||
- `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.interception.enable()`.
|
||||
Exception is immediately thrown if the request interception is not enabled.
|
||||
|
||||
```js
|
||||
await page.interception.enable();
|
||||
page.on('request', request => {
|
||||
// Override headers
|
||||
const headers = Object.assign({}, request.headers(), {
|
||||
foo: 'bar', // set "foo" header
|
||||
origin: undefined, // remove "origin" header
|
||||
});
|
||||
page.interception.continue(request, {headers});
|
||||
});
|
||||
```
|
||||
|
||||
#### chromiumInterception.disable()
|
||||
- returns: <[Promise]>
|
||||
|
||||
Disables network request interception.
|
||||
|
||||
#### chromiumInterception.enable()
|
||||
- returns: <[Promise]>
|
||||
|
||||
Once request interception is enabled, every request will stall unless it's continued, responded or aborted.
|
||||
An example of a naïve request interceptor that aborts all image requests:
|
||||
|
||||
```js
|
||||
const playwright = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const browser = await playwright.launch();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.interception.enable();
|
||||
page.on('request', interceptedRequest => {
|
||||
if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))
|
||||
page.interception.abort(interceptedRequest);
|
||||
else
|
||||
page.interception.continue(interceptedRequest);
|
||||
});
|
||||
await page.goto('https://example.com');
|
||||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
> **NOTE** Enabling request interception disables page caching.
|
||||
|
||||
#### chromiumInterception.fulfill(request, response)
|
||||
- `request` <[Request]>
|
||||
- `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
|
||||
- returns: <[Promise]>
|
||||
|
||||
Fulfills request with given response. To use this, request interception should
|
||||
be enabled with `page.interception.enable()`. Exception is thrown if
|
||||
request interception is not enabled.
|
||||
|
||||
An example of fulfilling all requests with 404 responses:
|
||||
|
||||
```js
|
||||
await page.interception.enable();
|
||||
page.on('request', request => {
|
||||
page.interception.respond(request, {
|
||||
status: 404,
|
||||
contentType: 'text/plain',
|
||||
body: 'Not Found!'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
> **NOTE** Mocking responses for dataURL requests is not supported.
|
||||
> Calling `request.respond` for a dataURL request is a noop.
|
||||
|
||||
#### chromiumInterception.setOfflineMode(enabled)
|
||||
- `enabled` <[boolean]> When `true`, enables offline mode for the page.
|
||||
- returns: <[Promise]>
|
||||
|
@ -10,7 +10,7 @@
|
||||
"playwright": {
|
||||
"chromium_revision": "724623",
|
||||
"firefox_revision": "1008",
|
||||
"webkit_revision": "1052"
|
||||
"webkit_revision": "1053"
|
||||
},
|
||||
"scripts": {
|
||||
"unit": "node test/test.js",
|
||||
|
@ -245,52 +245,27 @@ export class CRNetworkManager {
|
||||
}
|
||||
}
|
||||
|
||||
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||
|
||||
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
|
||||
return (request as any)[interceptableRequestSymbol];
|
||||
}
|
||||
|
||||
class InterceptableRequest {
|
||||
class InterceptableRequest implements network.RequestDelegate {
|
||||
readonly request: network.Request;
|
||||
_requestId: string;
|
||||
_interceptionId: string;
|
||||
_documentId: string;
|
||||
private _client: CRSession;
|
||||
private _allowInterception: boolean;
|
||||
private _interceptionHandled = false;
|
||||
|
||||
constructor(client: CRSession, frame: frames.Frame | null, interceptionId: string, documentId: string | undefined, allowInterception: boolean, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[]) {
|
||||
this._client = client;
|
||||
this._requestId = event.requestId;
|
||||
this._interceptionId = interceptionId;
|
||||
this._documentId = documentId;
|
||||
this._allowInterception = allowInterception;
|
||||
|
||||
this.request = new network.Request(frame, redirectChain, documentId,
|
||||
this.request = new network.Request(allowInterception ? this : null, frame, redirectChain, documentId,
|
||||
event.request.url, event.type.toLowerCase(), event.request.method, event.request.postData, headersObject(event.request.headers));
|
||||
(this.request as any)[interceptableRequestSymbol] = this;
|
||||
}
|
||||
|
||||
async continue(overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this.request.url().startsWith('data:'))
|
||||
return;
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
postData,
|
||||
headers
|
||||
} = overrides;
|
||||
this._interceptionHandled = true;
|
||||
async continue(overrides: { headers?: {[key: string]: string}; } = {}) {
|
||||
await this._client.send('Fetch.continueRequest', {
|
||||
requestId: this._interceptionId,
|
||||
url,
|
||||
method,
|
||||
postData,
|
||||
headers: headers ? headersArray(headers) : undefined,
|
||||
headers: overrides.headers ? headersArray(overrides.headers) : undefined,
|
||||
}).catch(error => {
|
||||
// In certain cases, protocol will return error if the request was already canceled
|
||||
// or the page was closed. We should tolerate these errors.
|
||||
@ -299,13 +274,6 @@ class InterceptableRequest {
|
||||
}
|
||||
|
||||
async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
||||
// Mocking responses for dataURL requests is not currently supported.
|
||||
if (this.request.url().startsWith('data:'))
|
||||
return;
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
|
||||
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(/** @type {string} */(response.body)) : /** @type {?Buffer} */(response.body || null);
|
||||
|
||||
const responseHeaders: { [s: string]: string; } = {};
|
||||
@ -321,7 +289,7 @@ class InterceptableRequest {
|
||||
await this._client.send('Fetch.fulfillRequest', {
|
||||
requestId: this._interceptionId,
|
||||
responseCode: response.status || 200,
|
||||
responsePhrase: STATUS_TEXTS[String(response.status || 200)],
|
||||
responsePhrase: network.STATUS_TEXTS[String(response.status || 200)],
|
||||
responseHeaders: headersArray(responseHeaders),
|
||||
body: responseBody ? responseBody.toString('base64') : undefined,
|
||||
}).catch(error => {
|
||||
@ -332,14 +300,8 @@ class InterceptableRequest {
|
||||
}
|
||||
|
||||
async abort(errorCode: string = 'failed') {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this.request.url().startsWith('data:'))
|
||||
return;
|
||||
const errorReason = errorReasons[errorCode];
|
||||
assert(errorReason, 'Unknown error code: ' + errorCode);
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
await this._client.send('Fetch.failRequest', {
|
||||
requestId: this._interceptionId,
|
||||
errorReason
|
||||
@ -384,69 +346,3 @@ function headersObject(headers: Protocol.Network.Headers): network.Headers {
|
||||
return result;
|
||||
}
|
||||
|
||||
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
|
||||
const STATUS_TEXTS: { [status: string]: string } = {
|
||||
'100': 'Continue',
|
||||
'101': 'Switching Protocols',
|
||||
'102': 'Processing',
|
||||
'103': 'Early Hints',
|
||||
'200': 'OK',
|
||||
'201': 'Created',
|
||||
'202': 'Accepted',
|
||||
'203': 'Non-Authoritative Information',
|
||||
'204': 'No Content',
|
||||
'205': 'Reset Content',
|
||||
'206': 'Partial Content',
|
||||
'207': 'Multi-Status',
|
||||
'208': 'Already Reported',
|
||||
'226': 'IM Used',
|
||||
'300': 'Multiple Choices',
|
||||
'301': 'Moved Permanently',
|
||||
'302': 'Found',
|
||||
'303': 'See Other',
|
||||
'304': 'Not Modified',
|
||||
'305': 'Use Proxy',
|
||||
'306': 'Switch Proxy',
|
||||
'307': 'Temporary Redirect',
|
||||
'308': 'Permanent Redirect',
|
||||
'400': 'Bad Request',
|
||||
'401': 'Unauthorized',
|
||||
'402': 'Payment Required',
|
||||
'403': 'Forbidden',
|
||||
'404': 'Not Found',
|
||||
'405': 'Method Not Allowed',
|
||||
'406': 'Not Acceptable',
|
||||
'407': 'Proxy Authentication Required',
|
||||
'408': 'Request Timeout',
|
||||
'409': 'Conflict',
|
||||
'410': 'Gone',
|
||||
'411': 'Length Required',
|
||||
'412': 'Precondition Failed',
|
||||
'413': 'Payload Too Large',
|
||||
'414': 'URI Too Long',
|
||||
'415': 'Unsupported Media Type',
|
||||
'416': 'Range Not Satisfiable',
|
||||
'417': 'Expectation Failed',
|
||||
'418': 'I\'m a teapot',
|
||||
'421': 'Misdirected Request',
|
||||
'422': 'Unprocessable Entity',
|
||||
'423': 'Locked',
|
||||
'424': 'Failed Dependency',
|
||||
'425': 'Too Early',
|
||||
'426': 'Upgrade Required',
|
||||
'428': 'Precondition Required',
|
||||
'429': 'Too Many Requests',
|
||||
'431': 'Request Header Fields Too Large',
|
||||
'451': 'Unavailable For Legal Reasons',
|
||||
'500': 'Internal Server Error',
|
||||
'501': 'Not Implemented',
|
||||
'502': 'Bad Gateway',
|
||||
'503': 'Service Unavailable',
|
||||
'504': 'Gateway Timeout',
|
||||
'505': 'HTTP Version Not Supported',
|
||||
'506': 'Variant Also Negotiates',
|
||||
'507': 'Insufficient Storage',
|
||||
'508': 'Loop Detected',
|
||||
'510': 'Not Extended',
|
||||
'511': 'Network Authentication Required',
|
||||
};
|
||||
|
@ -298,6 +298,10 @@ export class CRPage implements PageDelegate {
|
||||
return this._networkManager.setCacheEnabled(enabled);
|
||||
}
|
||||
|
||||
async setRequestInterception(enabled: boolean): Promise<void> {
|
||||
await this._networkManager.setRequestInterception(enabled);
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
await this._client.send('Page.reload');
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { CRNetworkManager, toInterceptableRequest } from '../crNetworkManager';
|
||||
import * as network from '../../network';
|
||||
import { CRNetworkManager } from '../crNetworkManager';
|
||||
|
||||
export class CRInterception {
|
||||
private _networkManager: CRNetworkManager;
|
||||
@ -11,26 +10,6 @@ export class CRInterception {
|
||||
this._networkManager = networkManager;
|
||||
}
|
||||
|
||||
async enable() {
|
||||
await this._networkManager.setRequestInterception(true);
|
||||
}
|
||||
|
||||
async disable() {
|
||||
await this._networkManager.setRequestInterception(false);
|
||||
}
|
||||
|
||||
async continue(request: network.Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
||||
return toInterceptableRequest(request).continue(overrides);
|
||||
}
|
||||
|
||||
async fulfill(request: network.Request, response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
||||
return toInterceptableRequest(request).fulfill(response);
|
||||
}
|
||||
|
||||
async abort(request: network.Request, errorCode: string = 'failed') {
|
||||
return toInterceptableRequest(request).abort(errorCode);
|
||||
}
|
||||
|
||||
setOfflineMode(enabled: boolean) {
|
||||
return this._networkManager.setOfflineMode(enabled);
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
import { BrowserContext } from '../../browserContext';
|
||||
import { CRPage } from '../crPage';
|
||||
import { Page } from '../../page';
|
||||
|
||||
export class CROverrides {
|
||||
private _context: BrowserContext;
|
||||
|
@ -1,33 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { FFNetworkManager, toInterceptableRequest } from '../ffNetworkManager';
|
||||
import * as network from '../../network';
|
||||
|
||||
export class FFInterception {
|
||||
private _networkManager: FFNetworkManager;
|
||||
|
||||
constructor(networkManager: FFNetworkManager) {
|
||||
this._networkManager = networkManager;
|
||||
}
|
||||
|
||||
async enable() {
|
||||
await this._networkManager.setRequestInterception(true);
|
||||
}
|
||||
|
||||
async disable() {
|
||||
await this._networkManager.setRequestInterception(false);
|
||||
}
|
||||
|
||||
async continue(request: network.Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
||||
return toInterceptableRequest(request).continue(overrides);
|
||||
}
|
||||
|
||||
async fulfill(request: network.Request, response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async abort(request: network.Request, errorCode: string = 'failed') {
|
||||
return toInterceptableRequest(request).abort();
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
export { FFInterception as FirefoxInterception } from './features/ffInterception';
|
||||
export { FFBrowser as FirefoxBrowser } from './ffBrowser';
|
||||
export { FFPlaywright as FirefoxPlaywright } from './ffPlaywright';
|
||||
|
@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||
import { debugError, helper, RegisteredListener } from '../helper';
|
||||
import { FFSession } from './ffConnection';
|
||||
import { Page } from '../page';
|
||||
import * as network from '../network';
|
||||
@ -139,41 +139,24 @@ const causeToResourceType = {
|
||||
TYPE_WEB_MANIFEST: 'manifest',
|
||||
};
|
||||
|
||||
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||
|
||||
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
|
||||
return (request as any)[interceptableRequestSymbol];
|
||||
}
|
||||
|
||||
class InterceptableRequest {
|
||||
class InterceptableRequest implements network.RequestDelegate {
|
||||
readonly request: network.Request;
|
||||
_id: string;
|
||||
private _session: FFSession;
|
||||
private _suspended: boolean;
|
||||
private _interceptionHandled: boolean;
|
||||
|
||||
constructor(session: FFSession, frame: frames.Frame, redirectChain: network.Request[], payload: any) {
|
||||
this._id = payload.requestId;
|
||||
this._session = session;
|
||||
this._suspended = payload.suspended;
|
||||
this._interceptionHandled = false;
|
||||
|
||||
const headers: network.Headers = {};
|
||||
for (const {name, value} of payload.headers)
|
||||
headers[name.toLowerCase()] = value;
|
||||
|
||||
this.request = new network.Request(frame, redirectChain, payload.navigationId,
|
||||
this.request = new network.Request(payload.suspended ? this : null, frame, redirectChain, payload.navigationId,
|
||||
payload.url, causeToResourceType[payload.cause] || 'other', payload.method, payload.postData, headers);
|
||||
(this.request as any)[interceptableRequestSymbol] = this;
|
||||
}
|
||||
|
||||
async continue(overrides: {url?: string, method?: string, postData?: string, headers?: {[key: string]: string}} = {}) {
|
||||
assert(!overrides.url, 'Playwright-Firefox does not support overriding URL');
|
||||
assert(!overrides.method, 'Playwright-Firefox does not support overriding method');
|
||||
assert(!overrides.postData, 'Playwright-Firefox does not support overriding postData');
|
||||
assert(this._suspended, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
async continue(overrides: { headers?: { [key: string]: string } } = {}) {
|
||||
const {
|
||||
headers,
|
||||
} = overrides;
|
||||
@ -185,10 +168,11 @@ class InterceptableRequest {
|
||||
});
|
||||
}
|
||||
|
||||
async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
||||
throw new Error('Fulfill is not supported in Firefox');
|
||||
}
|
||||
|
||||
async abort() {
|
||||
assert(this._suspended, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
await this._session.send('Network.abortSuspendedRequest', {
|
||||
requestId: this._id,
|
||||
}).catch(error => {
|
||||
|
@ -28,7 +28,6 @@ import { Protocol } from './protocol';
|
||||
import * as input from '../input';
|
||||
import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { FFInterception } from './features/ffInterception';
|
||||
import { FFAccessibility } from './features/ffAccessibility';
|
||||
import * as network from '../network';
|
||||
import * as types from '../types';
|
||||
@ -38,7 +37,7 @@ export class FFPage implements PageDelegate {
|
||||
readonly rawKeyboard: RawKeyboardImpl;
|
||||
readonly _session: FFSession;
|
||||
readonly _page: Page;
|
||||
private readonly _networkManager: FFNetworkManager;
|
||||
readonly _networkManager: FFNetworkManager;
|
||||
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
|
||||
@ -65,7 +64,6 @@ export class FFPage implements PageDelegate {
|
||||
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
|
||||
];
|
||||
(this._page as any).interception = new FFInterception(this._networkManager);
|
||||
(this._page as any).accessibility = new FFAccessibility(session);
|
||||
}
|
||||
|
||||
@ -214,6 +212,10 @@ export class FFPage implements PageDelegate {
|
||||
await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled});
|
||||
}
|
||||
|
||||
async setRequestInterception(enabled: boolean): Promise<void> {
|
||||
await this._networkManager.setRequestInterception(enabled);
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
await this._session.send('Page.reload', { frameId: this._page.mainFrame()._id });
|
||||
}
|
||||
|
108
src/network.ts
108
src/network.ts
@ -78,6 +78,7 @@ function stripFragmentFromUrl(url: string): string {
|
||||
export type Headers = { [key: string]: string };
|
||||
|
||||
export class Request {
|
||||
private _delegate: RequestDelegate | null;
|
||||
private _response: Response | null = null;
|
||||
_redirectChain: Request[];
|
||||
_finalRequest: Request;
|
||||
@ -93,9 +94,11 @@ export class Request {
|
||||
private _waitForResponsePromiseCallback: (value?: Response) => void;
|
||||
private _waitForFinishedPromise: Promise<Response | undefined>;
|
||||
private _waitForFinishedPromiseCallback: (value?: Response | undefined) => void;
|
||||
private _interceptionHandled = false;
|
||||
|
||||
constructor(frame: frames.Frame | null, redirectChain: Request[], documentId: string,
|
||||
constructor(delegate: RequestDelegate | null, frame: frames.Frame | null, redirectChain: Request[], documentId: string,
|
||||
url: string, resourceType: string, method: string, postData: string, headers: Headers) {
|
||||
this._delegate = delegate;
|
||||
this._frame = frame;
|
||||
this._redirectChain = redirectChain;
|
||||
this._finalRequest = this;
|
||||
@ -169,12 +172,40 @@ export class Request {
|
||||
}
|
||||
|
||||
failure(): { errorText: string; } | null {
|
||||
if (!this._failureText)
|
||||
if (this._failureText === null)
|
||||
return null;
|
||||
return {
|
||||
errorText: this._failureText
|
||||
};
|
||||
}
|
||||
|
||||
async abort(errorCode: string = 'failed') {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this.url().startsWith('data:'))
|
||||
return;
|
||||
assert(this._delegate, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
await this._delegate.abort(errorCode);
|
||||
}
|
||||
|
||||
async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) { // Mocking responses for dataURL requests is not currently supported.
|
||||
if (this.url().startsWith('data:'))
|
||||
return;
|
||||
assert(this._delegate, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
await this._delegate.fulfill(response);
|
||||
}
|
||||
|
||||
async continue(overrides: { headers?: { [key: string]: string } } = {}) {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this.url().startsWith('data:'))
|
||||
return;
|
||||
assert(this._delegate, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
await this._delegate.continue(overrides);
|
||||
}
|
||||
}
|
||||
|
||||
export type RemoteAddress = {
|
||||
@ -267,3 +298,76 @@ export class Response {
|
||||
return this._request.frame();
|
||||
}
|
||||
}
|
||||
|
||||
export interface RequestDelegate {
|
||||
abort(errorCode: string): Promise<void>;
|
||||
fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }): Promise<void>;
|
||||
continue(overrides: { url?: string; method?: string; postData?: string; headers?: { [key: string]: string; }; }): Promise<void>;
|
||||
}
|
||||
|
||||
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
|
||||
export const STATUS_TEXTS: { [status: string]: string } = {
|
||||
'100': 'Continue',
|
||||
'101': 'Switching Protocols',
|
||||
'102': 'Processing',
|
||||
'103': 'Early Hints',
|
||||
'200': 'OK',
|
||||
'201': 'Created',
|
||||
'202': 'Accepted',
|
||||
'203': 'Non-Authoritative Information',
|
||||
'204': 'No Content',
|
||||
'205': 'Reset Content',
|
||||
'206': 'Partial Content',
|
||||
'207': 'Multi-Status',
|
||||
'208': 'Already Reported',
|
||||
'226': 'IM Used',
|
||||
'300': 'Multiple Choices',
|
||||
'301': 'Moved Permanently',
|
||||
'302': 'Found',
|
||||
'303': 'See Other',
|
||||
'304': 'Not Modified',
|
||||
'305': 'Use Proxy',
|
||||
'306': 'Switch Proxy',
|
||||
'307': 'Temporary Redirect',
|
||||
'308': 'Permanent Redirect',
|
||||
'400': 'Bad Request',
|
||||
'401': 'Unauthorized',
|
||||
'402': 'Payment Required',
|
||||
'403': 'Forbidden',
|
||||
'404': 'Not Found',
|
||||
'405': 'Method Not Allowed',
|
||||
'406': 'Not Acceptable',
|
||||
'407': 'Proxy Authentication Required',
|
||||
'408': 'Request Timeout',
|
||||
'409': 'Conflict',
|
||||
'410': 'Gone',
|
||||
'411': 'Length Required',
|
||||
'412': 'Precondition Failed',
|
||||
'413': 'Payload Too Large',
|
||||
'414': 'URI Too Long',
|
||||
'415': 'Unsupported Media Type',
|
||||
'416': 'Range Not Satisfiable',
|
||||
'417': 'Expectation Failed',
|
||||
'418': 'I\'m a teapot',
|
||||
'421': 'Misdirected Request',
|
||||
'422': 'Unprocessable Entity',
|
||||
'423': 'Locked',
|
||||
'424': 'Failed Dependency',
|
||||
'425': 'Too Early',
|
||||
'426': 'Upgrade Required',
|
||||
'428': 'Precondition Required',
|
||||
'429': 'Too Many Requests',
|
||||
'431': 'Request Header Fields Too Large',
|
||||
'451': 'Unavailable For Legal Reasons',
|
||||
'500': 'Internal Server Error',
|
||||
'501': 'Not Implemented',
|
||||
'502': 'Bad Gateway',
|
||||
'503': 'Service Unavailable',
|
||||
'504': 'Gateway Timeout',
|
||||
'505': 'HTTP Version Not Supported',
|
||||
'506': 'Variant Also Negotiates',
|
||||
'507': 'Insufficient Storage',
|
||||
'508': 'Loop Detected',
|
||||
'510': 'Not Extended',
|
||||
'511': 'Network Authentication Required',
|
||||
};
|
||||
|
10
src/page.ts
10
src/page.ts
@ -48,6 +48,7 @@ export interface PageDelegate {
|
||||
setViewport(viewport: types.Viewport): Promise<void>;
|
||||
setEmulateMedia(mediaType: input.MediaType | null, colorScheme: input.ColorScheme | null): Promise<void>;
|
||||
setCacheEnabled(enabled: boolean): Promise<void>;
|
||||
setRequestInterception(enabled: boolean): Promise<void>;
|
||||
|
||||
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null>;
|
||||
canScreenshotOutsideViewport(): boolean;
|
||||
@ -71,6 +72,7 @@ type PageState = {
|
||||
colorScheme: input.ColorScheme | null;
|
||||
extraHTTPHeaders: network.Headers | null;
|
||||
cacheEnabled: boolean | null;
|
||||
interceptNetwork: boolean | null;
|
||||
};
|
||||
|
||||
export type FileChooser = {
|
||||
@ -107,6 +109,7 @@ export class Page extends EventEmitter {
|
||||
colorScheme: browserContext._options.colorScheme || null,
|
||||
extraHTTPHeaders: null,
|
||||
cacheEnabled: null,
|
||||
interceptNetwork: null
|
||||
};
|
||||
this.keyboard = new input.Keyboard(delegate.rawKeyboard);
|
||||
this.mouse = new input.Mouse(delegate.rawMouse, this.keyboard);
|
||||
@ -391,6 +394,13 @@ export class Page extends EventEmitter {
|
||||
await this._delegate.setCacheEnabled(enabled);
|
||||
}
|
||||
|
||||
async setRequestInterception(enabled: boolean) {
|
||||
if (this._state.interceptNetwork === enabled)
|
||||
return;
|
||||
this._state.interceptNetwork = enabled;
|
||||
await this._delegate.setRequestInterception(enabled);
|
||||
}
|
||||
|
||||
async screenshot(options?: types.ScreenshotOptions): Promise<Buffer> {
|
||||
return this._screenshotter.screenshotPage(options);
|
||||
}
|
||||
|
@ -17,16 +17,15 @@
|
||||
|
||||
import { WKTargetSession } from './wkConnection';
|
||||
import { Page } from '../page';
|
||||
import { helper, RegisteredListener } from '../helper';
|
||||
import { helper, RegisteredListener, assert } from '../helper';
|
||||
import { Protocol } from './protocol';
|
||||
import * as network from '../network';
|
||||
import * as frames from '../frames';
|
||||
|
||||
export class WKNetworkManager {
|
||||
private _session: WKTargetSession;
|
||||
private _page: Page;
|
||||
_page: Page;
|
||||
private _requestIdToRequest = new Map<string, InterceptableRequest>();
|
||||
private _attemptedAuthentications = new Set<string>();
|
||||
private _userCacheDisabled = false;
|
||||
private _sessionListeners: RegisteredListener[] = [];
|
||||
|
||||
@ -39,14 +38,19 @@ export class WKNetworkManager {
|
||||
this._session = session;
|
||||
this._sessionListeners = [
|
||||
helper.addEventListener(this._session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)),
|
||||
helper.addEventListener(this._session, 'Network.requestIntercepted', this._onRequestIntercepted.bind(this)),
|
||||
helper.addEventListener(this._session, 'Network.responseReceived', this._onResponseReceived.bind(this)),
|
||||
helper.addEventListener(this._session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
|
||||
helper.addEventListener(this._session, 'Network.loadingFailed', this._onLoadingFailed.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
async initializeSession(session: WKTargetSession) {
|
||||
await session.send('Network.enable');
|
||||
async initializeSession(session: WKTargetSession, enableInterception: boolean) {
|
||||
const promises = [];
|
||||
promises.push(session.send('Network.enable'));
|
||||
if (enableInterception)
|
||||
promises.push(session.send('Network.setInterceptionEnabled', { enabled: true }));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@ -58,6 +62,10 @@ export class WKNetworkManager {
|
||||
await this._updateProtocolCacheDisabled();
|
||||
}
|
||||
|
||||
async setRequestInterception(enabled: boolean): Promise<void> {
|
||||
await this._session.send('Network.setInterceptionEnabled', { enabled });
|
||||
}
|
||||
|
||||
async _updateProtocolCacheDisabled() {
|
||||
await this._session.send('Network.setResourceCachingDisabled', {
|
||||
disabled: this._userCacheDisabled
|
||||
@ -78,11 +86,15 @@ export class WKNetworkManager {
|
||||
// TODO(einbinder) this will fail if we are an XHR document request
|
||||
const isNavigationRequest = event.type === 'Document';
|
||||
const documentId = isNavigationRequest ? this._session._sessionId + '::' + event.loaderId : undefined;
|
||||
const request = new InterceptableRequest(frame, undefined, event, redirectChain, documentId);
|
||||
const request = new InterceptableRequest(this._session, this._page._state.interceptNetwork, frame, event, redirectChain, documentId);
|
||||
this._requestIdToRequest.set(event.requestId, request);
|
||||
this._page._frameManager.requestStarted(request.request);
|
||||
}
|
||||
|
||||
_onRequestIntercepted(event: Protocol.Network.requestInterceptedPayload) {
|
||||
this._requestIdToRequest.get(event.requestId)._interceptedCallback();
|
||||
}
|
||||
|
||||
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response {
|
||||
const remoteAddress: network.RemoteAddress = { ip: '', port: 0 };
|
||||
const getResponseBody = async () => {
|
||||
@ -97,7 +109,6 @@ export class WKNetworkManager {
|
||||
request.request._redirectChain.push(request.request);
|
||||
response._requestFinished(new Error('Response body is unavailable for redirect responses'));
|
||||
this._requestIdToRequest.delete(request._requestId);
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
this._page._frameManager.requestReceivedResponse(response);
|
||||
this._page._frameManager.requestFinished(request.request);
|
||||
}
|
||||
@ -123,7 +134,6 @@ export class WKNetworkManager {
|
||||
if (request.request.response())
|
||||
request.request.response()._requestFinished();
|
||||
this._requestIdToRequest.delete(request._requestId);
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
this._page._frameManager.requestFinished(request.request);
|
||||
}
|
||||
|
||||
@ -137,31 +147,93 @@ export class WKNetworkManager {
|
||||
if (response)
|
||||
response._requestFinished();
|
||||
this._requestIdToRequest.delete(request._requestId);
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
request.request._setFailureText(event.errorText);
|
||||
this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled'));
|
||||
}
|
||||
|
||||
authenticate(credentials: { username: string; password: string; }) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
setOfflineMode(enabled: boolean) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||
const errorReasons: { [reason: string]: string } = {
|
||||
'aborted': 'Cancellation',
|
||||
'accessdenied': 'AccessControl',
|
||||
'addressunreachable': 'General',
|
||||
'blockedbyclient': 'Cancellation',
|
||||
'blockedbyresponse': 'General',
|
||||
'connectionaborted': 'General',
|
||||
'connectionclosed': 'General',
|
||||
'connectionfailed': 'General',
|
||||
'connectionrefused': 'General',
|
||||
'connectionreset': 'General',
|
||||
'internetdisconnected': 'General',
|
||||
'namenotresolved': 'General',
|
||||
'timedout': 'Timeout',
|
||||
'failed': 'General',
|
||||
};
|
||||
|
||||
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
|
||||
return (request as any)[interceptableRequestSymbol];
|
||||
}
|
||||
|
||||
class InterceptableRequest {
|
||||
class InterceptableRequest implements network.RequestDelegate {
|
||||
private _session: WKTargetSession;
|
||||
readonly request: network.Request;
|
||||
_requestId: string;
|
||||
_interceptionId: string;
|
||||
_documentId: string | undefined;
|
||||
_interceptedCallback: () => void;
|
||||
private _interceptedPromise: Promise<unknown>;
|
||||
|
||||
constructor(frame: frames.Frame | null, interceptionId: string, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[], documentId: string | undefined) {
|
||||
constructor(session: WKTargetSession, allowInterception: boolean, frame: frames.Frame | null, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[], documentId: string | undefined) {
|
||||
this._session = session;
|
||||
this._requestId = event.requestId;
|
||||
this._interceptionId = interceptionId;
|
||||
this._documentId = documentId;
|
||||
this.request = new network.Request(frame, redirectChain, documentId, event.request.url,
|
||||
this.request = new network.Request(allowInterception ? this : null, frame, redirectChain, documentId, event.request.url,
|
||||
event.type ? event.type.toLowerCase() : 'Unknown', event.request.method, event.request.postData, headersObject(event.request.headers));
|
||||
(this.request as any)[interceptableRequestSymbol] = this;
|
||||
this._interceptedPromise = new Promise(f => this._interceptedCallback = f);
|
||||
}
|
||||
|
||||
async abort(errorCode: string) {
|
||||
const reason = errorReasons[errorCode];
|
||||
assert(reason, 'Unknown error code: ' + errorCode);
|
||||
await this._interceptedPromise;
|
||||
await this._session.send('Network.interceptAsError', { requestId: this._requestId, reason });
|
||||
}
|
||||
|
||||
async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
||||
await this._interceptedPromise;
|
||||
|
||||
const base64Encoded = !!response.body && !helper.isString(response.body);
|
||||
const responseBody = response.body ? (base64Encoded ? response.body.toString('base64') : response.body as string) : undefined;
|
||||
|
||||
const responseHeaders: { [s: string]: string; } = {};
|
||||
if (response.headers) {
|
||||
for (const header of Object.keys(response.headers))
|
||||
responseHeaders[header.toLowerCase()] = String(response.headers[header]);
|
||||
}
|
||||
if (response.contentType)
|
||||
responseHeaders['content-type'] = response.contentType;
|
||||
if (responseBody && !('content-length' in responseHeaders))
|
||||
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
|
||||
|
||||
await this._session.send('Network.interceptWithResponse', {
|
||||
requestId: this._requestId,
|
||||
status: response.status || 200,
|
||||
statusText: network.STATUS_TEXTS[String(response.status || 200)],
|
||||
mimeType: response.contentType || (base64Encoded ? 'application/octet-stream' : 'text/plain'),
|
||||
headers: responseHeaders,
|
||||
base64Encoded,
|
||||
content: responseBody
|
||||
});
|
||||
}
|
||||
|
||||
async continue(overrides: { headers?: { [key: string]: string; }; }) {
|
||||
await this._interceptedPromise;
|
||||
await this._session.send('Network.interceptContinue', {
|
||||
requestId: this._requestId,
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ export class WKPage implements PageDelegate {
|
||||
session.send('Runtime.enable').then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
|
||||
session.send('Console.enable'),
|
||||
session.send('Page.setInterceptFileChooserDialog', { enabled: true }),
|
||||
this._networkManager.initializeSession(session),
|
||||
this._networkManager.initializeSession(session, this._page._state.interceptNetwork),
|
||||
];
|
||||
if (!session.isProvisional()) {
|
||||
// FIXME: move dialog agent to web process.
|
||||
@ -305,6 +305,10 @@ export class WKPage implements PageDelegate {
|
||||
return this._networkManager.setCacheEnabled(enabled);
|
||||
}
|
||||
|
||||
setRequestInterception(enabled: boolean): Promise<void> {
|
||||
return this._networkManager.setRequestInterception(enabled);
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
await this._session.send('Page.reload');
|
||||
}
|
||||
|
@ -238,8 +238,8 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROME
|
||||
res.end('console.log(1);');
|
||||
});
|
||||
|
||||
await page.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.continue());
|
||||
await page.goto(server.PREFIX + '/intervention');
|
||||
// Check for feature URL substring rather than https://www.chromestatus.com to
|
||||
// make it work with Edgium.
|
||||
|
@ -44,8 +44,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
expect(page.frames().length).toBe(2);
|
||||
});
|
||||
it('should load oopif iframes with subresources and request interception', async function({browser, page, server, context}) {
|
||||
await page.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.continue());
|
||||
await page.goto(server.PREFIX + '/dynamic-oopif.html');
|
||||
expect(oopifs(browser).length).toBe(1);
|
||||
});
|
||||
|
BIN
test/golden-webkit/mock-binary-response.png
Normal file
BIN
test/golden-webkit/mock-binary-response.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
@ -17,19 +17,19 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('../utils');
|
||||
const utils = require('./utils');
|
||||
|
||||
module.exports.describe = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) {
|
||||
const {describe, xdescribe, fdescribe} = testRunner;
|
||||
const {it, fit, xit, dit} = testRunner;
|
||||
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
||||
|
||||
describe('Interception.enable', function() {
|
||||
describe('Page.setRequestInterception', function() {
|
||||
it('should intercept', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
if (utils.isFavicon(request)) {
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
return;
|
||||
}
|
||||
expect(request.url()).toContain('empty.html');
|
||||
@ -40,17 +40,16 @@ 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');
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.remoteAddress().port).toBe(server.PORT);
|
||||
});
|
||||
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.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.continue());
|
||||
await page.setContent(`
|
||||
<form action='/rredirect' method='post'>
|
||||
<input type="hidden" id="foo" name="foo" value="FOOBAR">
|
||||
@ -64,24 +63,24 @@ 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.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
const headers = Object.assign({}, request.headers(), {
|
||||
foo: 'bar'
|
||||
});
|
||||
page.interception.continue(request, { headers });
|
||||
request.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.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
const headers = Object.assign({}, request.headers(), {
|
||||
foo: 'bar',
|
||||
origin: undefined, // remove "origin" header
|
||||
});
|
||||
page.interception.continue(request, { headers });
|
||||
request.continue({ headers });
|
||||
});
|
||||
|
||||
const [serverRequest] = await Promise.all([
|
||||
@ -92,12 +91,12 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
expect(serverRequest.headers.origin).toBe(undefined);
|
||||
});
|
||||
it('should contain referer header', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
const requests = [];
|
||||
page.on('request', request => {
|
||||
if (!utils.isFavicon(request))
|
||||
requests.push(request);
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(requests[1].url()).toContain('/one-style.css');
|
||||
@ -109,26 +108,26 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
await context.setCookies([{ url: server.EMPTY_PAGE, name: 'foo', value: 'bar'}]);
|
||||
|
||||
// Setup request interception.
|
||||
await page.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.continue());
|
||||
const response = await page.reload();
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
it('should stop intercepting', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
page.once('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.once('request', request => request.continue());
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.interception.disable();
|
||||
await page.setRequestInterception(false);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
});
|
||||
it('should show custom HTTP headers', async({page, server}) => {
|
||||
await page.setExtraHTTPHeaders({
|
||||
foo: 'bar'
|
||||
});
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
expect(request.headers()['foo']).toBe('bar');
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(response.ok()).toBe(true);
|
||||
@ -137,8 +136,8 @@ 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.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.continue());
|
||||
const status = await page.evaluate(async() => {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('GET', '/logo.png', false); // `false` makes the request synchronous
|
||||
@ -149,21 +148,21 @@ 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.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(response.ok()).toBe(true);
|
||||
});
|
||||
it('should be abortable', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
if (request.url().endsWith('.css'))
|
||||
page.interception.abort(request);
|
||||
request.abort();
|
||||
else
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
let failedRequests = 0;
|
||||
page.on('requestfailed', event => ++failedRequests);
|
||||
@ -172,23 +171,28 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
expect(response.request().failure()).toBe(null);
|
||||
expect(failedRequests).toBe(1);
|
||||
});
|
||||
it.skip(FFOX)('should be abortable with custom error codes', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
it('should be abortable with custom error codes', async({page, server}) => {
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
page.interception.abort(request, 'internetdisconnected');
|
||||
request.abort('internetdisconnected');
|
||||
});
|
||||
let failedRequest = null;
|
||||
page.on('requestfailed', request => failedRequest = request);
|
||||
await page.goto(server.EMPTY_PAGE).catch(e => {});
|
||||
expect(failedRequest).toBeTruthy();
|
||||
expect(failedRequest.failure().errorText).toBe('net::ERR_INTERNET_DISCONNECTED');
|
||||
if (WEBKIT)
|
||||
expect(failedRequest.failure().errorText).toBe('Request intercepted');
|
||||
else if (FFOX)
|
||||
expect(failedRequest.failure().errorText).toBe('NS_ERROR_FAILURE');
|
||||
else
|
||||
expect(failedRequest.failure().errorText).toBe('net::ERR_INTERNET_DISCONNECTED');
|
||||
});
|
||||
it('should send referer', async({page, server}) => {
|
||||
await page.setExtraHTTPHeaders({
|
||||
referer: 'http://google.com/'
|
||||
});
|
||||
await page.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.continue());
|
||||
const [request] = await Promise.all([
|
||||
server.waitForRequest('/grid.html'),
|
||||
page.goto(server.PREFIX + '/grid.html'),
|
||||
@ -196,21 +200,23 @@ 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.interception.enable();
|
||||
page.on('request', request => page.interception.abort(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.abort());
|
||||
let error = null;
|
||||
await page.goto(server.EMPTY_PAGE).catch(e => error = e);
|
||||
expect(error).toBeTruthy();
|
||||
if (CHROME || WEBKIT)
|
||||
expect(error.message).toContain('net::ERR_FAILED');
|
||||
else
|
||||
if (WEBKIT)
|
||||
expect(error.message).toContain('Request intercepted');
|
||||
else if (FFOX)
|
||||
expect(error.message).toContain('NS_ERROR_FAILURE');
|
||||
else
|
||||
expect(error.message).toContain('net::ERR_FAILED');
|
||||
});
|
||||
it('should work with redirects', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
const requests = [];
|
||||
page.on('request', request => {
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
if (!utils.isFavicon(request))
|
||||
requests.push(request);
|
||||
});
|
||||
@ -235,10 +241,10 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
}
|
||||
});
|
||||
it('should work with redirects for subresources', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
const requests = [];
|
||||
page.on('request', request => {
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
if (!utils.isFavicon(request))
|
||||
requests.push(request);
|
||||
});
|
||||
@ -259,43 +265,20 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
expect(redirectChain[0].url()).toContain('/one-style.css');
|
||||
expect(redirectChain[2].url()).toContain('/three-style.css');
|
||||
});
|
||||
it('should be able to abort redirects', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
server.setRedirect('/non-existing.json', '/non-existing-2.json');
|
||||
server.setRedirect('/non-existing-2.json', '/simple.html');
|
||||
page.on('request', request => {
|
||||
if (request.url().includes('non-existing-2'))
|
||||
page.interception.abort(request);
|
||||
else
|
||||
page.interception.continue(request);
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const result = await page.evaluate(async() => {
|
||||
try {
|
||||
await fetch('/non-existing.json');
|
||||
} catch (e) {
|
||||
return e.message;
|
||||
}
|
||||
});
|
||||
if (CHROME)
|
||||
expect(result).toContain('Failed to fetch');
|
||||
else
|
||||
expect(result).toContain('NetworkError');
|
||||
});
|
||||
it('should work with equal requests', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
let responseCount = 1;
|
||||
server.setRoute('/zzz', (req, res) => res.end((responseCount++) * 11 + ''));
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
|
||||
let spinner = false;
|
||||
// Cancel 2nd request.
|
||||
page.on('request', request => {
|
||||
if (utils.isFavicon(request)) {
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
return;
|
||||
}
|
||||
spinner ? page.interception.abort(request) : page.interception.continue(request);
|
||||
spinner ? request.abort() : request.continue();
|
||||
spinner = !spinner;
|
||||
});
|
||||
const results = await page.evaluate(() => Promise.all([
|
||||
@ -306,11 +289,11 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
expect(results).toEqual(['11', 'FAILED', '22']);
|
||||
});
|
||||
it.skip(FFOX)('should navigate to dataURL and fire dataURL requests', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
const requests = [];
|
||||
page.on('request', request => {
|
||||
requests.push(request);
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
const dataURL = 'data:text/html,<div>yo</div>';
|
||||
const response = await page.goto(dataURL);
|
||||
@ -320,11 +303,11 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
});
|
||||
it.skip(FFOX)('should be able to fetch dataURL and fire dataURL requests', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
const requests = [];
|
||||
page.on('request', request => {
|
||||
requests.push(request);
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
const dataURL = 'data:text/html,<div>yo</div>';
|
||||
const text = await page.evaluate(url => fetch(url).then(r => r.text()), dataURL);
|
||||
@ -332,12 +315,12 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
expect(requests.length).toBe(1);
|
||||
expect(requests[0].url()).toBe(dataURL);
|
||||
});
|
||||
it.skip(FFOX)('should navigate to URL with hash and and fire requests without hash', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
it('should navigate to URL with hash and and fire requests without hash', async({page, server}) => {
|
||||
await page.setRequestInterception(true);
|
||||
const requests = [];
|
||||
page.on('request', request => {
|
||||
requests.push(request);
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE + '#hash');
|
||||
expect(response.status()).toBe(200);
|
||||
@ -348,25 +331,25 @@ 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.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.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}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
server.setRoute('/malformed?rnd=%911', (req, res) => res.end());
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
page.on('request', request => request.continue());
|
||||
const response = await page.goto(server.PREFIX + '/malformed?rnd=%911');
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
it.skip(FFOX)('should work with encoded server - 2', async({page, server}) => {
|
||||
// The requestWillBeSent will report URL as-is, whereas interception will
|
||||
// report encoded URL for stylesheet. @see crbug.com/759388
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
const requests = [];
|
||||
page.on('request', request => {
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
requests.push(request);
|
||||
});
|
||||
const response = await page.goto(`data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`);
|
||||
@ -376,7 +359,7 @@ 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>');
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
let request = null;
|
||||
page.on('request', async r => request = r);
|
||||
page.$eval('iframe', (frame, url) => frame.src = url, server.EMPTY_PAGE),
|
||||
@ -385,14 +368,15 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
// Delete frame to cause request to be canceled.
|
||||
await page.$eval('iframe', frame => frame.remove());
|
||||
let error = null;
|
||||
await page.interception.continue(request).catch(e => error = e);
|
||||
await request.continue().catch(e => error = e);
|
||||
expect(error).toBe(null);
|
||||
});
|
||||
it('should throw if interception is not enabled', async({page, server}) => {
|
||||
it('should throw if interception is not enabled', async({newPage, server}) => {
|
||||
let error = null;
|
||||
const page = await newPage();
|
||||
page.on('request', async request => {
|
||||
try {
|
||||
await page.interception.continue(request);
|
||||
await request.continue();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
@ -400,32 +384,20 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(error.message).toContain('Request Interception is not enabled');
|
||||
});
|
||||
it.skip(FFOX)('should work with file URLs', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
const urls = new Set();
|
||||
page.on('request', request => {
|
||||
urls.add(request.url().split('/').pop());
|
||||
page.interception.continue(request);
|
||||
});
|
||||
await page.goto(pathToFileURL(path.join(__dirname, '..', 'assets', 'one-style.html')));
|
||||
expect(urls.size).toBe(2);
|
||||
expect(urls.has('one-style.html')).toBe(true);
|
||||
expect(urls.has('one-style.css')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip(WEBKIT)('Interception.continue', function() {
|
||||
describe('Interception.continue', function() {
|
||||
it('should work', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.continue());
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
});
|
||||
it('should amend HTTP headers', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
const headers = Object.assign({}, request.headers());
|
||||
headers['FOO'] = 'bar';
|
||||
page.interception.continue(request, { headers });
|
||||
request.continue({ headers });
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const [request] = await Promise.all([
|
||||
@ -434,67 +406,18 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
]);
|
||||
expect(request.headers['foo']).toBe('bar');
|
||||
});
|
||||
it.skip(FFOX)('should redirect in a way non-observable to page', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
page.on('request', request => {
|
||||
const redirectURL = request.url().includes('/empty.html') ? server.PREFIX + '/consolelog.html' : undefined;
|
||||
page.interception.continue(request, { url: redirectURL });
|
||||
});
|
||||
let consoleMessage = null;
|
||||
page.on('console', msg => consoleMessage = msg);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||
expect(consoleMessage.text()).toBe('yellow');
|
||||
});
|
||||
it.skip(FFOX)('should amend method', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
||||
await page.interception.enable();
|
||||
page.on('request', request => {
|
||||
page.interception.continue(request, { method: 'POST' });
|
||||
});
|
||||
const [request] = await Promise.all([
|
||||
server.waitForRequest('/sleep.zzz'),
|
||||
page.evaluate(() => fetch('/sleep.zzz'))
|
||||
]);
|
||||
expect(request.method).toBe('POST');
|
||||
});
|
||||
it.skip(FFOX)('should amend post data', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
||||
await page.interception.enable();
|
||||
page.on('request', request => {
|
||||
page.interception.continue(request, { postData: 'doggo' });
|
||||
});
|
||||
const [serverRequest] = await Promise.all([
|
||||
server.waitForRequest('/sleep.zzz'),
|
||||
page.evaluate(() => fetch('/sleep.zzz', { method: 'POST', body: 'birdy' }))
|
||||
]);
|
||||
expect(await serverRequest.postBody).toBe('doggo');
|
||||
});
|
||||
it.skip(FFOX)('should amend both post data and method on navigation', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
page.on('request', request => {
|
||||
page.interception.continue(request, { method: 'POST', postData: 'doggo' });
|
||||
});
|
||||
const [serverRequest] = await Promise.all([
|
||||
server.waitForRequest('/empty.html'),
|
||||
page.goto(server.EMPTY_PAGE),
|
||||
]);
|
||||
expect(serverRequest.method).toBe('POST');
|
||||
expect(await serverRequest.postBody).toBe('doggo');
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip(FFOX || WEBKIT)('interception.fulfill', function() {
|
||||
describe.skip(FFOX)('interception.fulfill', function() {
|
||||
it('should work', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
page.interception.fulfill(request, {
|
||||
request.fulfill({
|
||||
status: 201,
|
||||
headers: {
|
||||
foo: 'bar'
|
||||
},
|
||||
contentType: 'text/html',
|
||||
body: 'Yo, page!'
|
||||
});
|
||||
});
|
||||
@ -504,9 +427,9 @@ 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.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
page.interception.fulfill(request, {
|
||||
request.fulfill({
|
||||
status: 422,
|
||||
body: 'Yo, page!'
|
||||
});
|
||||
@ -516,14 +439,14 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
expect(response.statusText()).toBe('Unprocessable Entity');
|
||||
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
|
||||
});
|
||||
it('should redirect', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
it.skip(WEBKIT)('should redirect', async({page, server}) => {
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
if (!request.url().includes('rrredirect')) {
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
return;
|
||||
}
|
||||
page.interception.fulfill(request, {
|
||||
request.fulfill({
|
||||
status: 302,
|
||||
headers: {
|
||||
location: server.EMPTY_PAGE,
|
||||
@ -536,10 +459,10 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||
});
|
||||
it('should allow mocking binary responses', async({page, server}) => {
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
const imageBuffer = fs.readFileSync(path.join(__dirname, '..', 'assets', 'pptr.png'));
|
||||
page.interception.fulfill(request, {
|
||||
const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png'));
|
||||
request.fulfill({
|
||||
contentType: 'image/png',
|
||||
body: imageBuffer
|
||||
});
|
||||
@ -554,9 +477,9 @@ 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.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
page.interception.fulfill(request, {
|
||||
request.fulfill({
|
||||
status: 200,
|
||||
headers: {
|
||||
'foo': true
|
||||
@ -572,7 +495,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip(FFOX)('Interception.authenticate', function() {
|
||||
describe.skip(FFOX || WEBKIT)('Interception.authenticate', function() {
|
||||
it('should work', async({page, server}) => {
|
||||
server.setAuth('/empty.html', 'user', 'pass');
|
||||
let response = await page.goto(server.EMPTY_PAGE);
|
||||
@ -610,7 +533,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip(FFOX)('Interception.setOfflineMode', function() {
|
||||
describe.skip(FFOX || WEBKIT)('Interception.setOfflineMode', function() {
|
||||
it('should work', async({page, server}) => {
|
||||
await page.interception.setOfflineMode(true);
|
||||
let error = null;
|
||||
@ -634,9 +557,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
const requests = new Map();
|
||||
page.on('request', request => {
|
||||
requests.set(request.url().split('/').pop(), request);
|
||||
page.interception.continue(request);
|
||||
request.continue();
|
||||
});
|
||||
await page.interception.enable();
|
||||
await page.setRequestInterception(true);
|
||||
server.setRedirect('/rrredirect', '/frames/one-frame.html');
|
||||
await page.goto(server.PREFIX + '/rrredirect');
|
||||
expect(requests.get('rrredirect').isNavigationRequest()).toBe(true);
|
||||
@ -650,8 +573,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
describe('Page.setCacheEnabled', function() {
|
||||
it('should stay disabled when toggling request interception on/off', async({page, server}) => {
|
||||
await page.setCacheEnabled(false);
|
||||
await page.interception.enable();
|
||||
await page.interception.disable();
|
||||
await page.setRequestInterception(true);
|
||||
await page.setRequestInterception(false);
|
||||
|
||||
await page.goto(server.PREFIX + '/cached/one-style.html');
|
||||
const [nonCachedRequest] = await Promise.all([
|
||||
@ -664,10 +587,10 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||
|
||||
describe('ignoreHTTPSErrors', function() {
|
||||
it('should work with request interception', async({newPage, httpsServer}) => {
|
||||
const page = await newPage({ ignoreHTTPSErrors: true });
|
||||
const page = await newPage({ ignoreHTTPSErrors: true, interceptNetwork: true });
|
||||
|
||||
await page.interception.enable();
|
||||
page.on('request', request => page.interception.continue(request));
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => request.continue());
|
||||
const response = await page.goto(httpsServer.EMPTY_PAGE);
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
@ -165,6 +165,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
|
||||
testRunner.loadTests(require('./queryselector.spec.js'), testOptions);
|
||||
testRunner.loadTests(require('./screenshot.spec.js'), testOptions);
|
||||
testRunner.loadTests(require('./waittask.spec.js'), testOptions);
|
||||
testRunner.loadTests(require('./interception.spec.js'), testOptions);
|
||||
|
||||
if (CHROME) {
|
||||
testRunner.loadTests(require('./chromium/chromium.spec.js'), testOptions);
|
||||
@ -178,7 +179,6 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
|
||||
if (CHROME || FFOX) {
|
||||
testRunner.loadTests(require('./features/accessibility.spec.js'), testOptions);
|
||||
testRunner.loadTests(require('./features/permissions.spec.js'), testOptions);
|
||||
testRunner.loadTests(require('./features/interception.spec.js'), testOptions);
|
||||
}
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user