feat(route): fulfill with HARResponse (#14865)
This commit is contained in:
parent
c8283cf9de
commit
e00a26a11d
|
|
@ -508,9 +508,9 @@ File path to respond with. The content type will be inferred from file extension
|
||||||
is resolved relative to the current working directory.
|
is resolved relative to the current working directory.
|
||||||
|
|
||||||
### option: Route.fulfill.response
|
### option: Route.fulfill.response
|
||||||
- `response` <[APIResponse]>
|
- `response` <[APIResponse]|[HARResponse]>
|
||||||
|
|
||||||
[APIResponse] to fulfill route's request with. Individual fields of the response (such as headers) can be overridden using fulfill options.
|
[APIResponse] or [HARResponse] to fulfill route's request with. Individual fields of the response (such as headers) can be overridden using fulfill options.
|
||||||
|
|
||||||
## method: Route.request
|
## method: Route.request
|
||||||
- returns: <[Request]>
|
- returns: <[Request]>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"./lib/server": "./lib/server/index.js",
|
"./lib/server": "./lib/server/index.js",
|
||||||
"./lib/utilsBundle": "./lib/utilsBundle.js",
|
"./lib/utilsBundle": "./lib/utilsBundle.js",
|
||||||
"./lib/zipBundle": "./lib/zipBundle.js",
|
"./lib/zipBundle": "./lib/zipBundle.js",
|
||||||
|
"./types/har": "./types/har.d.ts",
|
||||||
"./types/protocol": "./types/protocol.d.ts",
|
"./types/protocol": "./types/protocol.d.ts",
|
||||||
"./types/structs": "./types/structs.d.ts"
|
"./types/structs": "./types/structs.d.ts"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import type { HeadersArray, URLMatch } from '../common/types';
|
||||||
import { urlMatches } from '../common/netUtils';
|
import { urlMatches } from '../common/netUtils';
|
||||||
import { MultiMap } from '../utils/multimap';
|
import { MultiMap } from '../utils/multimap';
|
||||||
import { APIResponse } from './fetch';
|
import { APIResponse } from './fetch';
|
||||||
|
import type { HARResponse } from '../../types/har';
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -292,7 +293,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
this._reportHandled(true);
|
this._reportHandled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}) {
|
async fulfill(options: { response?: api.APIResponse | HARResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}) {
|
||||||
this._checkNotHandled();
|
this._checkNotHandled();
|
||||||
await this._wrapApiCall(async () => {
|
await this._wrapApiCall(async () => {
|
||||||
const fallback = await this._innerFulfill(options);
|
const fallback = await this._innerFulfill(options);
|
||||||
|
|
@ -304,9 +305,9 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _innerFulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}): Promise<'abort' | 'continue' | 'done'> {
|
private async _innerFulfill(options: { response?: api.APIResponse | HARResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}): Promise<'abort' | 'continue' | 'done'> {
|
||||||
let fetchResponseUid;
|
let fetchResponseUid;
|
||||||
let { status: statusOption, headers: headersOption, body } = options;
|
let { status: statusOption, headers: headersOption, body, contentType } = options;
|
||||||
|
|
||||||
if (options.har && options.response)
|
if (options.har && options.response)
|
||||||
throw new Error(`At most one of "har" and "response" options should be present`);
|
throw new Error(`At most one of "har" and "response" options should be present`);
|
||||||
|
|
@ -335,15 +336,25 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
body = Buffer.from(entry.body, 'base64');
|
body = Buffer.from(entry.body, 'base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.response) {
|
if (options.response instanceof APIResponse) {
|
||||||
statusOption ??= options.response.status();
|
statusOption ??= options.response.status();
|
||||||
headersOption ??= options.response.headers();
|
headersOption ??= options.response.headers();
|
||||||
if (body === undefined && options.path === undefined && options.response instanceof APIResponse) {
|
if (body === undefined && options.path === undefined) {
|
||||||
if (options.response._request._connection === this._connection)
|
if (options.response._request._connection === this._connection)
|
||||||
fetchResponseUid = (options.response as APIResponse)._fetchUid();
|
fetchResponseUid = (options.response as APIResponse)._fetchUid();
|
||||||
else
|
else
|
||||||
body = await options.response.body();
|
body = await options.response.body();
|
||||||
}
|
}
|
||||||
|
} else if (options.response) {
|
||||||
|
const harResponse = options.response as HARResponse;
|
||||||
|
statusOption ??= harResponse.status;
|
||||||
|
headersOption ??= headersArrayToObject(harResponse.headers, false);
|
||||||
|
if (body === undefined && options.path === undefined) {
|
||||||
|
body = harResponse.content.text;
|
||||||
|
contentType ??= harResponse.content.mimeType;
|
||||||
|
if (body !== undefined && harResponse.content.encoding === 'base64')
|
||||||
|
body = Buffer.from(body, 'base64');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isBase64 = false;
|
let isBase64 = false;
|
||||||
|
|
@ -365,8 +376,8 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
const headers: Headers = {};
|
const headers: Headers = {};
|
||||||
for (const header of Object.keys(headersOption || {}))
|
for (const header of Object.keys(headersOption || {}))
|
||||||
headers[header.toLowerCase()] = String(headersOption![header]);
|
headers[header.toLowerCase()] = String(headersOption![header]);
|
||||||
if (options.contentType)
|
if (contentType)
|
||||||
headers['content-type'] = String(options.contentType);
|
headers['content-type'] = String(contentType);
|
||||||
else if (options.path)
|
else if (options.path)
|
||||||
headers['content-type'] = mime.getType(options.path) || 'application/octet-stream';
|
headers['content-type'] = mime.getType(options.path) || 'application/octet-stream';
|
||||||
if (length && !('content-length' in headers))
|
if (length && !('content-length' in headers))
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// see http://www.softwareishard.com/blog/har-12-spec/
|
// see http://www.softwareishard.com/blog/har-12-spec/
|
||||||
|
export type HARFile = {
|
||||||
|
log: Log;
|
||||||
|
};
|
||||||
|
|
||||||
export type Log = {
|
export type Log = {
|
||||||
version: string;
|
version: string;
|
||||||
creator: Creator;
|
creator: Creator;
|
||||||
|
|
|
||||||
167
packages/playwright-core/types/har.d.ts
vendored
Normal file
167
packages/playwright-core/types/har.d.ts
vendored
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// see http://www.softwareishard.com/blog/har-12-spec/
|
||||||
|
export type HARFile = {
|
||||||
|
log: HARLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HARLog = {
|
||||||
|
version: string;
|
||||||
|
creator: HARCreator;
|
||||||
|
browser?: HARBrowser;
|
||||||
|
pages?: HARPage[];
|
||||||
|
entries: HAREntry[];
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARCreator = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARBrowser = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARPage = {
|
||||||
|
startedDateTime: string;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
pageTimings: HARPageTimings;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARPageTimings = {
|
||||||
|
onContentLoad?: number;
|
||||||
|
onLoad?: number;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HAREntry = {
|
||||||
|
pageref?: string;
|
||||||
|
startedDateTime: string;
|
||||||
|
time: number;
|
||||||
|
request: HARRequest;
|
||||||
|
response: HARResponse;
|
||||||
|
cache: HARCache;
|
||||||
|
timings: HARTimings;
|
||||||
|
serverIPAddress?: string;
|
||||||
|
connection?: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARRequest = {
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
httpVersion: string;
|
||||||
|
cookies: HARCookie[];
|
||||||
|
headers: HARHeader[];
|
||||||
|
queryString: HARQueryParameter[];
|
||||||
|
postData?: HARPostData;
|
||||||
|
headersSize: number;
|
||||||
|
bodySize: number;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARResponse = {
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
httpVersion: string;
|
||||||
|
cookies: HARCookie[];
|
||||||
|
headers: HARHeader[];
|
||||||
|
content: HARContent;
|
||||||
|
redirectURL: string;
|
||||||
|
headersSize: number;
|
||||||
|
bodySize: number;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARCookie = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
path?: string;
|
||||||
|
domain?: string;
|
||||||
|
expires?: string;
|
||||||
|
httpOnly?: boolean;
|
||||||
|
secure?: boolean;
|
||||||
|
sameSite?: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARHeader = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARQueryParameter = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARPostData = {
|
||||||
|
mimeType: string;
|
||||||
|
params: HARParam[];
|
||||||
|
text: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARParam = {
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
fileName?: string;
|
||||||
|
contentType?: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARContent = {
|
||||||
|
size: number;
|
||||||
|
compression?: number;
|
||||||
|
mimeType: string;
|
||||||
|
text?: string;
|
||||||
|
encoding?: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARCache = {
|
||||||
|
beforeRequest?: HARCacheState;
|
||||||
|
afterRequest?: HARCacheState;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARCacheState = {
|
||||||
|
expires?: string;
|
||||||
|
lastAccess: string;
|
||||||
|
eTag: string;
|
||||||
|
hitCount: number;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HARTimings = {
|
||||||
|
blocked?: number;
|
||||||
|
dns?: number;
|
||||||
|
connect?: number;
|
||||||
|
send: number;
|
||||||
|
wait: number;
|
||||||
|
receive: number;
|
||||||
|
ssl?: number;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
9
packages/playwright-core/types/types.d.ts
vendored
9
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -17,10 +17,13 @@
|
||||||
import { Protocol } from 'playwright-core/types/protocol';
|
import { Protocol } from 'playwright-core/types/protocol';
|
||||||
import { ChildProcess } from 'child_process';
|
import { ChildProcess } from 'child_process';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { HARResponse } from 'playwright-core/types/har';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { ReadStream } from 'fs';
|
import { ReadStream } from 'fs';
|
||||||
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from 'playwright-core/types/structs';
|
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from 'playwright-core/types/structs';
|
||||||
|
|
||||||
|
export * from 'playwright-core/types/har';
|
||||||
|
|
||||||
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
||||||
state?: 'visible'|'attached';
|
state?: 'visible'|'attached';
|
||||||
};
|
};
|
||||||
|
|
@ -14980,10 +14983,10 @@ export interface Route {
|
||||||
path?: string;
|
path?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [APIResponse] to fulfill route's request with. Individual fields of the response (such as headers) can be overridden
|
* [APIResponse] or [HARResponse] to fulfill route's request with. Individual fields of the response (such as headers) can
|
||||||
* using fulfill options.
|
* be overridden using fulfill options.
|
||||||
*/
|
*/
|
||||||
response?: APIResponse;
|
response?: APIResponse|HARResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response status code, defaults to `200`.
|
* Response status code, defaults to `200`.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
import { test as base, expect } from './pageTest';
|
import { test as base, expect } from './pageTest';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import type { HARFile } from '@playwright/test';
|
||||||
|
|
||||||
const it = base.extend<{
|
const it = base.extend<{
|
||||||
// We access test servers at 10.0.2.2 from inside the browser on Android,
|
// We access test servers at 10.0.2.2 from inside the browser on Android,
|
||||||
|
|
@ -418,3 +419,46 @@ it('should override status when fulfilling from har', async ({ page, isAndroid,
|
||||||
// 404 should fail the CSS and styles should not apply.
|
// 404 should fail the CSS and styles should not apply.
|
||||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
await expect(page.locator('body')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fulfill with har response', async ({ page, isAndroid, asset }) => {
|
||||||
|
it.fixme(isAndroid);
|
||||||
|
|
||||||
|
const harPath = asset('har-fulfill.har');
|
||||||
|
const har = JSON.parse(await fs.promises.readFile(harPath, 'utf-8')) as HARFile;
|
||||||
|
await page.route('**/*', async route => {
|
||||||
|
const response = findResponse(har, route.request().url());
|
||||||
|
await route.fulfill({ response });
|
||||||
|
});
|
||||||
|
await page.goto('http://no.playwright/');
|
||||||
|
// HAR contains a redirect for the script.
|
||||||
|
expect(await page.evaluate('window.value')).toBe('foo');
|
||||||
|
// HAR contains a POST for the css file but we match ignoring the method, so the file should be served.
|
||||||
|
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(0, 255, 255)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override status when fulfill with response from har', async ({ page, isAndroid, asset }) => {
|
||||||
|
it.fixme(isAndroid);
|
||||||
|
|
||||||
|
const harPath = asset('har-fulfill.har');
|
||||||
|
const har = JSON.parse(await fs.promises.readFile(harPath, 'utf-8')) as HARFile;
|
||||||
|
await page.route('**/*', async route => {
|
||||||
|
const response = findResponse(har, route.request().url());
|
||||||
|
await route.fulfill({ response, status: route.request().url().endsWith('.css') ? 404 : undefined });
|
||||||
|
});
|
||||||
|
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)');
|
||||||
|
});
|
||||||
|
|
||||||
|
function findResponse(har: HARFile, url: string) {
|
||||||
|
let entry;
|
||||||
|
const originalUrl = url;
|
||||||
|
while (url.trim()) {
|
||||||
|
entry = har.log.entries.find(entry => entry.request.url === url);
|
||||||
|
url = entry?.response.redirectURL;
|
||||||
|
}
|
||||||
|
expect(entry, originalUrl).toBeTruthy();
|
||||||
|
return entry?.response;
|
||||||
|
}
|
||||||
|
|
|
||||||
3
utils/generate_types/overrides.d.ts
vendored
3
utils/generate_types/overrides.d.ts
vendored
|
|
@ -16,10 +16,13 @@
|
||||||
import { Protocol } from 'playwright-core/types/protocol';
|
import { Protocol } from 'playwright-core/types/protocol';
|
||||||
import { ChildProcess } from 'child_process';
|
import { ChildProcess } from 'child_process';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { HARResponse } from 'playwright-core/types/har';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { ReadStream } from 'fs';
|
import { ReadStream } from 'fs';
|
||||||
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from 'playwright-core/types/structs';
|
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from 'playwright-core/types/structs';
|
||||||
|
|
||||||
|
export * from 'playwright-core/types/har';
|
||||||
|
|
||||||
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
||||||
state?: 'visible'|'attached';
|
state?: 'visible'|'attached';
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue