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.
## async method: BrowserContext.fetch
- returns: <[FetchResponse]>
Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
context cookies from the response. The method will automatically follow redirects.
### param: BrowserContext.fetch.urlOrRequest
- `urlOrRequest` <[string]|[Request]>
Target URL or Request to get all fetch parameters from.
### option: BrowserContext.fetch.method
- `method` <[string]>
If set changes the request method (e.g. PUT or POST). If not specified, GET method is used.
### option: BrowserContext.fetch.headers
- `headers` <[Object]<[string], [string]>>
Allows to set HTTP headers.
### option: BrowserContext.fetch.postData
- `postData` <[string]|[Buffer]>
Allows to set post data of the request.
### option: BrowserContext.fetch.timeout
- `timeout` <[float]>
Request timeout in milliseconds.
## async method: BrowserContext.grantPermissions
Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if

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.
## async method: Page.fetch
- returns: <[FetchResponse]>
Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
context cookies from the response. The method will automatically follow redirects.
### param: Page.fetch.urlOrRequest
- `urlOrRequest` <[string]|[Request]>
Target URL or Request to get all fetch parameters from.
### option: Page.fetch.method
- `method` <[string]>
If set changes the request method (e.g. PUT or POST). If not specified, GET method is used.
### option: Page.fetch.headers
- `headers` <[Object]<[string], [string]>>
Allows to set HTTP headers.
### option: Page.fetch.postData
- `postData` <[string]|[Buffer]>
Allows to set post data of the request.
### option: Page.fetch.timeout
- `timeout` <[float]>
Request timeout in milliseconds.
## async method: Page.fill
This method waits for an element matching [`param: selector`], waits for [actionability](./actionability.md) checks, focuses the element, fills it and triggers an `input` event after filling. Note that you can pass an empty string to clear the input field.

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
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
- returns: <[Request]>

View File

