diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index 575ca60d73..5437d40968 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -216,7 +216,7 @@ export class BrowserContext extends ChannelOwner { + async _fetch(url: string, options: FetchOptions = {}): Promise { 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 { if (options.videoSize && !options.videosPath) throw new Error(`"videoSize" option requires "videosPath" to be specified`); diff --git a/src/client/network.ts b/src/client/network.ts index 803dab01f6..c044b160c5 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -310,15 +310,18 @@ export class Route extends ChannelOwner { 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 { 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 { 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 implements api.WebSocket { diff --git a/src/client/page.ts b/src/client/page.ts index c91e0ab2ea..38d1a1733f 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -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 { + 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); diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index ed54effd40..82a56a2f1b 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -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 = {}; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index d399bc62e2..b3b5b9a89b 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -2191,6 +2191,7 @@ Route: body: string? isBase64: boolean? useInterceptedResponseBody: boolean? + fetchResponseUid: string? responseBody: returns: diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 08c7bb51ab..b87587f372 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -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({ diff --git a/src/server/network.ts b/src/server/network.ts index 639768b8fb..277de3cd4d 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -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 { diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts index c97f913a2c..350010b963 100644 --- a/tests/page/page-request-fulfill.spec.ts +++ b/tests/page/page-request-fulfill.spec.ts @@ -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'}); +});