feat(fetch): support query params (#8893)

This commit is contained in:
Yury Semikhatsky 2021-09-13 14:29:44 -07:00 committed by GitHub
parent 3e5645ecea
commit f8c0f0d637
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 98 additions and 6 deletions

View file

@ -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. 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 ### option: FetchRequest.fetch.method
- `method` <[string]> - `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. 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 ### option: FetchRequest.get.headers
- `headers` <[Object]<[string], [string]>> - `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. 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 ### option: FetchRequest.post.headers
- `headers` <[Object]<[string], [string]>> - `headers` <[Object]<[string], [string]>>

View file

@ -17,13 +17,19 @@
import * as api from '../../types/types'; import * as api from '../../types/types';
import { HeadersArray } from '../common/types'; import { HeadersArray } from '../common/types';
import * as channels from '../protocol/channels'; 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 { BrowserContext } from './browserContext';
import * as network from './network'; import * as network from './network';
import { RawHeaders } from './network'; import { RawHeaders } from './network';
import { Headers } from './types'; 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 { export class FetchRequest implements api.FetchRequest {
private _context: BrowserContext; private _context: BrowserContext;
@ -35,6 +41,7 @@ export class FetchRequest implements api.FetchRequest {
async get( async get(
urlOrRequest: string | api.Request, urlOrRequest: string | api.Request,
options?: { options?: {
params?: { [key: string]: string; };
headers?: { [key: string]: string; }; headers?: { [key: string]: string; };
timeout?: number; timeout?: number;
}): Promise<FetchResponse> { }): Promise<FetchResponse> {
@ -47,6 +54,7 @@ export class FetchRequest implements api.FetchRequest {
async post( async post(
urlOrRequest: string | api.Request, urlOrRequest: string | api.Request,
options?: { options?: {
params?: { [key: string]: string; };
headers?: { [key: string]: string; }; headers?: { [key: string]: string; };
data?: string | Buffer; data?: string | Buffer;
timeout?: number; 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; 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'); assert(request || typeof urlOrRequest === 'string', 'First argument must be either URL string or Request');
const url = request ? request.url() : urlOrRequest as string; const url = request ? request.url() : urlOrRequest as string;
const params = objectToArray(options.params);
const method = options.method || request?.method(); const method = options.method || request?.method();
// Cannot call allHeaders() here as the request may be paused inside route handler. // Cannot call allHeaders() here as the request may be paused inside route handler.
const headersObj = options.headers || request?.headers() ; const headersObj = options.headers || request?.headers() ;
@ -72,6 +81,7 @@ export class FetchRequest implements api.FetchRequest {
const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined); const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined);
const result = await channel.fetch({ const result = await channel.fetch({
url, url,
params,
method, method,
headers, headers,
postData, postData,

View file

@ -28,7 +28,7 @@ import { CallMetadata } from '../server/instrumentation';
import { ArtifactDispatcher } from './artifactDispatcher'; import { ArtifactDispatcher } from './artifactDispatcher';
import { Artifact } from '../server/artifact'; import { Artifact } from '../server/artifact';
import { Request, Response } from '../server/network'; import { Request, Response } from '../server/network';
import { headersArrayToObject } from '../utils/utils'; import { arrayToObject, headersArrayToObject } from '../utils/utils';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer, channels.BrowserContextEvents> implements channels.BrowserContextChannel { export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer, channels.BrowserContextEvents> implements channels.BrowserContextChannel {
private _context: BrowserContext; private _context: BrowserContext;
@ -110,6 +110,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
async fetch(params: channels.BrowserContextFetchParams): Promise<channels.BrowserContextFetchResult> { async fetch(params: channels.BrowserContextFetchParams): Promise<channels.BrowserContextFetchResult> {
const { fetchResponse, error } = await playwrightFetch(this._context, { const { fetchResponse, error } = await playwrightFetch(this._context, {
url: params.url, url: params.url,
params: arrayToObject(params.params),
method: params.method, method: params.method,
headers: params.headers ? headersArrayToObject(params.headers, false) : undefined, headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined, postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,

View file

@ -857,12 +857,14 @@ export type BrowserContextExposeBindingOptions = {
export type BrowserContextExposeBindingResult = void; export type BrowserContextExposeBindingResult = void;
export type BrowserContextFetchParams = { export type BrowserContextFetchParams = {
url: string, url: string,
params?: NameValue[],
method?: string, method?: string,
headers?: NameValue[], headers?: NameValue[],
postData?: Binary, postData?: Binary,
timeout?: number, timeout?: number,
}; };
export type BrowserContextFetchOptions = { export type BrowserContextFetchOptions = {
params?: NameValue[],
method?: string, method?: string,
headers?: NameValue[], headers?: NameValue[],
postData?: Binary, postData?: Binary,

View file

@ -616,6 +616,9 @@ BrowserContext:
fetch: fetch:
parameters: parameters:
url: string url: string
params:
type: array?
items: NameValue
method: string? method: string?
headers: headers:
type: array? type: array?

View file

@ -394,6 +394,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
}); });
scheme.BrowserContextFetchParams = tObject({ scheme.BrowserContextFetchParams = tObject({
url: tString, url: tString,
params: tOptional(tArray(tType('NameValue'))),
method: tOptional(tString), method: tOptional(tString),
headers: tOptional(tArray(tType('NameValue'))), headers: tOptional(tArray(tType('NameValue'))),
postData: tOptional(tBinary), postData: tOptional(tBinary),

View file

@ -66,7 +66,13 @@ export async function playwrightFetch(context: BrowserContext, params: types.Fet
if (context._options.ignoreHTTPSErrors) if (context._options.ignoreHTTPSErrors)
options.rejectUnauthorized = false; 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); const fetchUid = context.storeFetchResponseBody(fetchResponse.body);
return { fetchResponse: { ...fetchResponse, fetchUid } }; return { fetchResponse: { ...fetchResponse, fetchUid } };
} catch (e) { } catch (e) {

View file

@ -374,6 +374,7 @@ export type SetStorageState = {
export type FetchOptions = { export type FetchOptions = {
url: string, url: string,
params?: { [name: string]: string },
method?: string, method?: string,
headers?: { [name: string]: string }, headers?: { [name: string]: string },
postData?: Buffer, postData?: Buffer,

View file

@ -24,6 +24,7 @@ import { spawn } from 'child_process';
import { getProxyForUrl } from 'proxy-from-env'; import { getProxyForUrl } from 'proxy-from-env';
import * as URL from 'url'; import * as URL from 'url';
import { getUbuntuVersionSync } from './ubuntuVersion'; import { getUbuntuVersionSync } from './ubuntuVersion';
import { NameValue } from '../protocol/channels';
// `https-proxy-agent` v5 is written in TypeScript and exposes generated types. // `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 // 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<string> { export async function calculateFileSha1(filename: string): Promise<string> {
const hashStream = new HashStream(); const hashStream = new HashStream();
const stream = fs.createReadStream(filename); const stream = fs.createReadStream(filename);

View file

@ -40,7 +40,7 @@ it.afterAll(() => {
http.globalAgent = prevAgent; 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'); const response = await context.request.get(server.PREFIX + '/simple.json');
expect(response.url()).toBe(server.PREFIX + '/simple.json'); expect(response.url()).toBe(server.PREFIX + '/simple.json');
expect(response.status()).toBe(200); 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'); 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}) => { it('should not add context cookie if cookie header passed as a parameter', async ({context, server}) => {
await context.addCookies([{ await context.addCookies([{
name: 'username', name: 'username',

15
types/types.d.ts vendored
View file

@ -12646,6 +12646,11 @@ export interface FetchRequest {
*/ */
method?: string; method?: string;
/**
* Query parameters to be send with the URL.
*/
params?: { [key: string]: string; };
/** /**
* Request timeout in milliseconds. * Request timeout in milliseconds.
*/ */
@ -12664,6 +12669,11 @@ export interface FetchRequest {
*/ */
headers?: { [key: string]: string; }; headers?: { [key: string]: string; };
/**
* Query parameters to be send with the URL.
*/
params?: { [key: string]: string; };
/** /**
* Request timeout in milliseconds. * Request timeout in milliseconds.
*/ */
@ -12687,6 +12697,11 @@ export interface FetchRequest {
*/ */
headers?: { [key: string]: string; }; headers?: { [key: string]: string; };
/**
* Query parameters to be send with the URL.
*/
params?: { [key: string]: string; };
/** /**
* Request timeout in milliseconds. * Request timeout in milliseconds.
*/ */

View file

@ -238,7 +238,7 @@ class TestServer {
request.on('data', chunk => body = Buffer.concat([body, chunk])); request.on('data', chunk => body = Buffer.concat([body, chunk]));
request.on('end', () => resolve(body)); 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}`); this.debugServer(`request ${request.method} ${pathName}`);
if (this._auths.has(pathName)) { if (this._auths.has(pathName)) {
const auth = this._auths.get(pathName); const auth = this._auths.get(pathName);