From 59422a00f5b00aeda742be14b619fb4af44b16c7 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 24 Aug 2021 11:07:54 -0700 Subject: [PATCH] feat(route): rename method, add response option (#8386) --- docs/src/api/class-route.md | 5 ++ src/client/network.ts | 41 +++++++---- src/protocol/channels.ts | 2 + src/protocol/protocol.yml | 1 + src/protocol/validator.ts | 1 + src/server/network.ts | 8 +-- tests/page/page-request-intercept.spec.ts | 88 +++++++++++++++++------ types/types.d.ts | 5 ++ 8 files changed, 115 insertions(+), 36 deletions(-) diff --git a/docs/src/api/class-route.md b/docs/src/api/class-route.md index aabcd7560f..f096db67b9 100644 --- a/docs/src/api/class-route.md +++ b/docs/src/api/class-route.md @@ -181,6 +181,11 @@ page.route("**/xhr_endpoint", lambda route: route.fulfill(path="mock_data.json") await page.RouteAsync("**/xhr_endpoint", route => route.FulfillAsync(new RouteFulfillOptions { Path = "mock_data.json" })); ``` +### option: Route.fulfill._response +- `_response` <[Response]> + +Intercepted response. Will be used to populate all response fields not explicitely overridden. + ### option: Route.fulfill.status - `status` <[int]> diff --git a/src/client/network.ts b/src/client/network.ts index 74cdc566a3..ef1a8c39da 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -245,6 +245,8 @@ type InterceptResponse = true; type NotInterceptResponse = false; export class Route extends ChannelOwner implements api.Route { + private _interceptedResponse: api.Response | undefined; + static from(route: channels.RouteChannel): Route { return (route as any)._object; } @@ -263,8 +265,21 @@ export class Route extends ChannelOwner { + let useInterceptedResponseBody; + let { status: statusOption, headers: headersOption, body: bodyOption } = options; + if (options._response) { + statusOption = statusOption || options._response.status(); + headersOption = headersOption || options._response.headers(); + if (options.body === undefined && options.path === undefined) { + if (options._response === this._interceptedResponse) + useInterceptedResponseBody = true; + else + bodyOption = await options._response.body(); + } + } + let body = undefined; let isBase64 = false; let length = 0; @@ -273,19 +288,19 @@ export class Route extends ChannelOwner { - return await this._continue(options, true); + async _continueToResponse(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer, interceptResponse?: boolean } = {}): Promise { + this._interceptedResponse = await this._continue(options, true); + return this._interceptedResponse; } async continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}) { diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index c4b618ae65..74176ef724 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -2531,12 +2531,14 @@ export type RouteFulfillParams = { headers?: NameValue[], body?: string, isBase64?: boolean, + useInterceptedResponseBody?: boolean, }; export type RouteFulfillOptions = { status?: number, headers?: NameValue[], body?: string, isBase64?: boolean, + useInterceptedResponseBody?: boolean, }; export type RouteFulfillResult = void; export type RouteResponseBodyParams = {}; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 9e9ff598fa..97a7738963 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -2134,6 +2134,7 @@ Route: items: NameValue body: string? isBase64: boolean? + useInterceptedResponseBody: boolean? responseBody: returns: diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 41cec92632..b115d6fa66 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -1018,6 +1018,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { headers: tOptional(tArray(tType('NameValue'))), body: tOptional(tString), isBase64: tOptional(tBoolean), + useInterceptedResponseBody: tOptional(tBoolean), }); scheme.RouteResponseBodyParams = tOptional(tObject({})); scheme.ResourceTiming = tObject({ diff --git a/src/server/network.ts b/src/server/network.ts index a306e564bd..6c8a03e688 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -217,13 +217,13 @@ export class Route extends SdkObject { await this._delegate.abort(errorCode); } - async fulfill(overrides: { status?: number, headers?: types.HeadersArray, body?: string, isBase64?: boolean }) { + async fulfill(overrides: { status?: number, headers?: types.HeadersArray, body?: string, isBase64?: boolean, useInterceptedResponseBody?: boolean }) { 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) { + if (this._response && overrides.useInterceptedResponseBody) { body = (await this._delegate.responseBody()).toString('utf8'); isBase64 = false; } else { @@ -232,8 +232,8 @@ export class Route extends SdkObject { } } await this._delegate.fulfill({ - status: overrides.status || this._response?.status() || 200, - headers: overrides.headers || this._response?.headers() || [], + status: overrides.status || 200, + headers: overrides.headers || [], body, isBase64, }); diff --git a/tests/page/page-request-intercept.spec.ts b/tests/page/page-request-intercept.spec.ts index a94bd8fd85..dde0e7a112 100644 --- a/tests/page/page-request-intercept.spec.ts +++ b/tests/page/page-request-intercept.spec.ts @@ -23,7 +23,7 @@ import { expect, test as it } from './pageTest'; it('should fulfill intercepted response', async ({page, server, browserName}) => { await page.route('**/*', async route => { // @ts-expect-error - await route._intercept({}); + await route._continueToResponse({}); await route.fulfill({ status: 201, headers: { @@ -44,8 +44,9 @@ it('should fulfill response with empty body', async ({page, server, browserName, it.skip(browserName === 'chromium' && browserMajorVersion <= 91, 'Fails in Electron that uses old Chromium'); await page.route('**/*', async route => { // @ts-expect-error - await route._intercept({}); + const _response = await route._continueToResponse({}); await route.fulfill({ + _response, status: 201, body: '' }); @@ -55,6 +56,53 @@ it('should fulfill response with empty body', async ({page, server, browserName, expect(await response.text()).toBe(''); }); +it('should override with defaults when intercepted response not provided', async ({page, server, browserName, browserMajorVersion}) => { + it.skip(browserName === 'chromium' && browserMajorVersion <= 91, 'Fails in Electron that uses old Chromium'); + server.setRoute('/empty.html', (req, res) => { + res.setHeader('foo', 'bar'); + res.end('my content'); + }); + await page.route('**/*', async route => { + // @ts-expect-error + await route._continueToResponse({}); + await route.fulfill({ + status: 201, + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(201); + expect(await response.text()).toBe(''); + if (browserName === 'webkit') + expect(response.headers()).toEqual({'content-type': 'text/plain'}); + else + expect(response.headers()).toEqual({ }); +}); + +it('should fulfill with any response', async ({page, server, browserName, browserMajorVersion, isLinux}) => { + it.skip(browserName === 'chromium' && browserMajorVersion <= 91, 'Fails in Electron that uses old Chromium'); + it.fail(browserName === 'webkit' && isLinux, 'Network.responseReceived comes twice'); + + server.setRoute('/sample', (req, res) => { + res.setHeader('foo', 'bar'); + res.end('Woo-hoo'); + }); + const page2 = await page.context().newPage(); + const sampleResponse = await page2.goto(`${server.PREFIX}/sample`); + + await page.route('**/*', async route => { + // @ts-expect-error + await route._continueToResponse({}); + await route.fulfill({ + _response: sampleResponse, + status: 201, + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(201); + expect(await response.text()).toBe('Woo-hoo'); + expect(response.headers()['foo']).toBe('bar'); +}); + it('should throw on continue after intercept', async ({page, server, browserName}) => { let routeCallback; const routePromise = new Promise(f => routeCallback = f); @@ -63,7 +111,7 @@ it('should throw on continue after intercept', async ({page, server, browserName page.goto(server.EMPTY_PAGE).catch(e => {}); const route = await routePromise; // @ts-expect-error - await route._intercept(); + await route._continueToResponse(); try { await route.continue(); fail('did not throw'); @@ -76,8 +124,8 @@ it('should support fulfill after intercept', async ({page, server}) => { const requestPromise = server.waitForRequest('/title.html'); await page.route('**', async route => { // @ts-expect-error - await route._intercept(); - await route.fulfill(); + const _response = await route._continueToResponse(); + await route.fulfill({ _response }); }); const response = await page.goto(server.PREFIX + '/title.html'); const request = await requestPromise; @@ -95,8 +143,8 @@ it('should intercept failures', async ({page, browserName, browserMajorVersion, await page.route('**', async route => { try { // @ts-expect-error - await route._intercept(); - await route.fulfill(); + const _response = await route._continueToResponse(); + await route.fulfill({ _response }); } catch (e) { error = e; } @@ -115,13 +163,13 @@ it('should support request overrides', async ({page, server, browserName, browse const requestPromise = server.waitForRequest('/empty.html'); await page.route('**/foo', async route => { // @ts-expect-error - await route._intercept({ + const _response = await route._continueToResponse({ url: server.EMPTY_PAGE, method: 'POST', headers: {'foo': 'bar'}, postData: 'my data', }); - await route.fulfill(); + await route.fulfill({ _response }); }); await page.goto(server.PREFIX + '/foo'); const request = await requestPromise; @@ -142,14 +190,14 @@ it('should give access to the intercepted response', async ({page, server}) => { const route = await routePromise; // @ts-expect-error - const response = await route._intercept(); + const response = await route._continueToResponse(); expect(response.status()).toBe(200); expect(response.ok()).toBeTruthy(); expect(response.url()).toBe(server.PREFIX + '/title.html'); expect(response.headers()['content-type']).toBe('text/html; charset=utf-8'); - await Promise.all([route.fulfill(), evalPromise]); + await Promise.all([route.fulfill({ _response: response }), evalPromise]); }); it('should give access to the intercepted response status text', async ({page, server, browserName}) => { @@ -167,12 +215,12 @@ it('should give access to the intercepted response status text', async ({page, s const evalPromise = page.evaluate(url => fetch(url), server.PREFIX + '/title.html'); const route = await routePromise; // @ts-expect-error - const response = await route._intercept(); + const response = await route._continueToResponse(); expect(response.statusText()).toBe('You are awesome'); expect(response.url()).toBe(server.PREFIX + '/title.html'); - await Promise.all([route.fulfill(), evalPromise]); + await Promise.all([route.fulfill({ _response: response }), evalPromise]); }); it('should give access to the intercepted response body', async ({page, server}) => { @@ -186,17 +234,17 @@ it('should give access to the intercepted response body', async ({page, server}) const route = await routePromise; // @ts-expect-error - const response = await route._intercept(); + const response = await route._continueToResponse(); expect((await response.text())).toBe('{"foo": "bar"}\n'); - await Promise.all([route.fulfill(), evalPromise]); + await Promise.all([route.fulfill({ _response: response }), evalPromise]); }); it('should be abortable after interception', async ({page, server, browserName}) => { await page.route(/\.css$/, async route => { // @ts-expect-error - await route._intercept(); + await route._continueToResponse(); await route.abort(); }); let failed = false; @@ -224,7 +272,7 @@ it('should fulfill after redirects', async ({page, server, browserName}) => { await page.route('**/*', async route => { ++routeCalls; // @ts-expect-error - await route._intercept({}); + await route._continueToResponse({}); await route.fulfill({ status: 201, headers: { @@ -266,8 +314,8 @@ it('should fulfill original response after redirects', async ({page, browserName await page.route('**/*', async route => { ++routeCalls; // @ts-expect-error - await route._intercept({}); - await route.fulfill(); + const _response = await route._continueToResponse({}); + await route.fulfill({ _response }); }); const response = await page.goto(server.PREFIX + '/redirect/1.html'); expect(requestUrls).toEqual(expectedUrls); @@ -301,7 +349,7 @@ it('should abort after redirects', async ({page, browserName, server}) => { await page.route('**/*', async route => { ++routeCalls; // @ts-expect-error - await route._intercept({}); + await route._continueToResponse({}); await route.abort('connectionreset'); }); diff --git a/types/types.d.ts b/types/types.d.ts index 2f29346461..bcfb1ad4f4 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -11927,6 +11927,11 @@ export interface Route { * @param options */ fulfill(options?: { + /** + * Intercepted response. Will be used to populate all response fields not explicitely overridden. + */ + _response?: Response; + /** * Response body. */