From f8c0f0d637a1d54f161dced40d2ebfa88aea0af4 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 13 Sep 2021 14:29:44 -0700 Subject: [PATCH] feat(fetch): support query params (#8893) --- docs/src/api/class-fetchrequest.md | 15 +++++++++++++++ src/client/fetch.ts | 14 ++++++++++++-- src/dispatchers/browserContextDispatcher.ts | 3 ++- src/protocol/channels.ts | 2 ++ src/protocol/protocol.yml | 3 +++ src/protocol/validator.ts | 1 + src/server/fetch.ts | 8 +++++++- src/server/types.ts | 1 + src/utils/utils.ts | 19 +++++++++++++++++++ tests/browsercontext-fetch.spec.ts | 21 ++++++++++++++++++++- types/types.d.ts | 15 +++++++++++++++ utils/testserver/index.js | 2 +- 12 files changed, 98 insertions(+), 6 deletions(-) diff --git a/docs/src/api/class-fetchrequest.md b/docs/src/api/class-fetchrequest.md index aee9d0a1a6..76a2cc36c5 100644 --- a/docs/src/api/class-fetchrequest.md +++ b/docs/src/api/class-fetchrequest.md @@ -16,6 +16,11 @@ context cookies from the response. The method will automatically follow redirect Target URL or Request to get all fetch parameters from. +### option: FetchRequest.fetch.params +- `params` <[Object]<[string], [string]>> + +Query parameters to be send with the URL. + ### option: FetchRequest.fetch.method - `method` <[string]> @@ -47,6 +52,11 @@ context cookies from the response. The method will automatically follow redirect Target URL or Request to get all fetch parameters from. +### option: FetchRequest.get.params +- `params` <[Object]<[string], [string]>> + +Query parameters to be send with the URL. + ### option: FetchRequest.get.headers - `headers` <[Object]<[string], [string]>> @@ -68,6 +78,11 @@ context cookies from the response. The method will automatically follow redirect Target URL or Request to get all fetch parameters from. +### option: FetchRequest.post.params +- `params` <[Object]<[string], [string]>> + +Query parameters to be send with the URL. + ### option: FetchRequest.post.headers - `headers` <[Object]<[string], [string]>> diff --git a/src/client/fetch.ts b/src/client/fetch.ts index d4ea65c88b..0a4655df03 100644 --- a/src/client/fetch.ts +++ b/src/client/fetch.ts @@ -17,13 +17,19 @@ import * as api from '../../types/types'; import { HeadersArray } from '../common/types'; import * as channels from '../protocol/channels'; -import { assert, headersObjectToArray, isString } from '../utils/utils'; +import { assert, headersObjectToArray, isString, objectToArray } from '../utils/utils'; import { BrowserContext } from './browserContext'; import * as network from './network'; import { RawHeaders } from './network'; import { Headers } from './types'; -export type FetchOptions = { method?: string, headers?: Headers, data?: string | Buffer, timeout?: number }; +export type FetchOptions = { + params?: { [key: string]: string; }, + method?: string, + headers?: Headers, + data?: string | Buffer, + timeout?: number +}; export class FetchRequest implements api.FetchRequest { private _context: BrowserContext; @@ -35,6 +41,7 @@ export class FetchRequest implements api.FetchRequest { async get( urlOrRequest: string | api.Request, options?: { + params?: { [key: string]: string; }; headers?: { [key: string]: string; }; timeout?: number; }): Promise { @@ -47,6 +54,7 @@ export class FetchRequest implements api.FetchRequest { async post( urlOrRequest: string | api.Request, options?: { + params?: { [key: string]: string; }; headers?: { [key: string]: string; }; data?: string | Buffer; timeout?: number; @@ -62,6 +70,7 @@ export class FetchRequest implements api.FetchRequest { const request: network.Request | undefined = (urlOrRequest instanceof network.Request) ? urlOrRequest as network.Request : undefined; assert(request || typeof urlOrRequest === 'string', 'First argument must be either URL string or Request'); const url = request ? request.url() : urlOrRequest as string; + const params = objectToArray(options.params); const method = options.method || request?.method(); // Cannot call allHeaders() here as the request may be paused inside route handler. const headersObj = options.headers || request?.headers() ; @@ -72,6 +81,7 @@ export class FetchRequest implements api.FetchRequest { const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined); const result = await channel.fetch({ url, + params, method, headers, postData, diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index 093159a9a5..8dce00a27a 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -28,7 +28,7 @@ import { CallMetadata } from '../server/instrumentation'; import { ArtifactDispatcher } from './artifactDispatcher'; import { Artifact } from '../server/artifact'; import { Request, Response } from '../server/network'; -import { headersArrayToObject } from '../utils/utils'; +import { arrayToObject, headersArrayToObject } from '../utils/utils'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { private _context: BrowserContext; @@ -110,6 +110,7 @@ export class BrowserContextDispatcher extends Dispatcher { const { fetchResponse, error } = await playwrightFetch(this._context, { url: params.url, + params: arrayToObject(params.params), method: params.method, headers: params.headers ? headersArrayToObject(params.headers, false) : undefined, postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined, diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 82a56a2f1b..4eb25a2da7 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -857,12 +857,14 @@ export type BrowserContextExposeBindingOptions = { export type BrowserContextExposeBindingResult = void; export type BrowserContextFetchParams = { url: string, + params?: NameValue[], method?: string, headers?: NameValue[], postData?: Binary, timeout?: number, }; export type BrowserContextFetchOptions = { + params?: NameValue[], method?: string, headers?: NameValue[], postData?: Binary, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index b3b5b9a89b..979d4fd635 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -616,6 +616,9 @@ BrowserContext: fetch: parameters: url: string + params: + type: array? + items: NameValue method: string? headers: type: array? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index b87587f372..a8cc0da914 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -394,6 +394,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.BrowserContextFetchParams = tObject({ url: tString, + params: tOptional(tArray(tType('NameValue'))), method: tOptional(tString), headers: tOptional(tArray(tType('NameValue'))), postData: tOptional(tBinary), diff --git a/src/server/fetch.ts b/src/server/fetch.ts index 1714806f8f..097e4a3afc 100644 --- a/src/server/fetch.ts +++ b/src/server/fetch.ts @@ -66,7 +66,13 @@ export async function playwrightFetch(context: BrowserContext, params: types.Fet if (context._options.ignoreHTTPSErrors) options.rejectUnauthorized = false; - const fetchResponse = await sendRequest(context, new URL(params.url, context._options.baseURL), options, params.postData); + const requestUrl = new URL(params.url, context._options.baseURL); + if (params.params) { + for (const [name, value] of Object.entries(params.params)) + requestUrl.searchParams.set(name, value); + } + + const fetchResponse = await sendRequest(context, requestUrl, options, params.postData); const fetchUid = context.storeFetchResponseBody(fetchResponse.body); return { fetchResponse: { ...fetchResponse, fetchUid } }; } catch (e) { diff --git a/src/server/types.ts b/src/server/types.ts index 78ecdd3d9f..edca190ecd 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -374,6 +374,7 @@ export type SetStorageState = { export type FetchOptions = { url: string, + params?: { [name: string]: string }, method?: string, headers?: { [name: string]: string }, postData?: Buffer, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d1e1d55764..2d4d0b5195 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -24,6 +24,7 @@ import { spawn } from 'child_process'; import { getProxyForUrl } from 'proxy-from-env'; import * as URL from 'url'; import { getUbuntuVersionSync } from './ubuntuVersion'; +import { NameValue } from '../protocol/channels'; // `https-proxy-agent` v5 is written in TypeScript and exposes generated types. // However, as of June 2020, its types are generated with tsconfig that enables @@ -288,6 +289,24 @@ class HashStream extends stream.Writable { } } +export function objectToArray(map?: { [key: string]: string }): NameValue[] | undefined { + if (!map) + return undefined; + const result = []; + for (const [name, value] of Object.entries(map)) + result.push({ name, value }); + return result; +} + +export function arrayToObject(array?: NameValue[]): { [key: string]: string } | undefined { + if (!array) + return undefined; + const result: { [key: string]: string } = {}; + for (const {name, value} of array) + result[name] = value; + return result; +} + export async function calculateFileSha1(filename: string): Promise { const hashStream = new HashStream(); const stream = fs.createReadStream(filename); diff --git a/tests/browsercontext-fetch.spec.ts b/tests/browsercontext-fetch.spec.ts index a1e8520805..29f750b0de 100644 --- a/tests/browsercontext-fetch.spec.ts +++ b/tests/browsercontext-fetch.spec.ts @@ -40,7 +40,7 @@ it.afterAll(() => { http.globalAgent = prevAgent; }); -it('should work', async ({context, server}) => { +it('get should work', async ({context, server}) => { const response = await context.request.get(server.PREFIX + '/simple.json'); expect(response.url()).toBe(server.PREFIX + '/simple.json'); expect(response.status()).toBe(200); @@ -128,6 +128,25 @@ it('should add session cookies to request', async ({context, server}) => { expect(req.headers.cookie).toEqual('username=John Doe'); }); +it('should support queryParams', async ({context, server}) => { + let request; + server.setRoute('/empty.html', (req, res) => { + request = req; + server.serveFile(req, res); + }); + for (const method of ['get', 'post', 'fetch']) { + await context.request[method](server.EMPTY_PAGE + '?p1=foo', { + params: { + 'p1': 'v1', + 'парам2': 'знач2', + } + }); + const params = new URLSearchParams(request.url.substr(request.url.indexOf('?'))); + expect(params.get('p1')).toEqual('v1'); + expect(params.get('парам2')).toEqual('знач2'); + } +}); + it('should not add context cookie if cookie header passed as a parameter', async ({context, server}) => { await context.addCookies([{ name: 'username', diff --git a/types/types.d.ts b/types/types.d.ts index 60219e72c0..8a5d3d0f44 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -12646,6 +12646,11 @@ export interface FetchRequest { */ method?: string; + /** + * Query parameters to be send with the URL. + */ + params?: { [key: string]: string; }; + /** * Request timeout in milliseconds. */ @@ -12664,6 +12669,11 @@ export interface FetchRequest { */ headers?: { [key: string]: string; }; + /** + * Query parameters to be send with the URL. + */ + params?: { [key: string]: string; }; + /** * Request timeout in milliseconds. */ @@ -12687,6 +12697,11 @@ export interface FetchRequest { */ headers?: { [key: string]: string; }; + /** + * Query parameters to be send with the URL. + */ + params?: { [key: string]: string; }; + /** * Request timeout in milliseconds. */ diff --git a/utils/testserver/index.js b/utils/testserver/index.js index 077315bf24..b7090bd60e 100644 --- a/utils/testserver/index.js +++ b/utils/testserver/index.js @@ -238,7 +238,7 @@ class TestServer { request.on('data', chunk => body = Buffer.concat([body, chunk])); request.on('end', () => resolve(body)); }); - const pathName = url.parse(request.url).path; + const pathName = url.parse(request.url).pathname; this.debugServer(`request ${request.method} ${pathName}`); if (this._auths.has(pathName)) { const auth = this._auths.get(pathName);