feat(fetch): fulfill without passing fetch response body client<->server (#8789)

This commit is contained in:
Yury Semikhatsky 2021-09-08 14:59:12 -07:00 committed by GitHub
parent 5a305a9c2e
commit b11b274b0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 65 additions and 9 deletions

View File

@ -216,7 +216,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
});
}
async _fetch(url: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer, timeout?: number } = {}): Promise<network.FetchResponse> {
async _fetch(url: string, options: FetchOptions = {}): Promise<network.FetchResponse> {
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
const result = await channel.fetch({
@ -383,6 +383,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
}
}
export type FetchOptions = { url?: string, method?: string, headers?: Headers, postData?: string | Buffer, timeout?: number };
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
if (options.videoSize && !options.videosPath)
throw new Error(`"videoSize" option requires "videosPath" to be specified`);

View File

@ -310,15 +310,18 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
});
}
async fulfill(options: { response?: Response, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string } = {}) {
async fulfill(options: { response?: Response|FetchResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string } = {}) {
return this._wrapApiCall(async (channel: channels.RouteChannel) => {
let useInterceptedResponseBody;
let fetchResponseUid;
let { status: statusOption, headers: headersOption, body: bodyOption } = options;
if (options.response) {
statusOption ||= options.response.status();
headersOption ||= options.response.headers();
if (options.body === undefined && options.path === undefined) {
if (options.response === this._interceptedResponse)
if (options.response instanceof FetchResponse)
fetchResponseUid = (options.response as FetchResponse)._fetchUid();
else if (options.response === this._interceptedResponse)
useInterceptedResponseBody = true;
else
bodyOption = await options.response.body();
@ -358,7 +361,8 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
headers: headersObjectToArray(headers),
body,
isBase64,
useInterceptedResponseBody
useInterceptedResponseBody,
fetchResponseUid
});
});
}
@ -553,7 +557,7 @@ export class FetchResponse {
async body(): Promise<Buffer> {
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
const result = await channel.fetchResponseBody({ fetchUid: this._initializer.fetchUid });
const result = await channel.fetchResponseBody({ fetchUid: this._fetchUid() });
if (!result.binary)
throw new Error('Response has been disposed');
return Buffer.from(result.binary!, 'base64');
@ -572,9 +576,13 @@ export class FetchResponse {
async dispose(): Promise<void> {
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await channel.disposeFetchResponse({ fetchUid: this._initializer.fetchUid });
await channel.disposeFetchResponse({ fetchUid: this._fetchUid() });
});
}
_fetchUid(): string {
return this._initializer.fetchUid;
}
}
export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> implements api.WebSocket {

View File

@ -19,9 +19,10 @@ import { Events } from './events';
import { assert } from '../utils/utils';
import { TimeoutSettings } from '../utils/timeoutSettings';
import * as channels from '../protocol/channels';
import * as network from './network';
import { parseError, serializeError } from '../protocol/serializers';
import { Accessibility } from './accessibility';
import { BrowserContext } from './browserContext';
import { BrowserContext, FetchOptions } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog';
@ -437,6 +438,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
return this._mainFrame.evaluate(pageFunction, arg);
}
async _fetch(url: string, options: FetchOptions = {}): Promise<network.FetchResponse> {
return await this._browserContext._fetch(url, options);
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
return this._wrapApiCall(async (channel: channels.PageChannel) => {
const source = await evaluationScript(script, arg);

View File

@ -2680,6 +2680,7 @@ export type RouteFulfillParams = {
body?: string,
isBase64?: boolean,
useInterceptedResponseBody?: boolean,
fetchResponseUid?: string,
};
export type RouteFulfillOptions = {
status?: number,
@ -2687,6 +2688,7 @@ export type RouteFulfillOptions = {
body?: string,
isBase64?: boolean,
useInterceptedResponseBody?: boolean,
fetchResponseUid?: string,
};
export type RouteFulfillResult = void;
export type RouteResponseBodyParams = {};

View File

@ -2191,6 +2191,7 @@ Route:
body: string?
isBase64: boolean?
useInterceptedResponseBody: boolean?
fetchResponseUid: string?
responseBody:
returns:

View File

@ -1049,6 +1049,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
body: tOptional(tString),
isBase64: tOptional(tBoolean),
useInterceptedResponseBody: tOptional(tBoolean),
fetchResponseUid: tOptional(tString),
});
scheme.RouteResponseBodyParams = tOptional(tObject({}));
scheme.ResourceTiming = tObject({

View File

@ -219,13 +219,19 @@ export class Route extends SdkObject {
await this._delegate.abort(errorCode);
}
async fulfill(overrides: { status?: number, headers?: types.HeadersArray, body?: string, isBase64?: boolean, useInterceptedResponseBody?: boolean }) {
async fulfill(overrides: { status?: number, headers?: types.HeadersArray, body?: string, isBase64?: boolean, useInterceptedResponseBody?: boolean, fetchResponseUid?: string }) {
assert(!this._handled, 'Route is already handled!');
this._handled = true;
let body = overrides.body;
let isBase64 = overrides.isBase64 || false;
if (body === undefined) {
if (this._response && overrides.useInterceptedResponseBody) {
if (overrides.fetchResponseUid) {
const context = this._request.frame()._page._browserContext;
const buffer = context.fetchResponses.get(overrides.fetchResponseUid);
assert(buffer, 'Fetch response has been disposed');
body = buffer.toString('utf8');
isBase64 = false;
} else if (this._response && overrides.useInterceptedResponseBody) {
body = (await this._delegate.responseBody()).toString('utf8');
isBase64 = false;
} else {

View File

@ -193,3 +193,34 @@ it('should include the origin header', async ({page, server, isAndroid}) => {
expect(text).toBe('done');
expect(interceptedRequest.headers()['origin']).toEqual(server.PREFIX);
});
it('should fulfill with fetch result', async ({page, server}) => {
await page.route('**/*', async route => {
// @ts-expect-error
const response = await page._fetch(server.PREFIX + '/simple.json');
// @ts-expect-error
route.fulfill({ response });
});
const response = await page.goto(server.EMPTY_PAGE);
expect(response.status()).toBe(200);
expect(await response.json()).toEqual({'foo': 'bar'});
});
it('should fulfill with fetch result and overrides', async ({page, server}) => {
await page.route('**/*', async route => {
// @ts-expect-error
const response = await page._fetch(server.PREFIX + '/simple.json');
route.fulfill({
// @ts-expect-error
response,
status: 201,
headers: {
'foo': 'bar'
}
});
});
const response = await page.goto(server.EMPTY_PAGE);
expect(response.status()).toBe(201);
expect((await response.allHeaders()).foo).toEqual('bar');
expect(await response.json()).toEqual({'foo': 'bar'});
});