diff --git a/docs/src/api/class-apirequestcontext.md b/docs/src/api/class-apirequestcontext.md index 08cc8d993f..9275fb2d36 100644 --- a/docs/src/api/class-apirequestcontext.md +++ b/docs/src/api/class-apirequestcontext.md @@ -138,10 +138,13 @@ context cookies from the response. The method will automatically follow redirect ### param: APIRequestContext.delete.url = %%-fetch-param-url-%% * since: v1.16 +### option: APIRequestContext.delete.params = %%-js-fetch-option-params-%% +* since: v1.16 + ### param: APIRequestContext.delete.params = %%-java-csharp-fetch-params-%% * since: v1.18 -### option: APIRequestContext.delete.params = %%-js-python-fetch-option-params-%% +### option: APIRequestContext.delete.params = %%-python-fetch-option-params-%% * since: v1.16 ### option: APIRequestContext.delete.params = %%-csharp-fetch-option-params-%% @@ -297,10 +300,13 @@ await Request.FetchAsync("https://example.com/api/uploadScript", new() { Method Target URL or Request to get all parameters from. +### option: APIRequestContext.fetch.params = %%-js-fetch-option-params-%% +* since: v1.16 + ### param: APIRequestContext.fetch.params = %%-java-csharp-fetch-params-%% * since: v1.18 -### option: APIRequestContext.fetch.params = %%-js-python-fetch-option-params-%% +### option: APIRequestContext.fetch.params = %%-python-fetch-option-params-%% * since: v1.16 ### option: APIRequestContext.fetch.params = %%-csharp-fetch-option-params-%% @@ -397,10 +403,13 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query ### param: APIRequestContext.get.url = %%-fetch-param-url-%% * since: v1.16 +### option: APIRequestContext.get.params = %%-js-fetch-option-params-%% +* since: v1.16 + ### param: APIRequestContext.get.params = %%-java-csharp-fetch-params-%% * since: v1.18 -### option: APIRequestContext.get.params = %%-js-python-fetch-option-params-%% +### option: APIRequestContext.get.params = %%-python-fetch-option-params-%% * since: v1.16 ### option: APIRequestContext.get.params = %%-csharp-fetch-option-params-%% @@ -453,10 +462,13 @@ context cookies from the response. The method will automatically follow redirect ### param: APIRequestContext.head.url = %%-fetch-param-url-%% * since: v1.16 +### option: APIRequestContext.head.params = %%-js-fetch-option-params-%% +* since: v1.16 + ### param: APIRequestContext.head.params = %%-java-csharp-fetch-params-%% * since: v1.18 -### option: APIRequestContext.head.params = %%-js-python-fetch-option-params-%% +### option: APIRequestContext.head.params = %%-python-fetch-option-params-%% * since: v1.16 ### option: APIRequestContext.head.params = %%-csharp-fetch-option-params-%% @@ -509,10 +521,13 @@ context cookies from the response. The method will automatically follow redirect ### param: APIRequestContext.patch.url = %%-fetch-param-url-%% * since: v1.16 +### option: APIRequestContext.patch.params = %%-js-fetch-option-params-%% +* since: v1.16 + ### param: APIRequestContext.patch.params = %%-java-csharp-fetch-params-%% * since: v1.18 -### option: APIRequestContext.patch.params = %%-js-python-fetch-option-params-%% +### option: APIRequestContext.patch.params = %%-python-fetch-option-params-%% * since: v1.16 ### option: APIRequestContext.patch.params = %%-csharp-fetch-option-params-%% @@ -686,10 +701,13 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar ### param: APIRequestContext.post.url = %%-fetch-param-url-%% * since: v1.16 +### option: APIRequestContext.post.params = %%-js-fetch-option-params-%% +* since: v1.16 + ### param: APIRequestContext.post.params = %%-java-csharp-fetch-params-%% * since: v1.18 -### option: APIRequestContext.post.params = %%-js-python-fetch-option-params-%% +### option: APIRequestContext.post.params = %%-python-fetch-option-params-%% * since: v1.16 ### option: APIRequestContext.post.params = %%-csharp-fetch-option-params-%% @@ -742,10 +760,13 @@ context cookies from the response. The method will automatically follow redirect ### param: APIRequestContext.put.url = %%-fetch-param-url-%% * since: v1.16 +### option: APIRequestContext.put.params = %%-js-fetch-option-params-%% +* since: v1.16 + ### param: APIRequestContext.put.params = %%-java-csharp-fetch-params-%% * since: v1.18 -### option: APIRequestContext.put.params = %%-js-python-fetch-option-params-%% +### option: APIRequestContext.put.params = %%-python-fetch-option-params-%% * since: v1.16 ### option: APIRequestContext.put.params = %%-csharp-fetch-option-params-%% diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 1743066647..f4adc23a3c 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -356,8 +356,14 @@ Emulates consistent window screen size available inside web page via `window.scr Target URL. -## js-python-fetch-option-params -* langs: js, python +## js-fetch-option-params +* langs: js +- `params` <[Object]<[string], [string]|[number]|[boolean]>|[URLSearchParams]|[string]> + +Query parameters to be sent with the URL. + +## python-fetch-option-params +* langs: python - `params` <[Object]<[string], [string]|[float]|[boolean]>> Query parameters to be sent with the URL. diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 63f74ef26e..7aaa5069c2 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -32,7 +32,7 @@ import { TargetClosedError, isTargetClosedError } from './errors'; import { toClientCertificatesProtocol } from './browserContext'; export type FetchOptions = { - params?: { [key: string]: string; }, + params?: { [key: string]: string | number | boolean; } | URLSearchParams | string, method?: string, headers?: Headers, data?: string | Buffer | Serializable, @@ -175,7 +175,7 @@ export class APIRequestContext extends ChannelOwner= 0, `'maxRedirects' must be greater than or equal to '0'`); assert(options.maxRetries === undefined || options.maxRetries >= 0, `'maxRetries' must be greater than or equal to '0'`); const url = options.url !== undefined ? options.url : options.request!.url(); - const params = objectToArray(options.params); + const params = mapParamsToArray(options.params); const method = options.method || options.request?.method(); // Cannot call allHeaders() here as the request may be paused inside route handler. const headersObj = options.headers || options.request?.headers(); @@ -407,6 +407,30 @@ function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined { return result; } +function queryStringToArray(queryString: string): NameValue[] | undefined { + const searchParams = new URLSearchParams(queryString); + return searchParamsToArray(searchParams); +} + +function searchParamsToArray(searchParams: URLSearchParams): NameValue[] | undefined { + if (searchParams.size === 0) + return undefined; + + const result: NameValue[] = []; + for (const [name, value] of searchParams.entries()) + result.push({ name, value }); + return result; +} + +function mapParamsToArray(params: FetchOptions['params']): NameValue[] | undefined { + if (params instanceof URLSearchParams) + return searchParamsToArray(params); + if (typeof params === 'string') + return queryStringToArray(params); + + return objectToArray(params); +} + function isFilePayload(value: any): boolean { return typeof value === 'object' && value['name'] && value['mimeType'] && value['buffer']; } diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index aef805798d..6367e8875e 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -157,7 +157,7 @@ export abstract class APIRequestContext extends SdkObject { const requestUrl = new URL(params.url, defaults.baseURL); if (params.params) { for (const { name, value } of params.params) - requestUrl.searchParams.set(name, value); + requestUrl.searchParams.append(name, value); } const credentials = this._getHttpCredentials(requestUrl); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 91bf8a7d7c..b8bb07747d 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16518,7 +16518,7 @@ export interface APIRequestContext { /** * Query parameters to be sent with the URL. */ - params?: { [key: string]: string|number|boolean; }; + params?: { [key: string]: string|number|boolean; }|URLSearchParams|string; /** * Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. @@ -16654,7 +16654,7 @@ export interface APIRequestContext { /** * Query parameters to be sent with the URL. */ - params?: { [key: string]: string|number|boolean; }; + params?: { [key: string]: string|number|boolean; }|URLSearchParams|string; /** * Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. @@ -16754,7 +16754,7 @@ export interface APIRequestContext { /** * Query parameters to be sent with the URL. */ - params?: { [key: string]: string|number|boolean; }; + params?: { [key: string]: string|number|boolean; }|URLSearchParams|string; /** * Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. @@ -16840,7 +16840,7 @@ export interface APIRequestContext { /** * Query parameters to be sent with the URL. */ - params?: { [key: string]: string|number|boolean; }; + params?: { [key: string]: string|number|boolean; }|URLSearchParams|string; /** * Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. @@ -16926,7 +16926,7 @@ export interface APIRequestContext { /** * Query parameters to be sent with the URL. */ - params?: { [key: string]: string|number|boolean; }; + params?: { [key: string]: string|number|boolean; }|URLSearchParams|string; /** * Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. @@ -17054,7 +17054,7 @@ export interface APIRequestContext { /** * Query parameters to be sent with the URL. */ - params?: { [key: string]: string|number|boolean; }; + params?: { [key: string]: string|number|boolean; }|URLSearchParams|string; /** * Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. @@ -17140,7 +17140,7 @@ export interface APIRequestContext { /** * Query parameters to be sent with the URL. */ - params?: { [key: string]: string|number|boolean; }; + params?: { [key: string]: string|number|boolean; }|URLSearchParams|string; /** * Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 733596b481..eb8e8da075 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -122,22 +122,40 @@ it('should add session cookies to request', async ({ context, server }) => { }); for (const method of ['fetch', 'delete', 'get', 'head', 'patch', 'post', 'put'] as const) { - it(`${method} should support queryParams`, async ({ context, server }) => { - const url = new URL(server.EMPTY_PAGE); - url.searchParams.set('p1', 'v1'); - url.searchParams.set('парам2', 'знач2'); - const [request] = await Promise.all([ - server.waitForRequest(url.pathname + url.search), - 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(`${method} should support params passed as object`, async ({ context, server }) => { + const params = { + 'first-param': 'value2', + 'second-param': 'value', + }; + + const response = await context.request[method](server.EMPTY_PAGE + '?first-param=value1', { params }); + + const { searchParams } = new URL(response.url()); + expect(searchParams.getAll('first-param')).toEqual(['value1', 'value2']); + expect(searchParams.get('second-param')).toBe('value'); + }); + + it(`${method} should support params passed as URLSearchParams`, async ({ context, server }) => { + const params = new URLSearchParams(); + params.append('first-param', 'value1'); + params.append('first-param', 'value2'); + params.append('second-param', 'value'); + + const response = await context.request[method](server.EMPTY_PAGE, { params }); + + const { searchParams } = new URL(response.url()); + expect(searchParams.getAll('first-param')).toEqual(['value1', 'value2']); + expect(searchParams.get('second-param')).toBe('value'); + }); + + it(`${method} should support params passed as string`, async ({ context, server }) => { + const params = 'first-param=value1&first-param=value2&second-param=value'; + + const response = await context.request[method](server.EMPTY_PAGE, { params }); + + const { searchParams } = new URL(response.url()); + expect(searchParams.getAll('first-param')).toEqual(['value1', 'value2']); + expect(searchParams.get('second-param')).toBe('value'); }); it(`${method} should support failOnStatusCode`, async ({ context, server }) => {