feat(api): move fetch api into the namespace (#8871)

This commit is contained in:
Pavel Feldman 2021-09-13 12:43:07 -07:00 committed by GitHub
parent bf35da3656
commit 64f9c3ba1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 408 additions and 283 deletions

View file

@ -792,37 +792,6 @@ Name of the function on the window object.
Callback function that will be called in the Playwright's context.
## async method: BrowserContext.fetch
- returns: <[FetchResponse]>
Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
context cookies from the response. The method will automatically follow redirects.
### param: BrowserContext.fetch.urlOrRequest
- `urlOrRequest` <[string]|[Request]>
Target URL or Request to get all fetch parameters from.
### option: BrowserContext.fetch.method
- `method` <[string]>
If set changes the request method (e.g. PUT or POST). If not specified, GET method is used.
### option: BrowserContext.fetch.headers
- `headers` <[Object]<[string], [string]>>
Allows to set HTTP headers.
### option: BrowserContext.fetch.postData
- `postData` <[string]|[Buffer]>
Allows to set post data of the request.
### option: BrowserContext.fetch.timeout
- `timeout` <[float]>
Request timeout in milliseconds.
## async method: BrowserContext.grantPermissions
Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
@ -880,6 +849,11 @@ Creates a new page in the browser context.
Returns all open pages in the context.
## property: BrowserContext.request
- type: <[FetchRequest]>
API testing helper associated with this context. Requests made with this API will use context cookies.
## async method: BrowserContext.route
Routing provides the capability to modify network requests that are made by any page in the browser context. Once route

View file

@ -0,0 +1,84 @@
# class: FetchRequest
This API is used for Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare
environment or the service to your e2e test. When used on [Page] or a [BrowserContext], this API will automatically use
the cookies from the corresponding [BrowserContext]. This means that if you log in using this API, your e2e test
will be logged in and vice versa.
## async method: FetchRequest.fetch
- returns: <[FetchResponse]>
Sends HTTP(S) fetch and returns its response. The method will populate fetch cookies from the context and update
context cookies from the response. The method will automatically follow redirects.
### param: FetchRequest.fetch.urlOrRequest
- `urlOrRequest` <[string]|[Request]>
Target URL or Request to get all fetch parameters from.
### option: FetchRequest.fetch.method
- `method` <[string]>
If set changes the fetch method (e.g. PUT or POST). If not specified, GET method is used.
### option: FetchRequest.fetch.headers
- `headers` <[Object]<[string], [string]>>
Allows to set HTTP headers.
### option: FetchRequest.fetch.data
- `data` <[string]|[Buffer]>
Allows to set post data of the fetch.
### option: FetchRequest.fetch.timeout
- `timeout` <[float]>
Request timeout in milliseconds.
## async method: FetchRequest.get
- returns: <[FetchResponse]>
Sends HTTP(S) GET request and returns its response. The method will populate fetch cookies from the context and update
context cookies from the response. The method will automatically follow redirects.
### param: FetchRequest.get.urlOrRequest
- `urlOrRequest` <[string]|[Request]>
Target URL or Request to get all fetch parameters from.
### option: FetchRequest.get.headers
- `headers` <[Object]<[string], [string]>>
Allows to set HTTP headers.
### option: FetchRequest.get.timeout
- `timeout` <[float]>
Request timeout in milliseconds.
## async method: FetchRequest.post
- returns: <[FetchResponse]>
Sends HTTP(S) fetch and returns its response. The method will populate fetch cookies from the context and update
context cookies from the response. The method will automatically follow redirects.
### param: FetchRequest.post.urlOrRequest
- `urlOrRequest` <[string]|[Request]>
Target URL or Request to get all fetch parameters from.
### option: FetchRequest.post.headers
- `headers` <[Object]<[string], [string]>>
Allows to set HTTP headers.
### option: FetchRequest.post.data
- `data` <[string]|[Buffer]>
Allows to set post data of the fetch.
### option: FetchRequest.post.timeout
- `timeout` <[float]>
Request timeout in milliseconds.

View file

@ -1,6 +1,6 @@
# class: FetchResponse
[FetchResponse] class represents responses received from [`method: BrowserContext.fetch`] and [`method: Page.fetch`] methods.
[FetchResponse] class represents responses received from [`method: FetchRequest.fetch`].
## async method: FetchResponse.body
- returns: <[Buffer]>

View file

@ -1736,37 +1736,6 @@ Name of the function on the window object
Callback function which will be called in Playwright's context.
## async method: Page.fetch
- returns: <[FetchResponse]>
Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
context cookies from the response. The method will automatically follow redirects.
### param: Page.fetch.urlOrRequest
- `urlOrRequest` <[string]|[Request]>
Target URL or Request to get all fetch parameters from.
### option: Page.fetch.method
- `method` <[string]>
If set changes the request method (e.g. PUT or POST). If not specified, GET method is used.
### option: Page.fetch.headers
- `headers` <[Object]<[string], [string]>>
Allows to set HTTP headers.
### option: Page.fetch.postData
- `postData` <[string]|[Buffer]>
Allows to set post data of the request.
### option: Page.fetch.timeout
- `timeout` <[float]>
Request timeout in milliseconds.
## async method: Page.fill
This method waits for an element matching [`param: selector`], waits for [actionability](./actionability.md) checks, focuses the element, fills it and triggers an `input` event after filling. Note that you can pass an empty string to clear the input field.
@ -2440,6 +2409,11 @@ last redirect.
### option: Page.reload.timeout = %%-navigation-timeout-%%
## property: Page.request
- type: <[FetchRequest]>
API testing helper associated with this page. Requests made with this API will use page cookies.
## async method: Page.route
Routing provides the capability to modify network requests that are made by a page.

View file

@ -33,7 +33,8 @@ export { TimeoutError } from '../utils/errors';
export { Frame } from './frame';
export { Keyboard, Mouse, Touchscreen } from './input';
export { JSHandle } from './jsHandle';
export { FetchResponse, Request, Response, Route, WebSocket } from './network';
export { Request, Response, Route, WebSocket } from './network';
export { FetchRequest, FetchResponse } from './fetch';
export { Page } from './page';
export { Selectors } from './selectors';
export { Tracing } from './tracing';

View file

@ -28,7 +28,7 @@ import { Events } from './events';
import { TimeoutSettings } from '../utils/timeoutSettings';
import { Waiter } from './waiter';
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
import { isUnderTest, headersObjectToArray, mkdirIfNeeded, isString, assert } from '../utils/utils';
import { isUnderTest, headersObjectToArray, mkdirIfNeeded } from '../utils/utils';
import { isSafeCloseError } from '../utils/errors';
import * as api from '../../types/types';
import * as structs from '../../types/structs';
@ -36,6 +36,7 @@ import { CDPSession } from './cdpSession';
import { Tracing } from './tracing';
import type { BrowserType } from './browserType';
import { Artifact } from './artifact';
import { FetchRequest } from './fetch';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext {
_pages = new Set<Page>();
@ -48,8 +49,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
private _closedPromise: Promise<void>;
_options: channels.BrowserNewContextParams = { };
readonly request: FetchRequest;
readonly tracing: Tracing;
private _closed = false;
readonly _backgroundPages = new Set<Page>();
readonly _serviceWorkers = new Set<Worker>();
readonly _isChromium: boolean;
@ -68,6 +69,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
this._browser = parent;
this._isChromium = this._browser?._name === 'chromium';
this.tracing = new Tracing(this);
this.request = new FetchRequest(this);
this._channel.on('bindingCall', ({binding}) => this._onBinding(BindingCall.from(binding)));
this._channel.on('close', () => this._onClose());
@ -216,32 +218,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
});
}
async fetch(urlOrRequest: string|api.Request, options: FetchOptions = {}): Promise<network.FetchResponse> {
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
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 method = options.method || request?.method();
// Cannot call allHeaders() here as the request may be paused inside route handler.
const headersObj = options.headers || request?.headers() ;
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
let postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
if (postDataBuffer === undefined)
postDataBuffer = request?.postDataBuffer() || undefined;
const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined);
const result = await channel.fetch({
url,
method,
headers,
postData,
timeout: options.timeout,
});
if (result.error)
throw new Error(`Request failed: ${result.error}`);
return new network.FetchResponse(this, result.response!);
});
}
async setGeolocation(geolocation: { longitude: number, latitude: number, accuracy?: number } | null): Promise<void> {
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await channel.setGeolocation({ geolocation: geolocation || undefined });
@ -351,7 +327,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
}
_onClose() {
this._closed = true;
if (this._browser)
this._browser._contexts.delete(this);
this._browserType?._contexts?.delete(this);
@ -393,8 +368,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
}
}
export type FetchOptions = { method?: string, headers?: Headers, postData?: string | Buffer, timeout?: number };
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
if (options.videoSize && !options.videosPath)
throw new Error(`"videoSize" option requires "videosPath" to be specified`);

