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. 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 ## async method: BrowserContext.grantPermissions
Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if 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. 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 ## async method: BrowserContext.route
Routing provides the capability to modify network requests that are made by any page in the browser context. Once 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 # 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 ## async method: FetchResponse.body
- returns: <[Buffer]> - 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. 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 ## 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. 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-%% ### 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 ## async method: Page.route
Routing provides the capability to modify network requests that are made by a page. 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 { Frame } from './frame';
export { Keyboard, Mouse, Touchscreen } from './input'; export { Keyboard, Mouse, Touchscreen } from './input';
export { JSHandle } from './jsHandle'; 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 { Page } from './page';
export { Selectors } from './selectors'; export { Selectors } from './selectors';
export { Tracing } from './tracing'; export { Tracing } from './tracing';

View file

@ -28,7 +28,7 @@ import { Events } from './events';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import { Waiter } from './waiter'; import { Waiter } from './waiter';
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types'; 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 { isSafeCloseError } from '../utils/errors';
import * as api from '../../types/types'; import * as api from '../../types/types';
import * as structs from '../../types/structs'; import * as structs from '../../types/structs';
@ -36,6 +36,7 @@ import { CDPSession } from './cdpSession';
import { Tracing } from './tracing'; import { Tracing } from './tracing';
import type { BrowserType } from './browserType'; import type { BrowserType } from './browserType';
import { Artifact } from './artifact'; import { Artifact } from './artifact';
import { FetchRequest } from './fetch';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext { export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext {
_pages = new Set<Page>(); _pages = new Set<Page>();
@ -48,8 +49,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
private _closedPromise: Promise<void>; private _closedPromise: Promise<void>;
_options: channels.BrowserNewContextParams = { }; _options: channels.BrowserNewContextParams = { };
readonly request: FetchRequest;
readonly tracing: Tracing; readonly tracing: Tracing;
private _closed = false;
readonly _backgroundPages = new Set<Page>(); readonly _backgroundPages = new Set<Page>();
readonly _serviceWorkers = new Set<Worker>(); readonly _serviceWorkers = new Set<Worker>();
readonly _isChromium: boolean; readonly _isChromium: boolean;
@ -68,6 +69,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
this._browser = parent; this._browser = parent;
this._isChromium = this._browser?._name === 'chromium'; this._isChromium = this._browser?._name === 'chromium';
this.tracing = new Tracing(this); this.tracing = new Tracing(this);
this.request = new FetchRequest(this);
this._channel.on('bindingCall', ({binding}) => this._onBinding(BindingCall.from(binding))); this._channel.on('bindingCall', ({binding}) => this._onBinding(BindingCall.from(binding)));
this._channel.on('close', () => this._onClose()); 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> { async setGeolocation(geolocation: { longitude: number, latitude: number, accuracy?: number } | null): Promise<void> {
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await channel.setGeolocation({ geolocation: geolocation || undefined }); await channel.setGeolocation({ geolocation: geolocation || undefined });
@ -351,7 +327,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
} }
_onClose() { _onClose() {
this._closed = true;
if (this._browser) if (this._browser)
this._browser._contexts.delete(this); this._browser._contexts.delete(this);
this._browserType?._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> { export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
if (options.videoSize && !options.videosPath) if (options.videoSize && !options.videosPath)
throw new Error(`"videoSize" option requires "videosPath" to be specified`); 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 * as api from '../../types/types';
import { HeadersArray, URLMatch } from '../common/types'; import { HeadersArray, URLMatch } from '../common/types';
import { urlMatches } from './clientHelper'; import { urlMatches } from './clientHelper';
import { BrowserContext } from './browserContext';
import { MultiMap } from '../utils/multimap'; import { MultiMap } from '../utils/multimap';
import { FetchResponse } from './fetch';
export type NetworkCookie = { export type NetworkCookie = {
name: string, 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) => { return this._wrapApiCall(async (channel: channels.RouteChannel) => {
let useInterceptedResponseBody; let useInterceptedResponseBody;
let fetchResponseUid; 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 { export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> implements api.WebSocket {
private _page: Page; private _page: Page;
private _isClosed: boolean; private _isClosed: boolean;

View file

@ -19,10 +19,9 @@ import { Events } from './events';
import { assert } from '../utils/utils'; import { assert } from '../utils/utils';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import * as network from './network';
import { parseError, serializeError } from '../protocol/serializers'; import { parseError, serializeError } from '../protocol/serializers';
import { Accessibility } from './accessibility'; import { Accessibility } from './accessibility';
import { BrowserContext, FetchOptions } from './browserContext'; import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { ConsoleMessage } from './consoleMessage'; import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog'; import { Dialog } from './dialog';
@ -48,6 +47,7 @@ import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } fro
import { isSafeCloseError } from '../utils/errors'; import { isSafeCloseError } from '../utils/errors';
import { Video } from './video'; import { Video } from './video';
import { Artifact } from './artifact'; import { Artifact } from './artifact';
import { FetchRequest } from './fetch';
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & { type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
width?: string | number, width?: string | number,
@ -78,6 +78,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
readonly coverage: Coverage; readonly coverage: Coverage;
readonly keyboard: Keyboard; readonly keyboard: Keyboard;
readonly mouse: Mouse; readonly mouse: Mouse;
readonly request: FetchRequest;
readonly touchscreen: Touchscreen; readonly touchscreen: Touchscreen;
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>(); 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.accessibility = new Accessibility(this._channel);
this.keyboard = new Keyboard(this); this.keyboard = new Keyboard(this);
this.mouse = new Mouse(this); this.mouse = new Mouse(this);
this.request = new FetchRequest(this._browserContext);
this.touchscreen = new Touchscreen(this); this.touchscreen = new Touchscreen(this);
this._mainFrame = Frame.from(initializer.mainFrame); 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); 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) { async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
return this._wrapApiCall(async (channel: channels.PageChannel) => { return this._wrapApiCall(async (channel: channels.PageChannel) => {
const source = await evaluationScript(script, arg); const source = await evaluationScript(script, arg);

View file

@ -41,7 +41,19 @@ it.afterAll(() => {
}); });
it('should work', async ({context, server}) => { 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.url()).toBe(server.PREFIX + '/simple.json');
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
expect(response.statusText()).toBe('OK'); expect(response.statusText()).toBe('OK');
@ -56,8 +68,7 @@ it('should throw on network error', async ({context, server}) => {
server.setRoute('/test', (req, res) => { server.setRoute('/test', (req, res) => {
req.socket.destroy(); req.socket.destroy();
}); });
let error; const error = await context.request.get(server.PREFIX + '/test').catch(e => e);
await context.fetch(server.PREFIX + '/test').catch(e => error = e);
expect(error.message).toContain('socket hang up'); 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) => { server.setRoute('/test', (req, res) => {
req.socket.destroy(); req.socket.destroy();
}); });
let error; const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e);
await context.fetch(server.PREFIX + '/redirect').catch(e => error = e);
expect(error.message).toContain('socket hang up'); 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(); res.uncork();
req.socket.destroy(); req.socket.destroy();
}); });
let error; const error = await context.request.get(server.PREFIX + '/test').catch(e => e);
await context.fetch(server.PREFIX + '/test').catch(e => error = e);
expect(error.message).toContain('Error: aborted'); 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(); res.uncork();
req.socket.destroy(); req.socket.destroy();
}); });
let error; const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e);
await context.fetch(server.PREFIX + '/redirect').catch(e => error = e);
expect(error.message).toContain('Error: aborted'); 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([ const [req] = await Promise.all([
server.waitForRequest('/simple.json'), 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'); 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([ const [req] = await Promise.all([
server.waitForRequest('/empty.html'), 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: { headers: {
'Cookie': 'foo=bar' 'Cookie': 'foo=bar'
} }
@ -157,7 +165,7 @@ it('should follow redirects', async ({context, server}) => {
}]); }]);
const [req, response] = await Promise.all([ const [req, response] = await Promise.all([
server.waitForRequest('/simple.json'), 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(req.headers.cookie).toEqual('username=John Doe');
expect(response.url()).toBe(`http://www.my.playwright.dev:${server.PORT}/simple.json`); 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.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']);
res.end(); res.end();
}); });
await context.fetch(server.PREFIX + '/setcookie.html'); await context.request.get(server.PREFIX + '/setcookie.html');
const cookies = await context.cookies(); const cookies = await context.cookies();
expect(new Set(cookies.map(c => ({ name: c.name, value: c.value })))).toEqual(new Set([ 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']); 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) => { server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']); res.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']);
res.end('text content'); 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'); 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('/redirect1'),
server.waitForRequest('/a/b/redirect2'), server.waitForRequest('/a/b/redirect2'),
server.waitForRequest('/title.html'), server.waitForRequest('/title.html'),
context.fetch(`${server.PREFIX}/redirect1`), context.request.get(`${server.PREFIX}/redirect1`),
]); ]);
expect(req1.headers.cookie).toBeFalsy(); expect(req1.headers.cookie).toBeFalsy();
expect(req2.headers.cookie).toBe('r1=v1'); 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('/redirect1'),
server.waitForRequest('/a/b/redirect2'), server.waitForRequest('/a/b/redirect2'),
server.waitForRequest('/title.html'), server.waitForRequest('/title.html'),
context.fetch(`${server.PREFIX}/redirect1`), context.request.get(`${server.PREFIX}/redirect1`),
]); ]);
expect(req1.headers.cookie).toBe('r1=v1'); expect(req1.headers.cookie).toBe('r1=v1');
expect(req2.headers.cookie.split(';').map(s => s.trim()).sort()).toEqual(['r1=v1', 'r2=v2']); 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.uncork();
conn.end(); conn.end();
}); });
const response = await context.fetch(`${server.PREFIX}/headers`); const response = await context.request.get(`${server.PREFIX}/headers`);
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
const headers = response.headersArray().filter(({ name }) => name.toLowerCase().includes('name-')); 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' }]); 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([ const [request, response] = await Promise.all([
server.waitForRequest('/target.html'), 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(response.status()).toBe(200);
expect(request.url).toBe('/target.html'); 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' } proxy: { server: `localhost:${proxyServer.PORT}`, username: 'user', password: 'secret' }
}); });
const context = await browser.newContext(); 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(proxyServer.connectHosts).toContain('non-existent.com:80');
expect(auth).toBe('Basic ' + Buffer.from('user:secret').toString('base64')); expect(auth).toBe('Basic ' + Buffer.from('user:secret').toString('base64'));
expect(await response.json()).toEqual({foo: 'bar'}); 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([ const [request, response] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),
context.fetch(server.EMPTY_PAGE, { context.request.get(server.EMPTY_PAGE, {
headers: { headers: {
'authorization': 'Basic ' + Buffer.from('user:pass').toString('base64') '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'); 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'); 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); expect(response1.status()).toBe(401);
await context.setHTTPCredentials({ username: 'user', password: 'pass' }); 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); 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'); server.setAuth('/empty.html', 'user', 'pass');
await context.setHTTPCredentials({ username: 'user', password: 'wrong' }); 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); expect(response2.status()).toBe(401);
}); });
it('should support post data', async ({context, server}) => { it('should support post data', async ({context, server}) => {
const [request, response] = await Promise.all([ const [request, response] = await Promise.all([
server.waitForRequest('/simple.json'), server.waitForRequest('/simple.json'),
context.fetch(`${server.PREFIX}/simple.json`, { context.request.post(`${server.PREFIX}/simple.json`, {
method: 'POST', data: 'My request'
postData: 'My request'
}) })
]); ]);
expect(request.method).toBe('POST'); 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}) => { it('should add default headers', async ({context, server, page}) => {
const [request] = await Promise.all([ const [request] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),
context.fetch(server.EMPTY_PAGE) context.request.get(server.EMPTY_PAGE)
]); ]);
expect(request.headers['accept']).toBe('*/*'); expect(request.headers['accept']).toBe('*/*');
const userAgent = await page.evaluate(() => navigator.userAgent); 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'); server.setRedirect('/redirect', '/empty.html');
const [request] = await Promise.all([ const [request] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),
context.fetch(`${server.PREFIX}/redirect`) context.request.get(`${server.PREFIX}/redirect`)
]); ]);
expect(request.headers['accept']).toBe('*/*'); expect(request.headers['accept']).toBe('*/*');
const userAgent = await page.evaluate(() => navigator.userAgent); 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}) => { it('should allow to override default headers', async ({context, server, page}) => {
const [request] = await Promise.all([ const [request] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),
context.fetch(server.EMPTY_PAGE, { context.request.get(server.EMPTY_PAGE, {
headers: { headers: {
'User-Agent': 'Playwright', 'User-Agent': 'Playwright',
'Accept': 'text/html', 'Accept': 'text/html',
@ -414,7 +421,7 @@ it('should propagate custom headers with redirects', async ({context, server}) =
server.waitForRequest('/a/redirect1'), server.waitForRequest('/a/redirect1'),
server.waitForRequest('/b/c/redirect2'), server.waitForRequest('/b/c/redirect2'),
server.waitForRequest('/simple.json'), 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(req1.headers['foo']).toBe('bar');
expect(req2.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('/a/redirect1'),
server.waitForRequest('/b/c/redirect2'), server.waitForRequest('/b/c/redirect2'),
server.waitForRequest('/simple.json'), 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(req1.headers['my-secret']).toBe('Value');
expect(req2.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}) => { 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: { headers: {
'foo': 'недопустимое значение', '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}) => { 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'); 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'); 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'; process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
suppressCertificateWarning(); suppressCertificateWarning();
try { try {
const response = await context.fetch(httpsServer.EMPTY_PAGE); const response = await context.request.get(httpsServer.EMPTY_PAGE);
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
} finally { } finally {
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = oldValue; 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}) => { it('should support ignoreHTTPSErrors', async ({contextFactory, contextOptions, httpsServer}) => {
const context = await contextFactory({ ...contextOptions, ignoreHTTPSErrors: true }); 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); expect(response.status()).toBe(200);
}); });
@ -476,7 +483,7 @@ it('should resolve url relative to baseURL', async function({server, contextFact
...contextOptions, ...contextOptions,
baseURL: server.PREFIX, 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); expect(response.url()).toBe(server.EMPTY_PAGE);
}); });
@ -496,7 +503,7 @@ it('should support gzip compression', async function({context, server}) {
gzip.end(); 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!'); 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(); 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`); expect(error.message).toContain(`failed to decompress 'gzip' encoding`);
}); });
@ -530,7 +537,7 @@ it('should support brotli compression', async function({context, server}) {
brotli.end(); 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!'); 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(); 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`); expect(error.message).toContain(`failed to decompress 'br' encoding`);
}); });
@ -564,7 +571,7 @@ it('should support deflate compression', async function({context, server}) {
deflate.end(); 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!'); 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(); 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`); 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`); 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); 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`); expect(error.message).toContain(`Request timed out after 100ms`);
}); });
it('should dispose', async function({context, server}) { 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' }); expect(await response.json()).toEqual({ foo: 'bar' });
await response.dispose(); await response.dispose();
const error = await response.body().catch(e => e); 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}) { 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' }); expect(await response.json()).toEqual({ foo: 'bar' });
await context.close(); await context.close();
const error = await response.body().catch(e => e); 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}) { 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'); 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([ const [req] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),
context.fetch(pageReq, { context.request.post(pageReq, {
method: 'POST',
headers: { headers: {
'foo': 'bar' 'foo': 'bar'
}, },
postData: 'data' data: 'data'
}) })
]); ]);
expect(req.method).toBe('POST'); 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('should fulfill with fetch result', async ({page, server, isElectron}) => {
it.fixme(isElectron, 'error: Browser context management is not supported.'); it.fixme(isElectron, 'error: Browser context management is not supported.');
await page.route('**/*', async route => { 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 }); route.fulfill({ response });
}); });
const response = await page.goto(server.EMPTY_PAGE); 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('should fulfill with fetch result and overrides', async ({page, server, isElectron}) => {
it.fixme(isElectron, 'error: Browser context management is not supported.'); it.fixme(isElectron, 'error: Browser context management is not supported.');
await page.route('**/*', async route => { 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({ route.fulfill({
response, response,
status: 201, 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('should fetch original request and fulfill', async ({page, server, isElectron}) => {
it.fixme(isElectron, 'error: Browser context management is not supported.'); it.fixme(isElectron, 'error: Browser context management is not supported.');
await page.route('**/*', async route => { await page.route('**/*', async route => {
const response = await page.fetch(route.request()); const response = await page.request.get(route.request());
route.fulfill({ route.fulfill({
response, response,
}); });

146
types/types.d.ts vendored
View file

@ -1995,34 +1995,6 @@ export interface Page {
*/ */
exposeFunction(name: string, callback: Function): Promise<void>; 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 * 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 * 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"; waitUntil?: "load"|"domcontentloaded"|"networkidle";
}): Promise<null|Response>; }): 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. * 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>; 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 * Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
* specified. * specified.
@ -6496,6 +6445,11 @@ export interface BrowserContext {
*/ */
pages(): Array<Page>; 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 * 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. * 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>; }): 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 * [FetchResponse] class represents responses received from
* [browserContext.fetch(urlOrRequest[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-fetch) * [fetchRequest.fetch(urlOrRequest[, options])](https://playwright.dev/docs/api/class-fetchrequest#fetch-request-fetch).
* and [page.fetch(urlOrRequest[, options])](https://playwright.dev/docs/api/class-page#page-fetch) methods.
*/ */
export interface FetchResponse { export interface FetchResponse {
/** /**