feat(fetch): make fetch api public (#8853)

This commit is contained in:
Yury Semikhatsky 2021-09-10 18:36:55 -07:00 committed by GitHub
parent 8d6bcfb66c
commit b6180055df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 339 additions and 116 deletions

View file

@ -792,6 +792,37 @@ 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

View file

@ -0,0 +1,74 @@
# class: FetchResponse
[FetchResponse] class represents responses received from [`method: BrowserContext.fetch`] and [`method: Page.fetch`] methods.
## async method: FetchResponse.body
- returns: <[Buffer]>
Returns the buffer with response body.
## async method: FetchResponse.dispose
Disposes the body of this response. If not called then the body will stay in memory until the context closes.
## method: FetchResponse.headers
- returns: <[Object]<[string], [string]>>
An object with all the response HTTP headers associated with this response.
## method: FetchResponse.headersArray
* langs: js, csharp, python
- returns: <[Array]<[Array]<[string]>>>
An array with all the request HTTP headers associated with this response. Header names are not lower-cased.
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
## method: FetchResponse.headersArray
* langs: java
- returns: <[Array]<[Object]>>
- `name` <[string]> Name of the header.
- `value` <[string]> Value of the header.
An array with all the request HTTP headers associated with this response. Header names are not lower-cased.
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
## async method: FetchResponse.json
* langs: js, python
- returns: <[Serializable]>
Returns the JSON representation of response body.
This method will throw if the response body is not parsable via `JSON.parse`.
## async method: FetchResponse.json
* langs: csharp
- returns: <[null]|[JsonElement]>
Returns the JSON representation of response body.
This method will throw if the response body is not parsable via `JSON.parse`.
## method: FetchResponse.ok
- returns: <[boolean]>
Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
## method: FetchResponse.status
- returns: <[int]>
Contains the status code of the response (e.g., 200 for a success).
## method: FetchResponse.statusText
- returns: <[string]>
Contains the status text of the response (e.g. usually an "OK" for a success).
## async method: FetchResponse.text
- returns: <[string]>
Returns the text representation of response body.
## method: FetchResponse.url
- returns: <[string]>
Contains the URL of the response.

View file

@ -1736,6 +1736,37 @@ 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.

View file

@ -220,6 +220,11 @@ Optional response body as raw bytes.
File path to respond with. The content type will be inferred from file extension. If `path` is a relative path, then it File path to respond with. The content type will be inferred from file extension. If `path` is a relative path, then it
is resolved relative to the current working directory. is resolved relative to the current working directory.
### option: Route.fulfill.response
- `response` <[FetchResponse]>
[FetchResponse] to fulfill route's request with. Individual fields of the response (such as headers) can be overridden using fulfill options.
## method: Route.request ## method: Route.request
- returns: <[Request]> - returns: <[Request]>

View file

@ -33,7 +33,7 @@ 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 { Request, Response, Route, WebSocket } from './network'; export { FetchResponse, Request, Response, Route, WebSocket } from './network';
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

@ -216,20 +216,18 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
}); });
} }
async _fetch(request: network.Request, options?: { timeout?: number }): Promise<network.FetchResponse>; async fetch(urlOrRequest: string|api.Request, options: FetchOptions = {}): Promise<network.FetchResponse> {
async _fetch(url: string, options?: FetchOptions): Promise<network.FetchResponse>;
async _fetch(urlOrRequest: string|network.Request, options: FetchOptions = {}): Promise<network.FetchResponse> {
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
const request: network.Request | undefined = (urlOrRequest instanceof network.Request) ? urlOrRequest as network.Request : undefined; const request: network.Request | undefined = (urlOrRequest instanceof network.Request) ? urlOrRequest as network.Request : undefined;
assert(request || typeof urlOrRequest === 'string', 'First argument must be either URL string or Request'); assert(request || typeof urlOrRequest === 'string', 'First argument must be either URL string or Request');
const url = request ? request.url() : urlOrRequest as string; const url = request ? request.url() : urlOrRequest as string;
const method = request?.method() || options.method; const method = options.method || request?.method();
// Cannot call allHeaders() here as the request may be paused inside route handler. // Cannot call allHeaders() here as the request may be paused inside route handler.
const headersObj = request?.headers() || options.headers; const headersObj = options.headers || request?.headers() ;
const headers = headersObj ? headersObjectToArray(headersObj) : undefined; const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
let postDataBuffer = request?.postDataBuffer(); let postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
if (postDataBuffer === undefined) if (postDataBuffer === undefined)
postDataBuffer = (isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData); postDataBuffer = request?.postDataBuffer() || undefined;
const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined); const postData = (postDataBuffer ? postDataBuffer.toString('base64') : undefined);
const result = await channel.fetch({ const result = await channel.fetch({
url, url,
@ -395,7 +393,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
} }
} }
export type FetchOptions = { url?: string, method?: string, headers?: Headers, postData?: string | Buffer, timeout?: number }; 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)