150
src/client/fetch.ts Normal file
View file

@ -0,0 +1,150 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 { 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 class FetchRequest implements api.FetchRequest {
private _context: BrowserContext;
constructor(context: BrowserContext) {
this._context = context;
}
async get(
urlOrRequest: string | api.Request,
options?: {
headers?: { [key: string]: string; };
timeout?: number;
}): Promise<FetchResponse> {
return this.fetch(urlOrRequest, {
...options,
method: 'GET',
});
}
async post(
urlOrRequest: string | api.Request,
options?: {
headers?: { [key: string]: string; };
data?: string | Buffer;
timeout?: number;
}): Promise<FetchResponse> {
return this.fetch(urlOrRequest, {
...options,
method: 'POST',
});
}
async fetch(urlOrRequest: string | api.Request, options: FetchOptions = {}): Promise<FetchResponse> {
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
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 method = options.method || request?.method();
// Cannot call allHeaders() here as the request may be paused inside route handler.
const headersObj = options.headers || request?.headers() ;
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
let postDataBuffer = isString(options.data) ? Buffer.from(options.data, 'utf8') : options.data;
if (postDataBuffer === undefined)
postDataBuffer = request?.postDataBuffer() || undefined;
const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined);
const result = await channel.fetch({
url,
method,
headers,
postData,
timeout: options.timeout,
});
if (result.error)
throw new Error(`Request failed: ${result.error}`);
return new FetchResponse(this._context, result.response!);
});
}
}
export class FetchResponse implements api.FetchResponse {
private readonly _initializer: channels.FetchResponse;
private readonly _headers: RawHeaders;
private readonly _context: BrowserContext;
constructor(context: BrowserContext, initializer: channels.FetchResponse) {
this._context = context;
this._initializer = initializer;
this._headers = new RawHeaders(this._initializer.headers);
}
ok(): boolean {
return this._initializer.status === 0 || (this._initializer.status >= 200 && this._initializer.status <= 299);
}
url(): string {
return this._initializer.url;
}
status(): number {
return this._initializer.status;
}
statusText(): string {
return this._initializer.statusText;
}
headers(): Headers {
return this._headers.headers();
}
headersArray(): HeadersArray {
return this._headers.headersArray();
}
async body(): Promise<Buffer> {
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
const result = await channel.fetchResponseBody({ fetchUid: this._fetchUid() });
if (!result.binary)
throw new Error('Response has been disposed');
return Buffer.from(result.binary!, 'base64');
});
}
async text(): Promise<string> {
const content = await this.body();
return content.toString('utf8');
}
async json(): Promise<object> {
const content = await this.text();
return JSON.parse(content);
}
async dispose(): Promise<void> {
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await channel.disposeFetchResponse({ fetchUid: this._fetchUid() });
});
}
_fetchUid(): string {
return this._initializer.fetchUid;
}
}