@ -33,7 +33,7 @@ export { TimeoutError } from '../utils/errors';
export { Frame } from './frame';
export { Keyboard, Mouse, Touchscreen } from './input';
export { JSHandle } from './jsHandle';
export { Request, Response, Route, WebSocket } from './network';
export { FetchResponse, Request, Response, Route, WebSocket } from './network';
export { Page } from './page';
export { Selectors } from './selectors';
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(url: string, options?: FetchOptions): Promise<network.FetchResponse>;
async _fetch(urlOrRequest: string|network.Request, options: FetchOptions = {}): Promise<network.FetchResponse> {
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 = request?.method() || options.method;
const method = options.method || request?.method();
// 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;
let postDataBuffer = request?.postDataBuffer();
let postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
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 result = await channel.fetch({
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> {
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) => {
let useInterceptedResponseBody;
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 _headers: Headers;
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);
}
async _fetch(request: network.Request, options?: { timeout?: number }): Promise<network.FetchResponse>;
async _fetch(url: string, options?: FetchOptions): Promise<network.FetchResponse>;
async _fetch(urlOrRequest: string|network.Request, 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) {

View File

@ -41,8 +41,7 @@ it.afterAll(() => {
});
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.status()).toBe(200);
expect(response.statusText()).toBe('OK');
@ -58,8 +57,7 @@ it('should throw on network error', async ({context, server}) => {
req.socket.destroy();
});
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');
});
@ -69,8 +67,7 @@ it('should throw on network error after redirect', async ({context, server}) =>
req.socket.destroy();
});
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');
});
@ -85,8 +82,7 @@ it('should throw on network error when sending body', async ({context, server})
req.socket.destroy();
});
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');
});
@ -102,8 +98,7 @@ it('should throw on network error when sending body after redirect', async ({con
req.socket.destroy();
});
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');
});
@ -120,8 +115,7 @@ it('should add session cookies to request', async ({context, server}) => {
}]);
const [req] = await Promise.all([
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');
});
@ -139,8 +133,7 @@ it('should not add context cookie if cookie header passed as a parameter', async
}]);
const [req] = await Promise.all([
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: {
'Cookie': 'foo=bar'
}
@ -164,8 +157,7 @@ it('should follow redirects', async ({context, server}) => {
}]);
const [req, response] = await Promise.all([
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(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.end();
});
// @ts-expect-error
await context._fetch(server.PREFIX + '/setcookie.html');
await context.fetch(server.PREFIX + '/setcookie.html');
const cookies = await context.cookies();
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.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');
});
@ -220,8 +210,7 @@ it('should handle cookies on redirects', async ({context, server, browserName, i
server.waitForRequest('/redirect1'),
server.waitForRequest('/a/b/redirect2'),
server.waitForRequest('/title.html'),
// @ts-expect-error
context._fetch(`${server.PREFIX}/redirect1`),
context.fetch(`${server.PREFIX}/redirect1`),
]);
expect(req1.headers.cookie).toBeFalsy();
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('/a/b/redirect2'),
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(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.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);
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']]);
@ -307,8 +294,7 @@ it('should work with context level proxy', async ({browserOptions, browserType,
const [request, response] = await Promise.all([
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(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' }
});
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(auth).toBe('Basic ' + Buffer.from('user:secret').toString('base64'));
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([
server.waitForRequest('/empty.html'),
// @ts-expect-error
context._fetch(server.EMPTY_PAGE, {
context.fetch(server.EMPTY_PAGE, {
headers: {
'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}) => {
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);
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);
});
it('should return error with wrong credentials', async ({context, browser, server}) => {
server.setAuth('/empty.html', 'user', 'pass');
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);
});
it('should support post data', async ({context, server}) => {
const [request, response] = await Promise.all([
server.waitForRequest('/simple.json'),
// @ts-expect-error
context._fetch(`${server.PREFIX}/simple.json`, {
context.fetch(`${server.PREFIX}/simple.json`, {
method: 'POST',
postData: 'My request'
})
@ -391,8 +371,7 @@ it('should support post data', async ({context, server}) => {
it('should add default headers', async ({context, server, page}) => {
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
// @ts-expect-error
context._fetch(server.EMPTY_PAGE)
context.fetch(server.EMPTY_PAGE)
]);
expect(request.headers['accept']).toBe('*/*');
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');
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
// @ts-expect-error
context._fetch(`${server.PREFIX}/redirect`)
context.fetch(`${server.PREFIX}/redirect`)
]);
expect(request.headers['accept']).toBe('*/*');
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}) => {
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
// @ts-expect-error
context._fetch(server.EMPTY_PAGE, {
context.fetch(server.EMPTY_PAGE, {
headers: {
'User-Agent': 'Playwright',
'Accept': 'text/html',
@ -437,8 +414,7 @@ it('should propagate custom headers with redirects', async ({context, server}) =
server.waitForRequest('/a/redirect1'),
server.waitForRequest('/b/c/redirect2'),
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(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('/b/c/redirect2'),
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(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}) => {
// @ts-expect-error
const error = await context._fetch(`${server.PREFIX}/a/redirect1`, {
const error = await context.fetch(`${server.PREFIX}/a/redirect1`, {
headers: {
'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}) => {
// @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');
// @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');
});
@ -486,8 +458,7 @@ it('should support https', async ({context, httpsServer}) => {
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
suppressCertificateWarning();
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);
} finally {
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}) => {
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);
});
@ -506,8 +476,7 @@ it('should resolve url relative to baseURL', async function({server, contextFact
...contextOptions,
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);
});
@ -527,8 +496,7 @@ it('should support gzip compression', async function({context, server}) {
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!');
});
@ -542,8 +510,7 @@ it('should throw informatibe error on corrupted gzip body', async function({cont
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`);
});
@ -563,8 +530,7 @@ it('should support brotli compression', async function({context, server}) {
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!');
});
@ -578,8 +544,7 @@ it('should throw informatibe error on corrupted brotli body', async function({co
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`);
});
@ -599,8 +564,7 @@ it('should support deflate compression', async function({context, server}) {
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!');
});
@ -614,8 +578,7 @@ it('should throw informatibe error on corrupted deflate body', async function({c
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`);
});
@ -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`);
});
@ -642,14 +604,12 @@ it('should respect timeout after redirects', async function({context, server}) {
});
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`);
});
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' });
await response.dispose();
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}) {
// @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' });
await context.close();
const error = await response.body().catch(e => e);
expect(error.message).toContain('Target page, context or browser has been closed');
});
it('should throw on invalid first argument', async function({context, server}) {
// @ts-expect-error
const error = await context._fetch({}).catch(e => e);
it('should throw on invalid first argument', async function({context}) {
const error = await context.fetch({} as any).catch(e => e);
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.fixme(isElectron, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
// @ts-expect-error
const response = await page._fetch(server.PREFIX + '/simple.json');
// @ts-expect-error
const response = await page.fetch(server.PREFIX + '/simple.json');
route.fulfill({ response });
});
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.fixme(isElectron, 'error: Browser context management is not supported.');
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({
// @ts-expect-error
response,
status: 201,
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.fixme(isElectron, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
// @ts-expect-error
const response = await page._fetch(route.request());
const response = await page.fetch(route.request());
route.fulfill({
// @ts-expect-error
response,
});
});

View File

@ -46,7 +46,6 @@ it('should fulfill response with empty body', async ({page, server, browserName,
// @ts-expect-error
const response = await route._continueToResponse({});
await route.fulfill({
// @ts-expect-error
response,
status: 201,
body: ''
@ -127,7 +126,6 @@ it('should support fulfill after intercept', async ({page, server}) => {
await page.route('**', async route => {
// @ts-expect-error
const response = await route._continueToResponse();
// @ts-expect-error
await route.fulfill({ response });
});
const response = await page.goto(server.PREFIX + '/title.html');
@ -147,7 +145,6 @@ it('should intercept failures', async ({page, browserName, browserMajorVersion,
try {
// @ts-expect-error
const response = await route._continueToResponse();
// @ts-expect-error
await route.fulfill({ response });
} catch (e) {
error = e;
@ -173,7 +170,6 @@ it('should support request overrides', async ({page, server, browserName, browse
headers: {'foo': 'bar'},
postData: 'my data',
});
// @ts-expect-error
await route.fulfill({ response });
});
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.url()).toBe(server.PREFIX + '/title.html');
// @ts-expect-error
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');
// @ts-expect-error
await Promise.all([route.fulfill({ response }), evalPromise]);
});
@ -325,7 +319,6 @@ it('should fulfill original response after redirects', async ({page, browserName
++routeCalls;
// @ts-expect-error
const response = await route._continueToResponse({});
// @ts-expect-error
await route.fulfill({ response });
});
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>;
/**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
* @param urlOrRequest Target URL or Request to get all fetch parameters from.
* @param options
*/
fetch(urlOrRequest: string|Request, options?: {
/**
* Allows to set HTTP headers.
*/
headers?: { [key: string]: string; };
/**
* If set changes the request method (e.g. PUT or POST). If not specified, GET method is used.
*/
method?: string;
/**
* Allows to set post data of the request.
*/
postData?: string|Buffer;
/**
* Request timeout in milliseconds.
*/
timeout?: number;
}): Promise<FetchResponse>;
/**
* This method waits for an element matching `selector`, waits for [actionability](https://playwright.dev/docs/actionability) checks, focuses the
* element, fills it and triggers an `input` event after filling. Note that you can pass an empty string to clear the input
@ -6394,6 +6422,34 @@ export interface BrowserContext {
*/
exposeFunction(name: string, callback: Function): Promise<void>;
/**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
* @param urlOrRequest Target URL or Request to get all fetch parameters from.
* @param options
*/
fetch(urlOrRequest: string|Request, options?: {
/**
* Allows to set HTTP headers.
*/
headers?: { [key: string]: string; };
/**
* If set changes the request method (e.g. PUT or POST). If not specified, GET method is used.
*/
method?: string;
/**
* Allows to set post data of the request.
*/
postData?: string|Buffer;
/**
* Request timeout in milliseconds.
*/
timeout?: number;
}): Promise<FetchResponse>;
/**
* Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
* specified.
@ -12607,6 +12663,66 @@ export interface Electron {
}): 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
* [page.on('filechooser')](https://playwright.dev/docs/api/class-page#page-event-file-chooser) event.
@ -13472,6 +13588,12 @@ export interface Route {
*/
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`.
*/