View file

@ -310,7 +310,7 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
}); });
} }
async fulfill(options: { response?: Response|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;
@ -524,7 +524,7 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
} }
} }
export class FetchResponse { export class FetchResponse implements api.FetchResponse {
private readonly _initializer: channels.FetchResponse; private readonly _initializer: channels.FetchResponse;
private readonly _headers: Headers; private readonly _headers: Headers;
private readonly _context: BrowserContext; private readonly _context: BrowserContext;

View file

@ -443,10 +443,8 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
return this._mainFrame.evaluate(pageFunction, arg); return this._mainFrame.evaluate(pageFunction, arg);
} }
async _fetch(request: network.Request, options?: { timeout?: number }): Promise<network.FetchResponse>; async fetch(urlOrRequest: string|network.Request, options: FetchOptions = {}): Promise<network.FetchResponse> {
async _fetch(url: string, options?: FetchOptions): Promise<network.FetchResponse>; return await this._browserContext.fetch(urlOrRequest as any, options);
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) {

View file

@ -41,8 +41,7 @@ it.afterAll(() => {
}); });
it('should work', async ({context, server}) => { it('should work', async ({context, server}) => {
// @ts-expect-error const response = await context.fetch(server.PREFIX + '/simple.json');
const response = await context._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');
@ -58,8 +57,7 @@ it('should throw on network error', async ({context, server}) => {
req.socket.destroy(); req.socket.destroy();
}); });
let error; let error;
// @ts-expect-error await context.fetch(server.PREFIX + '/test').catch(e => error = 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');
}); });
@ -69,8 +67,7 @@ it('should throw on network error after redirect', async ({context, server}) =>
req.socket.destroy(); req.socket.destroy();
}); });
let error; let error;
// @ts-expect-error await context.fetch(server.PREFIX + '/redirect').catch(e => error = 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');
}); });
@ -85,8 +82,7 @@ it('should throw on network error when sending body', async ({context, server})
req.socket.destroy(); req.socket.destroy();
}); });
let error; let error;
// @ts-expect-error await context.fetch(server.PREFIX + '/test').catch(e => error = e);
await context._fetch(server.PREFIX + '/test').catch(e => error = e);
expect(error.message).toContain('Error: aborted'); expect(error.message).toContain('Error: aborted');
}); });
@ -102,8 +98,7 @@ it('should throw on network error when sending body after redirect', async ({con
req.socket.destroy(); req.socket.destroy();
}); });
let error; let error;
// @ts-expect-error await context.fetch(server.PREFIX + '/redirect').catch(e => error = e);
await context._fetch(server.PREFIX + '/redirect').catch(e => error = e);
expect(error.message).toContain('Error: aborted'); expect(error.message).toContain('Error: aborted');
}); });
@ -120,8 +115,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'),
// @ts-expect-error context.fetch(`http://www.my.playwright.dev:${server.PORT}/simple.json`),
context._fetch(`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');
}); });
@ -139,8 +133,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'),
// @ts-expect-error context.fetch(`http://www.my.playwright.dev:${server.PORT}/empty.html`, {
context._fetch(`http://www.my.playwright.dev:${server.PORT}/empty.html`, {
headers: { headers: {
'Cookie': 'foo=bar' 'Cookie': 'foo=bar'
} }
@ -164,8 +157,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'),
// @ts-expect-error context.fetch(`http://www.my.playwright.dev:${server.PORT}/redirect1`),
context._fetch(`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`);
@ -177,8 +169,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();
}); });
// @ts-expect-error await context.fetch(server.PREFIX + '/setcookie.html');
await context._fetch(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([
{ {
@ -199,8 +190,7 @@ it('should not lose body while handling Set-Cookie header', async ({context, pag
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');
}); });
// @ts-expect-error const response = await context.fetch(server.PREFIX + '/setcookie.html');
const response = await context._fetch(server.PREFIX + '/setcookie.html');
expect(await response.text()).toBe('text content'); expect(await response.text()).toBe('text content');
}); });
@ -220,8 +210,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'),
// @ts-expect-error context.fetch(`${server.PREFIX}/redirect1`),
context._fetch(`${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');
@ -232,8 +221,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'),
// @ts-expect-error context.fetch(`${server.PREFIX}/redirect1`),
context._fetch(`${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']);
@ -278,8 +266,7 @@ it('should return raw headers', async ({context, page, server}) => {
conn.uncork(); conn.uncork();
conn.end(); conn.end();
}); });
// @ts-expect-error const response = await context.fetch(`${server.PREFIX}/headers`);
const response = await context._fetch(`${server.PREFIX}/headers`);
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
const headers = response.headersArray().filter(([name, value]) => name.toLowerCase().includes('name-')); const headers = response.headersArray().filter(([name, value]) => name.toLowerCase().includes('name-'));
expect(headers).toEqual([['Name-A', 'v1'], ['name-b', 'v4'], ['Name-a', 'v2'], ['name-A', 'v3']]); expect(headers).toEqual([['Name-A', 'v1'], ['name-b', 'v4'], ['Name-a', 'v2'], ['name-A', 'v3']]);
@ -307,8 +294,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'),
// @ts-expect-error context.fetch(`http://non-existent.com/target.html`)
context._fetch(`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');
@ -329,8 +315,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();
// @ts-expect-error const response = await context.fetch('http://non-existent.com/simple.json');
const response = await context._fetch('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'});
@ -342,8 +327,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'),
// @ts-expect-error context.fetch(server.EMPTY_PAGE, {
context._fetch(server.EMPTY_PAGE, {
headers: { headers: {
'authorization': 'Basic ' + Buffer.from('user:pass').toString('base64') 'authorization': 'Basic ' + Buffer.from('user:pass').toString('base64')
} }
@ -355,29 +339,25 @@ it('should work with http credentials', async ({context, server}) => {
it('should work with setHTTPCredentials', async ({context, browser, server}) => { it('should work with setHTTPCredentials', async ({context, browser, server}) => {
server.setAuth('/empty.html', 'user', 'pass'); server.setAuth('/empty.html', 'user', 'pass');
// @ts-expect-error const response1 = await context.fetch(server.EMPTY_PAGE);
const response1 = await context._fetch(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' });
// @ts-expect-error const response2 = await context.fetch(server.EMPTY_PAGE);
const response2 = await context._fetch(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, browser, 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' });
// @ts-expect-error const response2 = await context.fetch(server.EMPTY_PAGE);
const response2 = await context._fetch(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'),
// @ts-expect-error context.fetch(`${server.PREFIX}/simple.json`, {
context._fetch(`${server.PREFIX}/simple.json`, {
method: 'POST', method: 'POST',
postData: 'My request' postData: 'My request'
}) })
@ -391,8 +371,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'),
// @ts-expect-error context.fetch(server.EMPTY_PAGE)
context._fetch(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);
@ -404,8 +383,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'),
// @ts-expect-error context.fetch(`${server.PREFIX}/redirect`)
context._fetch(`${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);
@ -416,8 +394,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'),
// @ts-expect-error context.fetch(server.EMPTY_PAGE, {
context._fetch(server.EMPTY_PAGE, {
headers: { headers: {
'User-Agent': 'Playwright', 'User-Agent': 'Playwright',
'Accept': 'text/html', 'Accept': 'text/html',
@ -437,8 +414,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'),
// @ts-expect-error context.fetch(`${server.PREFIX}/a/redirect1`, {headers: {'foo': 'bar'}}),
context._fetch(`${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');
@ -453,8 +429,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'),
// @ts-expect-error context.fetch(`${server.PREFIX}/a/redirect1`),
context._fetch(`${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');
@ -462,8 +437,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}) => {
// @ts-expect-error const error = await context.fetch(`${server.PREFIX}/a/redirect1`, {
const error = await context._fetch(`${server.PREFIX}/a/redirect1`, {
headers: { headers: {
'foo': 'недопустимое значение', 'foo': 'недопустимое значение',
} }
@ -472,11 +446,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}) => {
// @ts-expect-error const error1 = await context.fetch(`data:text/plain,test`).catch(e => e);
const error1 = await context._fetch(`data:text/plain,test`).catch(e => e);
expect(error1.message).toContain('Protocol "data:" not supported'); expect(error1.message).toContain('Protocol "data:" not supported');
// @ts-expect-error const error2 = await context.fetch(`file:///tmp/foo`).catch(e => e);
const error2 = await context._fetch(`file:///tmp/foo`).catch(e => e);
expect(error2.message).toContain('Protocol "file:" not supported'); expect(error2.message).toContain('Protocol "file:" not supported');
}); });
@ -486,8 +458,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 {
// @ts-expect-error const response = await context.fetch(httpsServer.EMPTY_PAGE);
const response = await context._fetch(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;
@ -496,8 +467,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 });
// @ts-expect-error const response = await context.fetch(httpsServer.EMPTY_PAGE);
const response = await context._fetch(httpsServer.EMPTY_PAGE);
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
}); });
@ -506,8 +476,7 @@ it('should resolve url relative to baseURL', async function({server, contextFact
...contextOptions, ...contextOptions,
baseURL: server.PREFIX, baseURL: server.PREFIX,
}); });
// @ts-expect-error const response = await context.fetch('/empty.html');
const response = await context._fetch('/empty.html');
expect(response.url()).toBe(server.EMPTY_PAGE); expect(response.url()).toBe(server.EMPTY_PAGE);
}); });
@ -527,8 +496,7 @@ it('should support gzip compression', async function({context, server}) {
gzip.end(); gzip.end();
}); });
// @ts-expect-error const response = await context.fetch(server.PREFIX + '/compressed');
const response = await context._fetch(server.PREFIX + '/compressed');
expect(await response.text()).toBe('Hello, world!'); expect(await response.text()).toBe('Hello, world!');
}); });
@ -542,8 +510,7 @@ it('should throw informatibe error on corrupted gzip body', async function({cont
res.end(); res.end();
}); });
// @ts-expect-error const error = await context.fetch(server.PREFIX + '/corrupted').catch(e => e);
const error = await context._fetch(server.PREFIX + '/corrupted').catch(e => e);
expect(error.message).toContain(`failed to decompress 'gzip' encoding`); expect(error.message).toContain(`failed to decompress 'gzip' encoding`);
}); });
@ -563,8 +530,7 @@ it('should support brotli compression', async function({context, server}) {
brotli.end(); brotli.end();
}); });
// @ts-expect-error const response = await context.fetch(server.PREFIX + '/compressed');
const response = await context._fetch(server.PREFIX + '/compressed');
expect(await response.text()).toBe('Hello, world!'); expect(await response.text()).toBe('Hello, world!');
}); });
@ -578,8 +544,7 @@ it('should throw informatibe error on corrupted brotli body', async function({co
res.end(); res.end();
}); });
// @ts-expect-error const error = await context.fetch(server.PREFIX + '/corrupted').catch(e => e);
const error = await context._fetch(server.PREFIX + '/corrupted').catch(e => e);
expect(error.message).toContain(`failed to decompress 'br' encoding`); expect(error.message).toContain(`failed to decompress 'br' encoding`);
}); });
@ -599,8 +564,7 @@ it('should support deflate compression', async function({context, server}) {
deflate.end(); deflate.end();
}); });
// @ts-expect-error const response = await context.fetch(server.PREFIX + '/compressed');
const response = await context._fetch(server.PREFIX + '/compressed');
expect(await response.text()).toBe('Hello, world!'); expect(await response.text()).toBe('Hello, world!');
}); });
@ -614,8 +578,7 @@ it('should throw informatibe error on corrupted deflate body', async function({c
res.end(); res.end();
}); });
// @ts-expect-error const error = await context.fetch(server.PREFIX + '/corrupted').catch(e => e);
const error = await context._fetch(server.PREFIX + '/corrupted').catch(e => e);
expect(error.message).toContain(`failed to decompress 'deflate' encoding`); expect(error.message).toContain(`failed to decompress 'deflate' encoding`);
}); });
@ -627,8 +590,7 @@ it('should support timeout option', async function({context, server}) {
}); });
}); });
// @ts-expect-error const error = await context.fetch(server.PREFIX + '/slow', { timeout: 10 }).catch(e => e);
const error = await context._fetch(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`);
}); });
@ -642,14 +604,12 @@ it('should respect timeout after redirects', async function({context, server}) {
}); });
context.setDefaultTimeout(100); context.setDefaultTimeout(100);
// @ts-expect-error const error = await context.fetch(server.PREFIX + '/redirect').catch(e => e);
const error = await context._fetch(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}) {
// @ts-expect-error const response = await context.fetch(server.PREFIX + '/simple.json');
const response = await context._fetch(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);
@ -657,17 +617,34 @@ 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}) {
// @ts-expect-error const response = await context.fetch(server.PREFIX + '/simple.json');
const response = await context._fetch(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);
expect(error.message).toContain('Target page, context or browser has been closed'); expect(error.message).toContain('Target page, context or browser has been closed');
}); });
it('should throw on invalid first argument', async function({context, server}) { it('should throw on invalid first argument', async function({context}) {
// @ts-expect-error const error = await context.fetch({} as any).catch(e => e);
const error = await context._fetch({}).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');
}); });
it('should override request parameters', async function({context, page, server}) {
const [pageReq] = await Promise.all([
page.waitForRequest('**/*'),
page.goto(server.EMPTY_PAGE)
]);
const [req] = await Promise.all([
server.waitForRequest('/empty.html'),
context.fetch(pageReq, {
method: 'POST',
headers: {
'foo': 'bar'
},
postData: 'data'
})
]);
expect(req.method).toBe('POST');
expect(req.headers.foo).toBe('bar');
expect((await req.postBody).toString('utf8')).toBe('data');
});

View file

@ -197,9 +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 => {
// @ts-expect-error const response = await page.fetch(server.PREFIX + '/simple.json');
const response = await page._fetch(server.PREFIX + '/simple.json');
// @ts-expect-error
route.fulfill({ response }); route.fulfill({ response });
}); });
const response = await page.goto(server.EMPTY_PAGE); const response = await page.goto(server.EMPTY_PAGE);
@ -210,10 +208,8 @@ 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 => {
// @ts-expect-error const response = await page.fetch(server.PREFIX + '/simple.json');
const response = await page._fetch(server.PREFIX + '/simple.json');
route.fulfill({ route.fulfill({
// @ts-expect-error
response, response,
status: 201, status: 201,
headers: { headers: {
@ -230,10 +226,8 @@ 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 => {
// @ts-expect-error const response = await page.fetch(route.request());
const response = await page._fetch(route.request());
route.fulfill({ route.fulfill({
// @ts-expect-error
response, response,
}); });
}); });

View file

@ -46,7 +46,6 @@ it('should fulfill response with empty body', async ({page, server, browserName,
// @ts-expect-error // @ts-expect-error
const response = await route._continueToResponse({}); const response = await route._continueToResponse({});
await route.fulfill({ await route.fulfill({
// @ts-expect-error
response, response,
status: 201, status: 201,
body: '' body: ''
@ -127,7 +126,6 @@ it('should support fulfill after intercept', async ({page, server}) => {
await page.route('**', async route => { await page.route('**', async route => {
// @ts-expect-error // @ts-expect-error
const response = await route._continueToResponse(); const response = await route._continueToResponse();
// @ts-expect-error
await route.fulfill({ response }); await route.fulfill({ response });
}); });
const response = await page.goto(server.PREFIX + '/title.html'); const response = await page.goto(server.PREFIX + '/title.html');
@ -147,7 +145,6 @@ it('should intercept failures', async ({page, browserName, browserMajorVersion,
try { try {
// @ts-expect-error // @ts-expect-error
const response = await route._continueToResponse(); const response = await route._continueToResponse();
// @ts-expect-error
await route.fulfill({ response }); await route.fulfill({ response });
} catch (e) { } catch (e) {
error = e; error = e;
@ -173,7 +170,6 @@ it('should support request overrides', async ({page, server, browserName, browse
headers: {'foo': 'bar'}, headers: {'foo': 'bar'},
postData: 'my data', postData: 'my data',
}); });
// @ts-expect-error
await route.fulfill({ response }); await route.fulfill({ response });
}); });
await page.goto(server.PREFIX + '/foo'); await page.goto(server.PREFIX + '/foo');
@ -228,7 +224,6 @@ it('should give access to the intercepted response status text', async ({page, s
expect(response.statusText()).toBe('You are awesome'); expect(response.statusText()).toBe('You are awesome');
expect(response.url()).toBe(server.PREFIX + '/title.html'); expect(response.url()).toBe(server.PREFIX + '/title.html');
// @ts-expect-error
await Promise.all([route.fulfill({ response }), evalPromise]); await Promise.all([route.fulfill({ response }), evalPromise]);
}); });
@ -247,7 +242,6 @@ it('should give access to the intercepted response body', async ({page, server})
expect((await response.text())).toBe('{"foo": "bar"}\n'); expect((await response.text())).toBe('{"foo": "bar"}\n');
// @ts-expect-error
await Promise.all([route.fulfill({ response }), evalPromise]); await Promise.all([route.fulfill({ response }), evalPromise]);
}); });
@ -325,7 +319,6 @@ it('should fulfill original response after redirects', async ({page, browserName
++routeCalls; ++routeCalls;
// @ts-expect-error // @ts-expect-error
const response = await route._continueToResponse({}); const response = await route._continueToResponse({});
// @ts-expect-error
await route.fulfill({ response }); await route.fulfill({ response });
}); });
const response = await page.goto(server.PREFIX + '/redirect/1.html'); const response = await page.goto(server.PREFIX + '/redirect/1.html');

122
types/types.d.ts vendored
View file

@ -1995,6 +1995,34 @@ 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
@ -6394,6 +6422,34 @@ 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.
@ -12607,6 +12663,66 @@ export interface Electron {
}): Promise<ElectronApplication>; }): Promise<ElectronApplication>;
} }
/**
* [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.
*/
export interface FetchResponse {
/**
* Returns the buffer with response body.
*/
body(): Promise<Buffer>;
/**
* Disposes the body of this response. If not called then the body will stay in memory until the context closes.
*/
dispose(): Promise<void>;
/**
* An object with all the response HTTP headers associated with this response.
*/
headers(): { [key: string]: string; };
/**
* An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers with
* multiple entries, such as `Set-Cookie`, appear in the array multiple times.
*/
headersArray(): Array<Array<string>>;
/**
* Returns the JSON representation of response body.
*
* This method will throw if the response body is not parsable via `JSON.parse`.
*/
json(): Promise<Serializable>;
/**
* Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
*/
ok(): boolean;
/**
* Contains the status code of the response (e.g., 200 for a success).
*/
status(): number;
/**
* Contains the status text of the response (e.g. usually an "OK" for a success).
*/
statusText(): string;
/**
* Returns the text representation of response body.
*/
text(): Promise<string>;
/**
* Contains the URL of the response.
*/
url(): string;
}
/** /**
* [FileChooser] objects are dispatched by the page in the * [FileChooser] objects are dispatched by the page in the
* [page.on('filechooser')](https://playwright.dev/docs/api/class-page#page-event-file-chooser) event. * [page.on('filechooser')](https://playwright.dev/docs/api/class-page#page-event-file-chooser) event.
@ -13472,6 +13588,12 @@ export interface Route {
*/ */
path?: string; path?: string;
/**
* [FetchResponse] to fulfill route's request with. Individual fields of the response (such as headers) can be overridden
* using fulfill options.
*/
response?: FetchResponse;
/** /**
* Response status code, defaults to `200`. * Response status code, defaults to `200`.
*/ */