View file

@ -29,8 +29,8 @@ import { Waiter } from './waiter';
import * as api from '../../types/types';
import { HeadersArray, URLMatch } from '../common/types';
import { urlMatches } from './clientHelper';
import { BrowserContext } from './browserContext';
import { MultiMap } from '../utils/multimap';
import { FetchResponse } from './fetch';
export type NetworkCookie = {
name: string,
@ -325,7 +325,7 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
});
}
async fulfill(options: { response?: api.Response|api.FetchResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string } = {}) {
async fulfill(options: { response?: api.Response | api.FetchResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string } = {}) {
return this._wrapApiCall(async (channel: channels.RouteChannel) => {
let useInterceptedResponseBody;
let fetchResponseUid;
@ -547,71 +547,6 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
}
}
export class FetchResponse implements api.FetchResponse {
private readonly _initializer: channels.FetchResponse;
private readonly _headers: RawHeaders;
private readonly _context: BrowserContext;
constructor(context: BrowserContext, initializer: channels.FetchResponse) {
this._context = context;
this._initializer = initializer;
this._headers = new RawHeaders(this._initializer.headers);
}
ok(): boolean {
return this._initializer.status === 0 || (this._initializer.status >= 200 && this._initializer.status <= 299);
}
url(): string {
return this._initializer.url;
}
status(): number {
return this._initializer.status;
}
statusText(): string {
return this._initializer.statusText;
}
headers(): Headers {
return this._headers.headers();
}
headersArray(): HeadersArray {
return this._headers.headersArray();
}
async body(): Promise<Buffer> {
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
const result = await channel.fetchResponseBody({ fetchUid: this._fetchUid() });
if (!result.binary)
throw new Error('Response has been disposed');
return Buffer.from(result.binary!, 'base64');
});
}
async text(): Promise<string> {
const content = await this.body();
return content.toString('utf8');
}
async json(): Promise<object> {
const content = await this.text();
return JSON.parse(content);
}
async dispose(): Promise<void> {
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await channel.disposeFetchResponse({ fetchUid: this._fetchUid() });
});
}
_fetchUid(): string {
return this._initializer.fetchUid;
}
}
export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> implements api.WebSocket {
private _page: Page;
private _isClosed: boolean;

View file

@ -19,10 +19,9 @@ 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, FetchOptions } from './browserContext';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog';
@ -48,6 +47,7 @@ import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } fro
import { isSafeCloseError } from '../utils/errors';
import { Video } from './video';
import { Artifact } from './artifact';
import { FetchRequest } from './fetch';
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
width?: string | number,
@ -78,6 +78,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
readonly coverage: Coverage;
readonly keyboard: Keyboard;
readonly mouse: Mouse;
readonly request: FetchRequest;
readonly touchscreen: Touchscreen;
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
@ -101,6 +102,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this.accessibility = new Accessibility(this._channel);
this.keyboard = new Keyboard(this);
this.mouse = new Mouse(this);
this.request = new FetchRequest(this._browserContext);
this.touchscreen = new Touchscreen(this);
this._mainFrame = Frame.from(initializer.mainFrame);
@ -443,10 +445,6 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
return this._mainFrame.evaluate(pageFunction, arg);
}
async fetch(urlOrRequest: string|network.Request, options: FetchOptions = {}): Promise<network.FetchResponse> {
return await this._browserContext.fetch(urlOrRequest as any, 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);

View file

@ -41,7 +41,19 @@ it.afterAll(() => {
});
it('should work', async ({context, server}) => {
const response = await context.fetch(server.PREFIX + '/simple.json');
const response = await context.request.get(server.PREFIX + '/simple.json');
expect(response.url()).toBe(server.PREFIX + '/simple.json');
expect(response.status()).toBe(200);
expect(response.statusText()).toBe('OK');
expect(response.ok()).toBeTruthy();
expect(response.url()).toBe(server.PREFIX + '/simple.json');
expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');
expect(response.headersArray()).toContainEqual({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
expect(await response.text()).toBe('{"foo": "bar"}\n');
});
it('fetch should work', async ({context, server}) => {
const response = await context.request.fetch(server.PREFIX + '/simple.json');
expect(response.url()).toBe(server.PREFIX + '/simple.json');
expect(response.status()).toBe(200);
expect(response.statusText()).toBe('OK');
@ -56,8 +68,7 @@ it('should throw on network error', async ({context, server}) => {
server.setRoute('/test', (req, res) => {
req.socket.destroy();
});
let error;
await context.fetch(server.PREFIX + '/test').catch(e => error = e);
const error = await context.request.get(server.PREFIX + '/test').catch(e => e);
expect(error.message).toContain('socket hang up');
});
@ -66,8 +77,7 @@ it('should throw on network error after redirect', async ({context, server}) =>
server.setRoute('/test', (req, res) => {
req.socket.destroy();
});
let error;
await context.fetch(server.PREFIX + '/redirect').catch(e => error = e);
const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e);
expect(error.message).toContain('socket hang up');
});
@ -81,8 +91,7 @@ it('should throw on network error when sending body', async ({context, server})
res.uncork();
req.socket.destroy();
});
let error;
await context.fetch(server.PREFIX + '/test').catch(e => error = e);
const error = await context.request.get(server.PREFIX + '/test').catch(e => e);
expect(error.message).toContain('Error: aborted');
});
@ -97,8 +106,7 @@ it('should throw on network error when sending body after redirect', async ({con
res.uncork();
req.socket.destroy();
});
let error;
await context.fetch(server.PREFIX + '/redirect').catch(e => error = e);
const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e);
expect(error.message).toContain('Error: aborted');
});
@ -115,7 +123,7 @@ it('should add session cookies to request', async ({context, server}) => {
}]);
const [req] = await Promise.all([
server.waitForRequest('/simple.json'),
context.fetch(`http://www.my.playwright.dev:${server.PORT}/simple.json`),
context.request.get(`http://www.my.playwright.dev:${server.PORT}/simple.json`),
]);
expect(req.headers.cookie).toEqual('username=John Doe');
});
@ -133,7 +141,7 @@ it('should not add context cookie if cookie header passed as a parameter', async
}]);
const [req] = await Promise.all([
server.waitForRequest('/empty.html'),
context.fetch(`http://www.my.playwright.dev:${server.PORT}/empty.html`, {
context.request.get(`http://www.my.playwright.dev:${server.PORT}/empty.html`, {
headers: {
'Cookie': 'foo=bar'
}
@ -157,7 +165,7 @@ it('should follow redirects', async ({context, server}) => {
}]);
const [req, response] = await Promise.all([
server.waitForRequest('/simple.json'),
context.fetch(`http://www.my.playwright.dev:${server.PORT}/redirect1`),
context.request.get(`http://www.my.playwright.dev:${server.PORT}/redirect1`),
]);
expect(req.headers.cookie).toEqual('username=John Doe');
expect(response.url()).toBe(`http://www.my.playwright.dev:${server.PORT}/simple.json`);
@ -169,7 +177,7 @@ it('should add cookies from Set-Cookie header', async ({context, page, server})
res.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']);
res.end();
});
await context.fetch(server.PREFIX + '/setcookie.html');
await context.request.get(server.PREFIX + '/setcookie.html');
const cookies = await context.cookies();
expect(new Set(cookies.map(c => ({ name: c.name, value: c.value })))).toEqual(new Set([
{
@ -185,12 +193,12 @@ it('should add cookies from Set-Cookie header', async ({context, page, server})
expect((await page.evaluate(() => document.cookie)).split(';').map(s => s.trim()).sort()).toEqual(['foo=bar', 'session=value']);
});
it('should not lose body while handling Set-Cookie header', async ({context, page, server}) => {
it('should not lose body while handling Set-Cookie header', async ({context, server}) => {
server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']);
res.end('text content');
});
const response = await context.fetch(server.PREFIX + '/setcookie.html');
const response = await context.request.get(server.PREFIX + '/setcookie.html');
expect(await response.text()).toBe('text content');
});
@ -210,7 +218,7 @@ it('should handle cookies on redirects', async ({context, server, browserName, i
server.waitForRequest('/redirect1'),
server.waitForRequest('/a/b/redirect2'),
server.waitForRequest('/title.html'),
context.fetch(`${server.PREFIX}/redirect1`),
context.request.get(`${server.PREFIX}/redirect1`),
]);
expect(req1.headers.cookie).toBeFalsy();
expect(req2.headers.cookie).toBe('r1=v1');
@ -221,7 +229,7 @@ it('should handle cookies on redirects', async ({context, server, browserName, i
server.waitForRequest('/redirect1'),
server.waitForRequest('/a/b/redirect2'),
server.waitForRequest('/title.html'),
context.fetch(`${server.PREFIX}/redirect1`),
context.request.get(`${server.PREFIX}/redirect1`),
]);
expect(req1.headers.cookie).toBe('r1=v1');
expect(req2.headers.cookie.split(';').map(s => s.trim()).sort()).toEqual(['r1=v1', 'r2=v2']);
@ -266,7 +274,7 @@ it('should return raw headers', async ({context, page, server}) => {
conn.uncork();
conn.end();
});
const response = await context.fetch(`${server.PREFIX}/headers`);
const response = await context.request.get(`${server.PREFIX}/headers`);
expect(response.status()).toBe(200);
const headers = response.headersArray().filter(({ name }) => name.toLowerCase().includes('name-'));
expect(headers).toEqual([{ name: 'Name-A', value: 'v1' }, { name: 'name-b', value: 'v4' }, { name: 'Name-a', value: 'v2' }, { name: 'name-A', value: 'v3' }]);
@ -294,7 +302,7 @@ it('should work with context level proxy', async ({browserOptions, browserType,
const [request, response] = await Promise.all([
server.waitForRequest('/target.html'),
context.fetch(`http://non-existent.com/target.html`)
context.request.get(`http://non-existent.com/target.html`)
]);
expect(response.status()).toBe(200);
expect(request.url).toBe('/target.html');
@ -315,7 +323,7 @@ it('should pass proxy credentials', async ({browserType, browserOptions, server,
proxy: { server: `localhost:${proxyServer.PORT}`, username: 'user', password: 'secret' }
});
const context = await browser.newContext();
const response = await context.fetch('http://non-existent.com/simple.json');
const response = await context.request.get('http://non-existent.com/simple.json');
expect(proxyServer.connectHosts).toContain('non-existent.com:80');
expect(auth).toBe('Basic ' + Buffer.from('user:secret').toString('base64'));
expect(await response.json()).toEqual({foo: 'bar'});
@ -327,7 +335,7 @@ it('should work with http credentials', async ({context, server}) => {
const [request, response] = await Promise.all([
server.waitForRequest('/empty.html'),
context.fetch(server.EMPTY_PAGE, {
context.request.get(server.EMPTY_PAGE, {
headers: {
'authorization': 'Basic ' + Buffer.from('user:pass').toString('base64')
}
@ -337,29 +345,28 @@ it('should work with http credentials', async ({context, server}) => {
expect(request.url).toBe('/empty.html');
});
it('should work with setHTTPCredentials', async ({context, browser, server}) => {
it('should work with setHTTPCredentials', async ({context, server}) => {
server.setAuth('/empty.html', 'user', 'pass');
const response1 = await context.fetch(server.EMPTY_PAGE);
const response1 = await context.request.get(server.EMPTY_PAGE);
expect(response1.status()).toBe(401);
await context.setHTTPCredentials({ username: 'user', password: 'pass' });
const response2 = await context.fetch(server.EMPTY_PAGE);
const response2 = await context.request.get(server.EMPTY_PAGE);
expect(response2.status()).toBe(200);
});
it('should return error with wrong credentials', async ({context, browser, server}) => {
it('should return error with wrong credentials', async ({context, server}) => {
server.setAuth('/empty.html', 'user', 'pass');
await context.setHTTPCredentials({ username: 'user', password: 'wrong' });
const response2 = await context.fetch(server.EMPTY_PAGE);
const response2 = await context.request.get(server.EMPTY_PAGE);
expect(response2.status()).toBe(401);
});
it('should support post data', async ({context, server}) => {
const [request, response] = await Promise.all([
server.waitForRequest('/simple.json'),
context.fetch(`${server.PREFIX}/simple.json`, {
method: 'POST',
postData: 'My request'
context.request.post(`${server.PREFIX}/simple.json`, {
data: 'My request'
})
]);
expect(request.method).toBe('POST');
@ -371,7 +378,7 @@ it('should support post data', async ({context, server}) => {
it('should add default headers', async ({context, server, page}) => {
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
context.fetch(server.EMPTY_PAGE)
context.request.get(server.EMPTY_PAGE)
]);
expect(request.headers['accept']).toBe('*/*');
const userAgent = await page.evaluate(() => navigator.userAgent);
@ -383,7 +390,7 @@ it('should add default headers to redirects', async ({context, server, page}) =>
server.setRedirect('/redirect', '/empty.html');
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
context.fetch(`${server.PREFIX}/redirect`)
context.request.get(`${server.PREFIX}/redirect`)
]);
expect(request.headers['accept']).toBe('*/*');
const userAgent = await page.evaluate(() => navigator.userAgent);
@ -394,7 +401,7 @@ it('should add default headers to redirects', async ({context, server, page}) =>
it('should allow to override default headers', async ({context, server, page}) => {
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
context.fetch(server.EMPTY_PAGE, {
context.request.get(server.EMPTY_PAGE, {
headers: {
'User-Agent': 'Playwright',
'Accept': 'text/html',
@ -414,7 +421,7 @@ it('should propagate custom headers with redirects', async ({context, server}) =
server.waitForRequest('/a/redirect1'),
server.waitForRequest('/b/c/redirect2'),
server.waitForRequest('/simple.json'),
context.fetch(`${server.PREFIX}/a/redirect1`, {headers: {'foo': 'bar'}}),
context.request.get(`${server.PREFIX}/a/redirect1`, {headers: {'foo': 'bar'}}),
]);
expect(req1.headers['foo']).toBe('bar');
expect(req2.headers['foo']).toBe('bar');
@ -429,7 +436,7 @@ it('should propagate extra http headers with redirects', async ({context, server
server.waitForRequest('/a/redirect1'),
server.waitForRequest('/b/c/redirect2'),
server.waitForRequest('/simple.json'),
context.fetch(`${server.PREFIX}/a/redirect1`),
context.request.get(`${server.PREFIX}/a/redirect1`),
]);
expect(req1.headers['my-secret']).toBe('Value');
expect(req2.headers['my-secret']).toBe('Value');
@ -437,7 +444,7 @@ it('should propagate extra http headers with redirects', async ({context, server
});
it('should throw on invalid header value', async ({context, server}) => {
const error = await context.fetch(`${server.PREFIX}/a/redirect1`, {
const error = await context.request.get(`${server.PREFIX}/a/redirect1`, {
headers: {
'foo': 'недопустимое значение',
}
@ -446,9 +453,9 @@ it('should throw on invalid header value', async ({context, server}) => {
});
it('should throw on non-http(s) protocol', async ({context}) => {
const error1 = await context.fetch(`data:text/plain,test`).catch(e => e);
const error1 = await context.request.get(`data:text/plain,test`).catch(e => e);
expect(error1.message).toContain('Protocol "data:" not supported');
const error2 = await context.fetch(`file:///tmp/foo`).catch(e => e);
const error2 = await context.request.get(`file:///tmp/foo`).catch(e => e);
expect(error2.message).toContain('Protocol "file:" not supported');
});
@ -458,7 +465,7 @@ it('should support https', async ({context, httpsServer}) => {
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
suppressCertificateWarning();
try {
const response = await context.fetch(httpsServer.EMPTY_PAGE);
const response = await context.request.get(httpsServer.EMPTY_PAGE);
expect(response.status()).toBe(200);
} finally {
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = oldValue;
@ -467,7 +474,7 @@ it('should support https', async ({context, httpsServer}) => {
it('should support ignoreHTTPSErrors', async ({contextFactory, contextOptions, httpsServer}) => {
const context = await contextFactory({ ...contextOptions, ignoreHTTPSErrors: true });
const response = await context.fetch(httpsServer.EMPTY_PAGE);
const response = await context.request.get(httpsServer.EMPTY_PAGE);
expect(response.status()).toBe(200);
});
@ -476,7 +483,7 @@ it('should resolve url relative to baseURL', async function({server, contextFact
...contextOptions,
baseURL: server.PREFIX,
});
const response = await context.fetch('/empty.html');
const response = await context.request.get('/empty.html');
expect(response.url()).toBe(server.EMPTY_PAGE);
});
@ -496,7 +503,7 @@ it('should support gzip compression', async function({context, server}) {
gzip.end();
});
const response = await context.fetch(server.PREFIX + '/compressed');
const response = await context.request.get(server.PREFIX + '/compressed');
expect(await response.text()).toBe('Hello, world!');
});
@ -510,7 +517,7 @@ it('should throw informatibe error on corrupted gzip body', async function({cont
res.end();
});
const error = await context.fetch(server.PREFIX + '/corrupted').catch(e => e);
const error = await context.request.get(server.PREFIX + '/corrupted').catch(e => e);
expect(error.message).toContain(`failed to decompress 'gzip' encoding`);
});
@ -530,7 +537,7 @@ it('should support brotli compression', async function({context, server}) {
brotli.end();
});
const response = await context.fetch(server.PREFIX + '/compressed');
const response = await context.request.get(server.PREFIX + '/compressed');
expect(await response.text()).toBe('Hello, world!');
});
@ -544,7 +551,7 @@ it('should throw informatibe error on corrupted brotli body', async function({co
res.end();
});
const error = await context.fetch(server.PREFIX + '/corrupted').catch(e => e);
const error = await context.request.get(server.PREFIX + '/corrupted').catch(e => e);
expect(error.message).toContain(`failed to decompress 'br' encoding`);
});
@ -564,7 +571,7 @@ it('should support deflate compression', async function({context, server}) {
deflate.end();
});
const response = await context.fetch(server.PREFIX + '/compressed');
const response = await context.request.get(server.PREFIX + '/compressed');
expect(await response.text()).toBe('Hello, world!');
});
@ -578,7 +585,7 @@ it('should throw informatibe error on corrupted deflate body', async function({c
res.end();
});
const error = await context.fetch(server.PREFIX + '/corrupted').catch(e => e);
const error = await context.request.get(server.PREFIX + '/corrupted').catch(e => e);
expect(error.message).toContain(`failed to decompress 'deflate' encoding`);
});
@ -590,7 +597,7 @@ it('should support timeout option', async function({context, server}) {
});
});
const error = await context.fetch(server.PREFIX + '/slow', { timeout: 10 }).catch(e => e);
const error = await context.request.get(server.PREFIX + '/slow', { timeout: 10 }).catch(e => e);
expect(error.message).toContain(`Request timed out after 10ms`);
});
@ -604,12 +611,12 @@ it('should respect timeout after redirects', async function({context, server}) {
});
context.setDefaultTimeout(100);
const error = await context.fetch(server.PREFIX + '/redirect').catch(e => e);
const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e);
expect(error.message).toContain(`Request timed out after 100ms`);
});
it('should dispose', async function({context, server}) {
const response = await context.fetch(server.PREFIX + '/simple.json');
const response = await context.request.get(server.PREFIX + '/simple.json');
expect(await response.json()).toEqual({ foo: 'bar' });
await response.dispose();
const error = await response.body().catch(e => e);
@ -617,7 +624,7 @@ it('should dispose', async function({context, server}) {
});
it('should dispose when context closes', async function({context, server}) {
const response = await context.fetch(server.PREFIX + '/simple.json');
const response = await context.request.get(server.PREFIX + '/simple.json');
expect(await response.json()).toEqual({ foo: 'bar' });
await context.close();
const error = await response.body().catch(e => e);
@ -625,7 +632,7 @@ it('should dispose when context closes', async function({context, server}) {
});
it('should throw on invalid first argument', async function({context}) {
const error = await context.fetch({} as any).catch(e => e);
const error = await context.request.get({} as any).catch(e => e);
expect(error.message).toContain('First argument must be either URL string or Request');
});
@ -636,12 +643,11 @@ it('should override request parameters', async function({context, page, server})
]);
const [req] = await Promise.all([
server.waitForRequest('/empty.html'),
context.fetch(pageReq, {
method: 'POST',
context.request.post(pageReq, {
headers: {
'foo': 'bar'
},
postData: 'data'
data: 'data'
})
]);
expect(req.method).toBe('POST');

View file

@ -197,7 +197,7 @@ it('should include the origin header', async ({page, server, isAndroid}) => {
it('should fulfill with fetch result', async ({page, server, isElectron}) => {
it.fixme(isElectron, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
const response = await page.fetch(server.PREFIX + '/simple.json');
const response = await page.request.get(server.PREFIX + '/simple.json');
route.fulfill({ response });
});
const response = await page.goto(server.EMPTY_PAGE);
@ -208,7 +208,7 @@ it('should fulfill with fetch result', async ({page, server, isElectron}) => {
it('should fulfill with fetch result and overrides', async ({page, server, isElectron}) => {
it.fixme(isElectron, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
const response = await page.fetch(server.PREFIX + '/simple.json');
const response = await page.request.get(server.PREFIX + '/simple.json');
route.fulfill({
response,
status: 201,
@ -226,7 +226,7 @@ it('should fulfill with fetch result and overrides', async ({page, server, isEle
it('should fetch original request and fulfill', async ({page, server, isElectron}) => {
it.fixme(isElectron, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
const response = await page.fetch(route.request());
const response = await page.request.get(route.request());
route.fulfill({
response,
});

146
types/types.d.ts vendored
View file

@ -1995,34 +1995,6 @@ export interface Page {
*/
exposeFunction(name: string, callback: Function): Promise<void>;
/**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
* @param urlOrRequest Target URL or Request to get all fetch parameters from.
* @param options
*/
fetch(urlOrRequest: string|Request, options?: {
/**
* Allows to set HTTP headers.
*/
headers?: { [key: string]: string; };
/**
* If set changes the request method (e.g. PUT or POST). If not specified, GET method is used.
*/
method?: string;
/**
* Allows to set post data of the request.
*/
postData?: string|Buffer;
/**
* Request timeout in milliseconds.
*/
timeout?: number;
}): Promise<FetchResponse>;
/**
* This method waits for an element matching `selector`, waits for [actionability](https://playwright.dev/docs/actionability) checks, focuses the
* element, fills it and triggers an `input` event after filling. Note that you can pass an empty string to clear the input
@ -2777,6 +2749,11 @@ export interface Page {
waitUntil?: "load"|"domcontentloaded"|"networkidle";
}): Promise<null|Response>;
/**
* API testing helper associated with this page. Requests made with this API will use page cookies.
*/
request: FetchRequest;
/**
* Routing provides the capability to modify network requests that are made by a page.
*
@ -6422,34 +6399,6 @@ export interface BrowserContext {
*/
exposeFunction(name: string, callback: Function): Promise<void>;
/**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
* @param urlOrRequest Target URL or Request to get all fetch parameters from.
* @param options
*/
fetch(urlOrRequest: string|Request, options?: {
/**
* Allows to set HTTP headers.
*/
headers?: { [key: string]: string; };
/**
* If set changes the request method (e.g. PUT or POST). If not specified, GET method is used.
*/
method?: string;
/**
* Allows to set post data of the request.
*/
postData?: string|Buffer;
/**
* Request timeout in milliseconds.
*/
timeout?: number;
}): Promise<FetchResponse>;
/**
* Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
* specified.
@ -6496,6 +6445,11 @@ export interface BrowserContext {
*/
pages(): Array<Page>;
/**
* API testing helper associated with this context. Requests made with this API will use context cookies.
*/
request: FetchRequest;
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
@ -12663,10 +12617,86 @@ export interface Electron {
}): Promise<ElectronApplication>;
}
/**
* This API is used for Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare
* environment or the service to your e2e test. When used on [Page] or a [BrowserContext], this API will automatically use
* the cookies from the corresponding [BrowserContext]. This means that if you log in using this API, your e2e test will be
* logged in and vice versa.
*/
export interface FetchRequest {
/**
* Sends HTTP(S) fetch and returns its response. The method will populate fetch cookies from the context and update context
* cookies from the response. The method will automatically follow redirects.
* @param urlOrRequest Target URL or Request to get all fetch parameters from.
* @param options
*/
fetch(urlOrRequest: string|Request, options?: {
/**
* Allows to set post data of the fetch.
*/
data?: string|Buffer;
/**
* Allows to set HTTP headers.
*/
headers?: { [key: string]: string; };
/**
* If set changes the fetch method (e.g. PUT or POST). If not specified, GET method is used.
*/
method?: string;
/**
* Request timeout in milliseconds.
*/
timeout?: number;
}): Promise<FetchResponse>;
/**
* Sends HTTP(S) GET request and returns its response. The method will populate fetch cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
* @param urlOrRequest Target URL or Request to get all fetch parameters from.
* @param options
*/
get(urlOrRequest: string|Request, options?: {
/**
* Allows to set HTTP headers.
*/
headers?: { [key: string]: string; };
/**
* Request timeout in milliseconds.
*/
timeout?: number;
}): Promise<FetchResponse>;
/**
* Sends HTTP(S) fetch and returns its response. The method will populate fetch cookies from the context and update context
* cookies from the response. The method will automatically follow redirects.
* @param urlOrRequest Target URL or Request to get all fetch parameters from.
* @param options
*/
post(urlOrRequest: string|Request, options?: {
/**
* Allows to set post data of the fetch.
*/
data?: string|Buffer;
/**
* Allows to set HTTP headers.
*/
headers?: { [key: string]: string; };
/**
* Request timeout in milliseconds.
*/
timeout?: number;
}): Promise<FetchResponse>;
}
/**
* [FetchResponse] class represents responses received from
* [browserContext.fetch(urlOrRequest[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-fetch)
* and [page.fetch(urlOrRequest[, options])](https://playwright.dev/docs/api/class-page#page-fetch) methods.
* [fetchRequest.fetch(urlOrRequest[, options])](https://playwright.dev/docs/api/class-fetchrequest#fetch-request-fetch).
*/
export interface FetchResponse {
/**