feat(fulfill): improve fulfilling from har (#14789)
- `har` option is now an object `{ path, fallback }`.
- Allows falling back to `abort()`, `continue()` or throwing.
- Matches based on url + method.
- Follows redirects in the HAR file.
- Nice error/stack when throwing.
- Tests.
This commit is contained in:
parent
c7a28ac7e9
commit
7c0bff15ca
|
|
@ -217,10 +217,12 @@ Optional response body as text.
|
|||
Optional response body as raw bytes.
|
||||
|
||||
### option: Route.fulfill.har
|
||||
- `har` <[path]>
|
||||
- `har` <[Object]>
|
||||
- `path` <[string]> Path to the HAR file.
|
||||
- `fallback` ?<[RouteHARFallback]<"abort"|"continue"|"throw">> Behavior in the case where matching entry was not found in the HAR. Either [`method: Route.abort`] the request, [`method: Route.continue`] it, or throw an error. Defaults to "abort".
|
||||
|
||||
HAR file to extract the response from. If HAR file contains an entry with the matching the url, its headers, status and body will be used. Individual fields such as headers can be overridden using fulfill options. If matching entry is not found, this method will throw.
|
||||
If `har` is a relative path, then it is resolved relative to the current working directory.
|
||||
HAR file to extract the response from. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed automatically. Individual fields such as headers can be overridden using fulfill options.
|
||||
If `path` is a relative path, then it is resolved relative to the current working directory.
|
||||
|
||||
### option: Route.fulfill.path
|
||||
- `path` <[path]>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@ export type SetNetworkCookieParam = {
|
|||
sameSite?: 'Strict' | 'Lax' | 'None'
|
||||
};
|
||||
|
||||
type RouteHAR = {
|
||||
fallback?: 'abort' | 'continue' | 'throw';
|
||||
path: string;
|
||||
};
|
||||
|
||||
export class Request extends ChannelOwner<channels.RequestChannel> implements api.Request {
|
||||
private _redirectedFrom: Request | null = null;
|
||||
private _redirectedTo: Request | null = null;
|
||||
|
|
@ -259,12 +264,18 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
await this._followChain(true);
|
||||
}
|
||||
|
||||
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: string } = {}) {
|
||||
await this._innerFulfill(options);
|
||||
await this._followChain(true);
|
||||
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
const fallback = await this._innerFulfill(options);
|
||||
switch (fallback) {
|
||||
case 'abort': await this.abort(); break;
|
||||
case 'continue': await this.continue(); break;
|
||||
case 'done': await this._followChain(true); break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _innerFulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: string } = {}) {
|
||||
private async _innerFulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}): Promise<'abort' | 'continue' | 'done'> {
|
||||
let fetchResponseUid;
|
||||
let { status: statusOption, headers: headersOption, body } = options;
|
||||
|
||||
|
|
@ -272,14 +283,21 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
throw new Error(`At most one of "har" and "response" options should be present`);
|
||||
|
||||
if (options.har) {
|
||||
const fallback = options.har.fallback ?? 'abort';
|
||||
if (!['abort', 'continue', 'throw'].includes(fallback))
|
||||
throw new Error(`har.fallback: expected one of "abort", "continue" or "throw", received "${fallback}"`);
|
||||
const entry = await this._connection.localUtils()._channel.harFindEntry({
|
||||
cacheKey: this.request()._context()._guid,
|
||||
harFile: options.har,
|
||||
harFile: options.har.path,
|
||||
url: this.request().url(),
|
||||
method: this.request().method(),
|
||||
needBody: body === undefined,
|
||||
});
|
||||
if (entry.error)
|
||||
throw new Error(entry.error);
|
||||
if (entry.error) {
|
||||
if (fallback === 'throw')
|
||||
throw new Error(entry.error);
|
||||
return fallback;
|
||||
}
|
||||
if (statusOption === undefined)
|
||||
statusOption = entry.status;
|
||||
if (headersOption === undefined && entry.headers)
|
||||
|
|
@ -332,6 +350,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
isBase64,
|
||||
fetchResponseUid
|
||||
}));
|
||||
return 'done';
|
||||
}
|
||||
|
||||
async continue(options: OverridesForContinue = {}) {
|
||||
|
|
@ -577,7 +596,9 @@ export class RouteHandler {
|
|||
public handle(route: Route, request: Request, routeChain: (done: boolean) => Promise<void>) {
|
||||
++this.handledCount;
|
||||
route._startHandling(routeChain);
|
||||
this.handler(route, request);
|
||||
// Extract handler into a variable to avoid [RouteHandler.handler] in the stack.
|
||||
const handler = this.handler;
|
||||
handler(route, request);
|
||||
}
|
||||
|
||||
public willExpire(): boolean {
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@ export type LocalUtilsHarFindEntryParams = {
|
|||
cacheKey: string,
|
||||
harFile: string,
|
||||
url: string,
|
||||
method: string,
|
||||
needBody: boolean,
|
||||
};
|
||||
export type LocalUtilsHarFindEntryOptions = {
|
||||
|
|
|
|||
|
|
@ -479,6 +479,7 @@ LocalUtils:
|
|||
cacheKey: string
|
||||
harFile: string
|
||||
url: string
|
||||
method: string
|
||||
needBody: boolean
|
||||
returns:
|
||||
error: string?
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
cacheKey: tString,
|
||||
harFile: tString,
|
||||
url: tString,
|
||||
method: tString,
|
||||
needBody: tBoolean,
|
||||
});
|
||||
scheme.LocalUtilsHarClearCacheParams = tObject({
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { assert, createGuid } from '../../utils';
|
|||
import type { DispatcherScope } from './dispatcher';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import { yazl, yauzl } from '../../zipBundle';
|
||||
import type { Log } from '../har/har';
|
||||
import type { Log, Entry } from '../har/har';
|
||||
|
||||
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel> implements channels.LocalUtilsChannel {
|
||||
_type_LocalUtils: boolean;
|
||||
|
|
@ -104,17 +104,38 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
cache.set(params.harFile, harLog);
|
||||
}
|
||||
|
||||
const entry = harLog.entries.find(entry => entry.request.url === params.url);
|
||||
if (!entry)
|
||||
throw new Error(`No entry matching ${params.url}`);
|
||||
let base64body: string | undefined;
|
||||
if (params.needBody && entry.response.content && entry.response.content.text !== undefined) {
|
||||
if (entry.response.content.encoding === 'base64')
|
||||
base64body = entry.response.content.text;
|
||||
else
|
||||
base64body = Buffer.from(entry.response.content.text, 'utf8').toString('base64');
|
||||
const visited = new Set<Entry>();
|
||||
let url = params.url;
|
||||
let method = params.method;
|
||||
while (true) {
|
||||
const entry = harLog.entries.find(entry => entry.request.url === url && entry.request.method === method);
|
||||
if (!entry)
|
||||
throw new Error(`No entry matching ${params.url}`);
|
||||
if (visited.has(entry))
|
||||
throw new Error(`Found redirect cycle for ${params.url}`);
|
||||
visited.add(entry);
|
||||
|
||||
const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location');
|
||||
if (redirectStatus.includes(entry.response.status) && locationHeader) {
|
||||
const locationURL = new URL(locationHeader.value, url);
|
||||
url = locationURL.toString();
|
||||
if ((entry.response.status === 301 || entry.response.status === 302) && method === 'POST' ||
|
||||
entry.response.status === 303 && !['GET', 'HEAD'].includes(method)) {
|
||||
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
||||
method = 'GET';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let base64body: string | undefined;
|
||||
if (params.needBody && entry.response.content && entry.response.content.text !== undefined) {
|
||||
if (entry.response.content.encoding === 'base64')
|
||||
base64body = entry.response.content.text;
|
||||
else
|
||||
base64body = Buffer.from(entry.response.content.text, 'utf8').toString('base64');
|
||||
}
|
||||
return { status: entry.response.status, headers: entry.response.headers, body: base64body };
|
||||
}
|
||||
return { status: entry.response.status, headers: entry.response.headers, body: base64body };
|
||||
} catch (e) {
|
||||
return { error: `Error reading HAR file ${params.harFile}: ` + e.message };
|
||||
}
|
||||
|
|
@ -124,3 +145,5 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
this._harCache.delete(params.cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
|
|
|||
23
packages/playwright-core/types/types.d.ts
vendored
23
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -14868,12 +14868,25 @@ export interface Route {
|
|||
contentType?: string;
|
||||
|
||||
/**
|
||||
* HAR file to extract the response from. If HAR file contains an entry with the matching the url, its headers, status and
|
||||
* body will be used. Individual fields such as headers can be overridden using fulfill options. If matching entry is not
|
||||
* found, this method will throw. If `har` is a relative path, then it is resolved relative to the current working
|
||||
* directory.
|
||||
* HAR file to extract the response from. If HAR file contains an entry with the matching url and HTTP method, then the
|
||||
* entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed
|
||||
* automatically. Individual fields such as headers can be overridden using fulfill options. If `path` is a relative path,
|
||||
* then it is resolved relative to the current working directory.
|
||||
*/
|
||||
har?: string;
|
||||
har?: {
|
||||
/**
|
||||
* Path to the HAR file.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* Behavior in the case where matching entry was not found in the HAR. Either
|
||||
* [route.abort([errorCode])](https://playwright.dev/docs/api/class-route#route-abort) the request,
|
||||
* [route.continue([options])](https://playwright.dev/docs/api/class-route#route-continue) it, or throw an error. Defaults
|
||||
* to "abort".
|
||||
*/
|
||||
fallback?: "abort"|"continue"|"throw";
|
||||
};
|
||||
|
||||
/**
|
||||
* Response headers. Header values will be converted to a string.
|
||||
|
|
|
|||
371
tests/assets/har-fulfill.har
Normal file
371
tests/assets/har-fulfill.har
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
{
|
||||
"log": {
|
||||
"version": "1.2",
|
||||
"creator": {
|
||||
"name": "Playwright",
|
||||
"version": "1.23.0-next"
|
||||
},
|
||||
"browser": {
|
||||
"name": "chromium",
|
||||
"version": "103.0.5060.33"
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"startedDateTime": "2022-06-10T04:27:32.125Z",
|
||||
"id": "page@b17b177f1c2e66459db3dcbe44636ffd",
|
||||
"title": "Hey",
|
||||
"pageTimings": {
|
||||
"onContentLoad": 70,
|
||||
"onLoad": 70
|
||||
}
|
||||
}
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"_requestref": "request@ee2a0dc164935fcd4d9432d37b245f3c",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572145.898,
|
||||
"startedDateTime": "2022-06-10T04:27:32.146Z",
|
||||
"time": 8.286,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://no.playwright/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Accept",
|
||||
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||
},
|
||||
{
|
||||
"name": "Upgrade-Insecure-Requests",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
|
||||
}
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": 326,
|
||||
"bodySize": 0
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "111"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "text/html"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"size": 111,
|
||||
"mimeType": "text/html",
|
||||
"compression": 0,
|
||||
"text": "<title>Hey</title><link rel='stylesheet' href='./style.css'><script src='./script.js'></script><div>hello</div>"
|
||||
},
|
||||
"headersSize": 65,
|
||||
"bodySize": 170,
|
||||
"redirectURL": "",
|
||||
"_transferSize": 170
|
||||
},
|
||||
"cache": {
|
||||
"beforeRequest": null,
|
||||
"afterRequest": null
|
||||
},
|
||||
"timings": {
|
||||
"dns": -1,
|
||||
"connect": -1,
|
||||
"ssl": -1,
|
||||
"send": 0,
|
||||
"wait": 8.286,
|
||||
"receive": -1
|
||||
},
|
||||
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
|
||||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@f2ff0fd79321ff90d0bc1b5d6fc13bad",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572174.683,
|
||||
"startedDateTime": "2022-06-10T04:27:32.172Z",
|
||||
"time": 7.132,
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "http://no.playwright/style.css",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Accept",
|
||||
"value": "text/css,*/*;q=0.1"
|
||||
},
|
||||
{
|
||||
"name": "Referer",
|
||||
"value": "http://no.playwright/"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
|
||||
}
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": 220,
|
||||
"bodySize": 0
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "24"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "text/css"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"size": 24,
|
||||
"mimeType": "text/css",
|
||||
"compression": 0,
|
||||
"text": "body { background:cyan }"
|
||||
},
|
||||
"headersSize": 63,
|
||||
"bodySize": 81,
|
||||
"redirectURL": "",
|
||||
"_transferSize": 81
|
||||
},
|
||||
"cache": {
|
||||
"beforeRequest": null,
|
||||
"afterRequest": null
|
||||
},
|
||||
"timings": {
|
||||
"dns": -1,
|
||||
"connect": -1,
|
||||
"ssl": -1,
|
||||
"send": 0,
|
||||
"wait": 8.132,
|
||||
"receive": -1
|
||||
},
|
||||
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
|
||||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@f2ff0fd79321ff90d0bc1b5d6fc13bac",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572174.683,
|
||||
"startedDateTime": "2022-06-10T04:27:32.174Z",
|
||||
"time": 8.132,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://no.playwright/style.css",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Accept",
|
||||
"value": "text/css,*/*;q=0.1"
|
||||
},
|
||||
{
|
||||
"name": "Referer",
|
||||
"value": "http://no.playwright/"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
|
||||
}
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": 220,
|
||||
"bodySize": 0
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "24"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "text/css"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"size": 24,
|
||||
"mimeType": "text/css",
|
||||
"compression": 0,
|
||||
"text": "body { background: red }"
|
||||
},
|
||||
"headersSize": 63,
|
||||
"bodySize": 81,
|
||||
"redirectURL": "",
|
||||
"_transferSize": 81
|
||||
},
|
||||
"cache": {
|
||||
"beforeRequest": null,
|
||||
"afterRequest": null
|
||||
},
|
||||
"timings": {
|
||||
"dns": -1,
|
||||
"connect": -1,
|
||||
"ssl": -1,
|
||||
"send": 0,
|
||||
"wait": 8.132,
|
||||
"receive": -1
|
||||
},
|
||||
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
|
||||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@9626f59acb1f4a95f25112d32e9f7f60",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572175.042,
|
||||
"startedDateTime": "2022-06-10T04:27:32.175Z",
|
||||
"time": 15.997,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://no.playwright/script.js",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Accept",
|
||||
"value": "*/*"
|
||||
},
|
||||
{
|
||||
"name": "Referer",
|
||||
"value": "http://no.playwright/"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
|
||||
}
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": 205,
|
||||
"bodySize": 0
|
||||
},
|
||||
"response": {
|
||||
"status": 301,
|
||||
"statusText": "Moved Permanently",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "location",
|
||||
"value": "http://no.playwright/script2.js"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "x-unknown",
|
||||
"compression": 0
|
||||
},
|
||||
"headersSize": 77,
|
||||
"bodySize": 0,
|
||||
"redirectURL": "http://no.playwright/script2.js",
|
||||
"_transferSize": 77
|
||||
},
|
||||
"cache": {
|
||||
"beforeRequest": null,
|
||||
"afterRequest": null
|
||||
},
|
||||
"timings": {
|
||||
"dns": -1,
|
||||
"connect": -1,
|
||||
"ssl": -1,
|
||||
"send": 0,
|
||||
"wait": 7.673,
|
||||
"receive": 8.324
|
||||
},
|
||||
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
|
||||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@d7ee53396148a663b819c348c53b03fb",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572181.822,
|
||||
"startedDateTime": "2022-06-10T04:27:32.182Z",
|
||||
"time": 6.735,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://no.playwright/script2.js",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Accept",
|
||||
"value": "*/*"
|
||||
},
|
||||
{
|
||||
"name": "Referer",
|
||||
"value": "http://no.playwright/"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
|
||||
}
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": 206,
|
||||
"bodySize": 0
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "18"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "text/javascript"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"size": 18,
|
||||
"mimeType": "text/javascript",
|
||||
"compression": 0,
|
||||
"text": "window.value='foo'"
|
||||
},
|
||||
"headersSize": 70,
|
||||
"bodySize": 82,
|
||||
"redirectURL": "",
|
||||
"_transferSize": 82
|
||||
},
|
||||
"cache": {
|
||||
"beforeRequest": null,
|
||||
"afterRequest": null
|
||||
},
|
||||
"timings": {
|
||||
"dns": -1,
|
||||
"connect": -1,
|
||||
"ssl": -1,
|
||||
"send": 0,
|
||||
"wait": 6.735,
|
||||
"receive": -1
|
||||
},
|
||||
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
|
||||
"_securityDetails": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -292,47 +292,6 @@ it('should filter by regexp', async ({ contextFactory, server }, testInfo) => {
|
|||
expect(log.entries[0].request.url.endsWith('har.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should fulfill route from har', async ({ contextFactory, server }, testInfo) => {
|
||||
const kCustomCSS = 'body { background-color: rgb(50, 100, 150); }';
|
||||
|
||||
const harPath = testInfo.outputPath('test.har');
|
||||
const harContext = await contextFactory({ baseURL: server.PREFIX, recordHar: { path: harPath, urlFilter: '/*.css' }, ignoreHTTPSErrors: true });
|
||||
const harPage = await harContext.newPage();
|
||||
await harPage.route('**/one-style.css', async route => {
|
||||
// Make sure har content is not what the server returns.
|
||||
await route.fulfill({ body: kCustomCSS });
|
||||
});
|
||||
await harPage.goto('/har.html');
|
||||
await harContext.close();
|
||||
|
||||
const context = await contextFactory();
|
||||
const page1 = await context.newPage();
|
||||
await page1.route('**/*.css', async route => {
|
||||
// Fulfulling from har should give expected CSS.
|
||||
await route.fulfill({ har: harPath });
|
||||
});
|
||||
const [response1] = await Promise.all([
|
||||
page1.waitForResponse('**/one-style.css'),
|
||||
page1.goto(server.PREFIX + '/one-style.html'),
|
||||
]);
|
||||
expect(await response1.text()).toBe(kCustomCSS);
|
||||
await expect(page1.locator('body')).toHaveCSS('background-color', 'rgb(50, 100, 150)');
|
||||
await page1.close();
|
||||
|
||||
const page2 = await context.newPage();
|
||||
await page2.route('**/*.css', async route => {
|
||||
// Overriding status should make CSS not apply.
|
||||
await route.fulfill({ har: harPath, status: 404 });
|
||||
});
|
||||
const [response2] = await Promise.all([
|
||||
page2.waitForResponse('**/one-style.css'),
|
||||
page2.goto(server.PREFIX + '/one-style.html'),
|
||||
]);
|
||||
expect(response2.status()).toBe(404);
|
||||
await expect(page2.locator('body')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await page2.close();
|
||||
});
|
||||
|
||||
it('should include sizes', async ({ contextFactory, server, asset }, testInfo) => {
|
||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||
await page.goto(server.PREFIX + '/har.html');
|
||||
|
|
|
|||
|
|
@ -322,43 +322,98 @@ it('headerValue should return set-cookie from intercepted response', async ({ pa
|
|||
expect(await response.headerValue('Set-Cookie')).toBe('a=b');
|
||||
});
|
||||
|
||||
it('should complain about bad har', async ({ page, server, isElectron, isAndroid }, testInfo) => {
|
||||
it.fixme(isElectron, 'error: Browser context management is not supported.');
|
||||
it('should complain about har + response options', async ({ page, server, isAndroid }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
const response = await page.request.fetch(route.request());
|
||||
error = await route.fulfill({ har: { path: 'har' }, response }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toBe(`route.fulfill: At most one of "har" and "response" options should be present`);
|
||||
});
|
||||
|
||||
it('should complain about bad har.fallback', async ({ page, server, isAndroid }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
error = await route.fulfill({ har: { path: 'har', fallback: 'foo' } as any }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toBe(`route.fulfill: har.fallback: expected one of "abort", "continue" or "throw", received "foo"`);
|
||||
});
|
||||
|
||||
it('should fulfill from har, matching the method and following redirects', async ({ page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.route('**/*', route => route.fulfill({ har: { path: harPath } }));
|
||||
await page.goto('http://no.playwright/');
|
||||
// HAR contains a redirect for the script that should be followed automatically.
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
// HAR contains a POST for the css file that should not be used.
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
it('should fallback to abort when not found in har', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.route('**/*', route => route.fulfill({ har: { path: harPath } }));
|
||||
const error = await page.goto(server.EMPTY_PAGE).catch(e => e);
|
||||
expect(error instanceof Error).toBe(true);
|
||||
});
|
||||
|
||||
it('should support fallback:continue when not found in har', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.route('**/*', route => route.fulfill({ har: { path: harPath, fallback: 'continue' } }));
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
|
||||
});
|
||||
|
||||
it('should support fallback:throw when not found in har', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
error = await route.fulfill({ har: { path: harPath, fallback: 'throw' } }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toBe(`route.fulfill: Error reading HAR file ${harPath}: No entry matching ${server.PREFIX + '/one-style.css'}`);
|
||||
});
|
||||
|
||||
it('should complain about bad har with fallback:throw', async ({ page, server, isAndroid }, testInfo) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = testInfo.outputPath('test.har');
|
||||
fs.writeFileSync(harPath, JSON.stringify({ log: {} }), 'utf-8');
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
error = await route.fulfill({ har: harPath }).catch(e => e);
|
||||
error = await route.fulfill({ har: { path: harPath, fallback: 'throw' } }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toContain(`Error reading HAR file ${harPath}: Cannot read`);
|
||||
expect(error.message).toContain(`route.fulfill: Error reading HAR file ${harPath}:`);
|
||||
});
|
||||
|
||||
it('should complain about no entry found in har', async ({ page, server, isElectron, isAndroid }, testInfo) => {
|
||||
it.fixme(isElectron, 'error: Browser context management is not supported.');
|
||||
it('should override status when fulfilling from har', async ({ page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
const harPath = testInfo.outputPath('test.har');
|
||||
fs.writeFileSync(harPath, JSON.stringify({ log: { entries: [] } }), 'utf-8');
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
error = await route.fulfill({ har: harPath }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toBe(`Error reading HAR file ${harPath}: No entry matching ${server.PREFIX + '/one-style.css'}`);
|
||||
});
|
||||
|
||||
it('should complain about har + response options', async ({ page, server, isElectron, isAndroid }) => {
|
||||
it.fixme(isElectron, 'error: Browser context management is not supported.');
|
||||
it.fixme(isAndroid);
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
const response = await page.request.fetch(route.request());
|
||||
error = await route.fulfill({ har: 'har', response }).catch(e => e);
|
||||
await route.continue();
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.route('**/*', async route => {
|
||||
await route.fulfill({ har: { path: harPath }, status: route.request().url().endsWith('.css') ? 404 : undefined });
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toBe(`At most one of "har" and "response" options should be present`);
|
||||
await page.goto('http://no.playwright/');
|
||||
// Script should work.
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
// 404 should fail the CSS and styles should not apply.
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -895,7 +895,7 @@ it('should continue after exception', async ({ page, server }) => {
|
|||
});
|
||||
await page.route('**/empty.html', async route => {
|
||||
try {
|
||||
await route.fulfill({ har: 'file', response: {} as any });
|
||||
await route.fulfill({ har: { path: 'file' }, response: {} as any });
|
||||
} catch (e) {
|
||||
route.continue();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue