feat(fetch): support options in playwright._newRequest (#9061)
This commit is contained in:
parent
c673ef5330
commit
79eb7744bc
|
|
@ -83,6 +83,35 @@ class PlaywrightExample
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## async method: Playwright._newRequest
|
||||||
|
* langs: js
|
||||||
|
- returns: <[FetchRequest]>
|
||||||
|
|
||||||
|
**experimental** Creates new instances of [FetchRequest].
|
||||||
|
|
||||||
|
### option: Playwright._newRequest.useragent = %%-context-option-useragent-%%
|
||||||
|
|
||||||
|
### option: Playwright._newRequest.extraHTTPHeaders = %%-context-option-extrahttpheaders-%%
|
||||||
|
|
||||||
|
### option: Playwright._newRequest.httpCredentials = %%-context-option-httpcredentials-%%
|
||||||
|
|
||||||
|
### option: Playwright._newRequest.proxy = %%-browser-option-proxy-%%
|
||||||
|
|
||||||
|
### option: Playwright._newRequest.timeout
|
||||||
|
- `timeout` <[float]>
|
||||||
|
|
||||||
|
Maximum time in milliseconds to wait for the response. Defaults to
|
||||||
|
`30000` (30 seconds). Pass `0` to disable timeout.
|
||||||
|
|
||||||
|
### option: Playwright._newRequest.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%%
|
||||||
|
|
||||||
|
### option: Playwright._newRequest.baseURL
|
||||||
|
- `baseURL` <[string]>
|
||||||
|
|
||||||
|
When using [`method: FetchRequest.get`], [`method: FetchRequest.post`], [`method: FetchRequest.fetch`] it takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. Examples:
|
||||||
|
* baseURL: `http://localhost:3000` and sending rquest to `/bar.html` results in `http://localhost:3000/bar.html`
|
||||||
|
* baseURL: `http://localhost:3000/foo/` and sending rquest to `./bar.html` results in `http://localhost:3000/foo/bar.html`
|
||||||
|
|
||||||
## property: Playwright.chromium
|
## property: Playwright.chromium
|
||||||
- type: <[BrowserType]>
|
- type: <[BrowserType]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,14 @@ import util from 'util';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { TimeoutError } from '../utils/errors';
|
import { TimeoutError } from '../utils/errors';
|
||||||
import { createSocket } from '../utils/netUtils';
|
import { createSocket } from '../utils/netUtils';
|
||||||
|
import { headersObjectToArray } from '../utils/utils';
|
||||||
import { Android } from './android';
|
import { Android } from './android';
|
||||||
import { BrowserType } from './browserType';
|
import { BrowserType } from './browserType';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import { Electron } from './electron';
|
import { Electron } from './electron';
|
||||||
import { FetchRequest } from './fetch';
|
import { FetchRequest } from './fetch';
|
||||||
import { Selectors, SelectorsOwner, sharedSelectors } from './selectors';
|
import { Selectors, SelectorsOwner, sharedSelectors } from './selectors';
|
||||||
import { Size } from './types';
|
import { NewRequestOptions, Size } from './types';
|
||||||
const dnsLookupAsync = util.promisify(dns.lookup);
|
const dnsLookupAsync = util.promisify(dns.lookup);
|
||||||
|
|
||||||
type DeviceDescriptor = {
|
type DeviceDescriptor = {
|
||||||
|
|
@ -69,9 +70,12 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
||||||
this.selectors._addChannel(this._selectorsOwner);
|
this.selectors._addChannel(this._selectorsOwner);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _newRequest(options?: {}): Promise<FetchRequest> {
|
async _newRequest(options: NewRequestOptions = {}): Promise<FetchRequest> {
|
||||||
return await this._wrapApiCall(async (channel: channels.PlaywrightChannel) => {
|
return await this._wrapApiCall(async (channel: channels.PlaywrightChannel) => {
|
||||||
return FetchRequest.from((await channel.newRequest({})).request);
|
return FetchRequest.from((await channel.newRequest({
|
||||||
|
...options,
|
||||||
|
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined
|
||||||
|
})).request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import type { Size } from '../common/types';
|
import type { Size } from '../common/types';
|
||||||
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray } from '../common/types';
|
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray, NewRequestOptions } from '../common/types';
|
||||||
|
|
||||||
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
|
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
|
||||||
export interface Logger {
|
export interface Logger {
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,20 @@ export type URLMatch = string | RegExp | ((url: URL) => boolean);
|
||||||
export type TimeoutOptions = { timeout?: number };
|
export type TimeoutOptions = { timeout?: number };
|
||||||
export type NameValue = { name: string, value: string };
|
export type NameValue = { name: string, value: string };
|
||||||
export type HeadersArray = NameValue[];
|
export type HeadersArray = NameValue[];
|
||||||
|
export type NewRequestOptions = {
|
||||||
|
baseURL?: string;
|
||||||
|
extraHTTPHeaders?: { [key: string]: string; };
|
||||||
|
httpCredentials?: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
ignoreHTTPSErrors?: boolean;
|
||||||
|
proxy?: {
|
||||||
|
server: string;
|
||||||
|
bypass?: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
};
|
||||||
|
timeout?: number;
|
||||||
|
userAgent?: string;
|
||||||
|
};
|
||||||
|
|
@ -16,18 +16,18 @@
|
||||||
|
|
||||||
import net, { AddressInfo } from 'net';
|
import net, { AddressInfo } from 'net';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
|
import { GlobalFetchRequest } from '../server/fetch';
|
||||||
import { Playwright } from '../server/playwright';
|
import { Playwright } from '../server/playwright';
|
||||||
|
import * as types from '../server/types';
|
||||||
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
|
import { SocksConnection, SocksConnectionClient } from '../utils/socksProxy';
|
||||||
|
import { createGuid } from '../utils/utils';
|
||||||
import { AndroidDispatcher } from './androidDispatcher';
|
import { AndroidDispatcher } from './androidDispatcher';
|
||||||
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
|
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
|
||||||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||||
import { ElectronDispatcher } from './electronDispatcher';
|
import { ElectronDispatcher } from './electronDispatcher';
|
||||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
|
||||||
import * as types from '../server/types';
|
|
||||||
import { SocksConnection, SocksConnectionClient } from '../utils/socksProxy';
|
|
||||||
import { createGuid } from '../utils/utils';
|
|
||||||
import { debugLogger } from '../utils/debugLogger';
|
|
||||||
import { GlobalFetchRequest } from '../server/fetch';
|
|
||||||
import { FetchRequestDispatcher } from './networkDispatchers';
|
import { FetchRequestDispatcher } from './networkDispatchers';
|
||||||
|
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||||
|
|
||||||
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightInitializer, channels.PlaywrightEvents> implements channels.PlaywrightChannel {
|
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightInitializer, channels.PlaywrightEvents> implements channels.PlaywrightChannel {
|
||||||
private _socksProxy: SocksProxy | undefined;
|
private _socksProxy: SocksProxy | undefined;
|
||||||
|
|
@ -75,7 +75,7 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
||||||
}
|
}
|
||||||
|
|
||||||
async newRequest(params: channels.PlaywrightNewRequestParams, metadata?: channels.Metadata): Promise<channels.PlaywrightNewRequestResult> {
|
async newRequest(params: channels.PlaywrightNewRequestParams, metadata?: channels.Metadata): Promise<channels.PlaywrightNewRequestResult> {
|
||||||
const request = new GlobalFetchRequest(this._object);
|
const request = new GlobalFetchRequest(this._object, params);
|
||||||
return { request: FetchRequestDispatcher.from(this._scope, request) };
|
return { request: FetchRequestDispatcher.from(this._scope, request) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -322,10 +322,38 @@ export type PlaywrightSocksEndOptions = {
|
||||||
};
|
};
|
||||||
export type PlaywrightSocksEndResult = void;
|
export type PlaywrightSocksEndResult = void;
|
||||||
export type PlaywrightNewRequestParams = {
|
export type PlaywrightNewRequestParams = {
|
||||||
|
baseURL?: string,
|
||||||
|
userAgent?: string,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
extraHTTPHeaders?: NameValue[],
|
||||||
|
httpCredentials?: {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
},
|
||||||
|
proxy?: {
|
||||||
|
server: string,
|
||||||
|
bypass?: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string,
|
||||||
|
},
|
||||||
|
timeout?: number,
|
||||||
};
|
};
|
||||||
export type PlaywrightNewRequestOptions = {
|
export type PlaywrightNewRequestOptions = {
|
||||||
|
baseURL?: string,
|
||||||
|
userAgent?: string,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
extraHTTPHeaders?: NameValue[],
|
||||||
|
httpCredentials?: {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
},
|
||||||
|
proxy?: {
|
||||||
|
server: string,
|
||||||
|
bypass?: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string,
|
||||||
|
},
|
||||||
|
timeout?: number,
|
||||||
};
|
};
|
||||||
export type PlaywrightNewRequestResult = {
|
export type PlaywrightNewRequestResult = {
|
||||||
request: FetchRequestChannel,
|
request: FetchRequestChannel,
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,26 @@ Playwright:
|
||||||
|
|
||||||
newRequest:
|
newRequest:
|
||||||
parameters:
|
parameters:
|
||||||
|
baseURL: string?
|
||||||
|
userAgent: string?
|
||||||
ignoreHTTPSErrors: boolean?
|
ignoreHTTPSErrors: boolean?
|
||||||
|
extraHTTPHeaders:
|
||||||
|
type: array?
|
||||||
|
items: NameValue
|
||||||
|
httpCredentials:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
proxy:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
server: string
|
||||||
|
bypass: string?
|
||||||
|
username: string?
|
||||||
|
password: string?
|
||||||
|
timeout: number?
|
||||||
|
|
||||||
returns:
|
returns:
|
||||||
request: FetchRequest
|
request: FetchRequest
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,21 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
uid: tString,
|
uid: tString,
|
||||||
});
|
});
|
||||||
scheme.PlaywrightNewRequestParams = tObject({
|
scheme.PlaywrightNewRequestParams = tObject({
|
||||||
|
baseURL: tOptional(tString),
|
||||||
|
userAgent: tOptional(tString),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||||
|
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||||
|
httpCredentials: tOptional(tObject({
|
||||||
|
username: tString,
|
||||||
|
password: tString,
|
||||||
|
})),
|
||||||
|
proxy: tOptional(tObject({
|
||||||
|
server: tString,
|
||||||
|
bypass: tOptional(tString),
|
||||||
|
username: tOptional(tString),
|
||||||
|
password: tOptional(tString),
|
||||||
|
})),
|
||||||
|
timeout: tOptional(tNumber),
|
||||||
});
|
});
|
||||||
scheme.SelectorsRegisterParams = tObject({
|
scheme.SelectorsRegisterParams = tObject({
|
||||||
name: tString,
|
name: tString,
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,25 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
||||||
import url from 'url';
|
|
||||||
import zlib from 'zlib';
|
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import { BrowserContext } from './browserContext';
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
import * as types from './types';
|
|
||||||
import { pipeline, Readable, Transform } from 'stream';
|
import { pipeline, Readable, Transform } from 'stream';
|
||||||
|
import url from 'url';
|
||||||
|
import zlib from 'zlib';
|
||||||
|
import { HTTPCredentials } from '../../types/types';
|
||||||
|
import { NameValue, NewRequestOptions } from '../common/types';
|
||||||
|
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||||
import { createGuid, isFilePayload, monotonicTime } from '../utils/utils';
|
import { createGuid, isFilePayload, monotonicTime } from '../utils/utils';
|
||||||
|
import { BrowserContext } from './browserContext';
|
||||||
|
import { MultipartFormData } from './formData';
|
||||||
import { SdkObject } from './instrumentation';
|
import { SdkObject } from './instrumentation';
|
||||||
import { Playwright } from './playwright';
|
import { Playwright } from './playwright';
|
||||||
|
import * as types from './types';
|
||||||
import { HeadersArray, ProxySettings } from './types';
|
import { HeadersArray, ProxySettings } from './types';
|
||||||
import { HTTPCredentials } from '../../types/types';
|
|
||||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
|
||||||
import { MultipartFormData } from './formData';
|
|
||||||
|
|
||||||
|
|
||||||
type FetchRequestOptions = {
|
export type FetchRequestOptions = {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
extraHTTPHeaders?: HeadersArray;
|
extraHTTPHeaders?: HeadersArray;
|
||||||
httpCredentials?: HTTPCredentials;
|
httpCredentials?: HTTPCredentials;
|
||||||
|
|
@ -336,8 +337,29 @@ export class BrowserContextFetchRequest extends FetchRequest {
|
||||||
|
|
||||||
|
|
||||||
export class GlobalFetchRequest extends FetchRequest {
|
export class GlobalFetchRequest extends FetchRequest {
|
||||||
constructor(playwright: Playwright) {
|
private readonly _options: FetchRequestOptions;
|
||||||
|
constructor(playwright: Playwright, options: Omit<NewRequestOptions, 'extraHTTPHeaders'> & { extraHTTPHeaders?: NameValue[] }) {
|
||||||
super(playwright);
|
super(playwright);
|
||||||
|
const timeoutSettings = new TimeoutSettings();
|
||||||
|
if (options.timeout !== undefined)
|
||||||
|
timeoutSettings.setDefaultTimeout(options.timeout);
|
||||||
|
const proxy = options.proxy;
|
||||||
|
if (proxy?.server) {
|
||||||
|
let url = proxy?.server.trim();
|
||||||
|
if (!/^\w+:\/\//.test(url))
|
||||||
|
url = 'http://' + url;
|
||||||
|
proxy.server = url;
|
||||||
|
}
|
||||||
|
this._options = {
|
||||||
|
baseURL: options.baseURL,
|
||||||
|
userAgent: options.userAgent || '',
|
||||||
|
extraHTTPHeaders: options.extraHTTPHeaders,
|
||||||
|
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
|
||||||
|
httpCredentials: options.httpCredentials,
|
||||||
|
proxy,
|
||||||
|
timeoutSettings,
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override dispose() {
|
override dispose() {
|
||||||
|
|
@ -345,14 +367,7 @@ export class GlobalFetchRequest extends FetchRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
_defaultOptions(): FetchRequestOptions {
|
_defaultOptions(): FetchRequestOptions {
|
||||||
return {
|
return this._options;
|
||||||
userAgent: '',
|
|
||||||
extraHTTPHeaders: undefined,
|
|
||||||
proxy: undefined,
|
|
||||||
timeoutSettings: new TimeoutSettings(),
|
|
||||||
ignoreHTTPSErrors: false,
|
|
||||||
baseURL: undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _addCookies(cookies: types.SetNetworkCookieParam[]): Promise<void> {
|
async _addCookies(cookies: types.SetNetworkCookieParam[]): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -42,19 +42,6 @@ it.afterAll(() => {
|
||||||
http.globalAgent = prevAgent;
|
http.globalAgent = prevAgent;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('global get should work', async ({playwright, context, server}) => {
|
|
||||||
const request = await playwright._newRequest();
|
|
||||||
const response = await 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('get should work', async ({context, server}) => {
|
it('get should work', async ({context, server}) => {
|
||||||
const response = await context._request.get(server.PREFIX + '/simple.json');
|
const response = await context._request.get(server.PREFIX + '/simple.json');
|
||||||
expect(response.url()).toBe(server.PREFIX + '/simple.json');
|
expect(response.url()).toBe(server.PREFIX + '/simple.json');
|
||||||
|
|
@ -706,15 +693,6 @@ it('should dispose when context closes', async function({context, server}) {
|
||||||
expect(error.message).toContain('Response has been disposed');
|
expect(error.message).toContain('Response has been disposed');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispose global request', async function({playwright, context, server}) {
|
|
||||||
const request = await playwright._newRequest();
|
|
||||||
const response = await request.get(server.PREFIX + '/simple.json');
|
|
||||||
expect(await response.json()).toEqual({ foo: 'bar' });
|
|
||||||
await request.dispose();
|
|
||||||
const error = await response.body().catch(e => e);
|
|
||||||
expect(error.message).toContain('Response has been disposed');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw on invalid first argument', async function({context}) {
|
it('should throw on invalid first argument', async function({context}) {
|
||||||
const error = await context._request.get({} 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');
|
||||||
|
|
|
||||||
145
tests/global-fetch.spec.ts
Normal file
145
tests/global-fetch.spec.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
*
|
||||||
|
* 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 http from 'http';
|
||||||
|
import { expect, playwrightTest as it } from './config/browserTest';
|
||||||
|
|
||||||
|
it.skip(({ mode }) => mode !== 'default');
|
||||||
|
|
||||||
|
let prevAgent: http.Agent;
|
||||||
|
it.beforeAll(() => {
|
||||||
|
prevAgent = http.globalAgent;
|
||||||
|
http.globalAgent = new http.Agent({
|
||||||
|
// @ts-expect-error
|
||||||
|
lookup: (hostname, options, callback) => {
|
||||||
|
if (hostname === 'localhost' || hostname.endsWith('playwright.dev'))
|
||||||
|
callback(null, '127.0.0.1', 4);
|
||||||
|
else
|
||||||
|
throw new Error(`Failed to resolve hostname: ${hostname}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.afterAll(() => {
|
||||||
|
http.globalAgent = prevAgent;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const method of ['get', 'post', 'fetch']) {
|
||||||
|
it(`${method} should work`, async ({playwright, server}) => {
|
||||||
|
const request = await playwright._newRequest();
|
||||||
|
const response = await request[method](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(`should dispose global ${method} request`, async function({playwright, context, server}) {
|
||||||
|
const request = await playwright._newRequest();
|
||||||
|
const response = await request.get(server.PREFIX + '/simple.json');
|
||||||
|
expect(await response.json()).toEqual({ foo: 'bar' });
|
||||||
|
await request.dispose();
|
||||||
|
const error = await response.body().catch(e => e);
|
||||||
|
expect(error.message).toContain('Response has been disposed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should support global userAgent option', async ({playwright, server}) => {
|
||||||
|
const request = await playwright._newRequest({ userAgent: 'My Agent'});
|
||||||
|
const [serverRequest, response] = await Promise.all([
|
||||||
|
server.waitForRequest('/empty.html'),
|
||||||
|
request.get(server.EMPTY_PAGE)
|
||||||
|
]);
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||||
|
expect(serverRequest.headers['user-agent']).toBe('My Agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support global timeout option', async ({playwright, server}) => {
|
||||||
|
const request = await playwright._newRequest({ timeout: 1});
|
||||||
|
server.setRoute('/empty.html', (req, res) => {});
|
||||||
|
const error = await request.get(server.EMPTY_PAGE).catch(e => e);
|
||||||
|
expect(error.message).toContain('Request timed out after 1ms');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate extra http headers with redirects', async ({playwright, server}) => {
|
||||||
|
server.setRedirect('/a/redirect1', '/b/c/redirect2');
|
||||||
|
server.setRedirect('/b/c/redirect2', '/simple.json');
|
||||||
|
const request = await playwright._newRequest({ extraHTTPHeaders: { 'My-Secret': 'Value' }});
|
||||||
|
const [req1, req2, req3] = await Promise.all([
|
||||||
|
server.waitForRequest('/a/redirect1'),
|
||||||
|
server.waitForRequest('/b/c/redirect2'),
|
||||||
|
server.waitForRequest('/simple.json'),
|
||||||
|
request.get(`${server.PREFIX}/a/redirect1`),
|
||||||
|
]);
|
||||||
|
expect(req1.headers['my-secret']).toBe('Value');
|
||||||
|
expect(req2.headers['my-secret']).toBe('Value');
|
||||||
|
expect(req3.headers['my-secret']).toBe('Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support global httpCredentials option', async ({playwright, server}) => {
|
||||||
|
server.setAuth('/empty.html', 'user', 'pass');
|
||||||
|
const request1 = await playwright._newRequest();
|
||||||
|
const response1 = await request1.get(server.EMPTY_PAGE);
|
||||||
|
expect(response1.status()).toBe(401);
|
||||||
|
await request1.dispose();
|
||||||
|
|
||||||
|
const request2 = await playwright._newRequest({ httpCredentials: { username: 'user', password: 'pass' }});
|
||||||
|
const response2 = await request2.get(server.EMPTY_PAGE);
|
||||||
|
expect(response2.status()).toBe(200);
|
||||||
|
await request2.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error with wrong credentials', async ({playwright, server}) => {
|
||||||
|
server.setAuth('/empty.html', 'user', 'pass');
|
||||||
|
const request = await playwright._newRequest({ httpCredentials: { username: 'user', password: 'wrong' }});
|
||||||
|
const response2 = await request.get(server.EMPTY_PAGE);
|
||||||
|
expect(response2.status()).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass proxy credentials', async ({playwright, server, proxyServer}) => {
|
||||||
|
proxyServer.forwardTo(server.PORT);
|
||||||
|
let auth;
|
||||||
|
proxyServer.setAuthHandler(req => {
|
||||||
|
auth = req.headers['proxy-authorization'];
|
||||||
|
return !!auth;
|
||||||
|
});
|
||||||
|
const request = await playwright._newRequest({
|
||||||
|
proxy: { server: `localhost:${proxyServer.PORT}`, username: 'user', password: 'secret' }
|
||||||
|
});
|
||||||
|
const response = await request.get('http://non-existent.com/simple.json');
|
||||||
|
expect(proxyServer.connectHosts).toContain('non-existent.com:80');
|
||||||
|
expect(auth).toBe('Basic ' + Buffer.from('user:secret').toString('base64'));
|
||||||
|
expect(await response.json()).toEqual({foo: 'bar'});
|
||||||
|
await request.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support global ignoreHTTPSErrors option', async ({playwright, httpsServer}) => {
|
||||||
|
const request = await playwright._newRequest({ ignoreHTTPSErrors: true });
|
||||||
|
const response = await request.get(httpsServer.EMPTY_PAGE);
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve url relative to gobal baseURL option', async ({playwright, server}) => {
|
||||||
|
const request = await playwright._newRequest({ baseURL: server.PREFIX });
|
||||||
|
const response = await request.get('/empty.html');
|
||||||
|
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||||
|
});
|
||||||
|
|
||||||
99
types/types.d.ts
vendored
99
types/types.d.ts
vendored
|
|
@ -10360,7 +10360,6 @@ type AccessibilityNode = {
|
||||||
children?: AccessibilityNode[];
|
children?: AccessibilityNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectors: Selectors;
|
|
||||||
export const devices: Devices & DeviceDescriptor[];
|
export const devices: Devices & DeviceDescriptor[];
|
||||||
|
|
||||||
//@ts-ignore this will be any if electron is not installed
|
//@ts-ignore this will be any if electron is not installed
|
||||||
|
|
@ -10669,12 +10668,8 @@ export type AndroidKey =
|
||||||
'Copy' |
|
'Copy' |
|
||||||
'Paste';
|
'Paste';
|
||||||
|
|
||||||
export const chromium: BrowserType;
|
|
||||||
export const firefox: BrowserType;
|
|
||||||
export const webkit: BrowserType;
|
|
||||||
export const _electron: Electron;
|
export const _electron: Electron;
|
||||||
export const _android: Android;
|
export const _android: Android;
|
||||||
export const _newRequest: () => Promise<FetchRequest>;
|
|
||||||
|
|
||||||
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
|
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
|
||||||
export {};
|
export {};
|
||||||
|
|
@ -13227,6 +13222,100 @@ export interface Mouse {
|
||||||
wheel(deltaX: number, deltaY: number): Promise<void>;
|
wheel(deltaX: number, deltaY: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **experimental** Creates new instances of [FetchRequest].
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export const _newRequest: (options?: {
|
||||||
|
/**
|
||||||
|
* When using
|
||||||
|
* [fetchRequest.get(urlOrRequest[, options])](https://playwright.dev/docs/api/class-fetchrequest#fetch-request-get),
|
||||||
|
* [fetchRequest.post(urlOrRequest[, options])](https://playwright.dev/docs/api/class-fetchrequest#fetch-request-post),
|
||||||
|
* [fetchRequest.fetch(urlOrRequest[, options])](https://playwright.dev/docs/api/class-fetchrequest#fetch-request-fetch) it
|
||||||
|
* takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL)
|
||||||
|
* constructor for building the corresponding URL. Examples:
|
||||||
|
* - baseURL: `http://localhost:3000` and sending rquest to `/bar.html` results in `http://localhost:3000/bar.html`
|
||||||
|
* - baseURL: `http://localhost:3000/foo/` and sending rquest to `./bar.html` results in
|
||||||
|
* `http://localhost:3000/foo/bar.html`
|
||||||
|
*/
|
||||||
|
baseURL?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing additional HTTP headers to be sent with every request.
|
||||||
|
*/
|
||||||
|
extraHTTPHeaders?: { [key: string]: string; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
|
||||||
|
*/
|
||||||
|
httpCredentials?: {
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to ignore HTTPS errors when sending network requests. Defaults to `false`.
|
||||||
|
*/
|
||||||
|
ignoreHTTPSErrors?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network proxy settings.
|
||||||
|
*/
|
||||||
|
proxy?: {
|
||||||
|
/**
|
||||||
|
* Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example `http://myproxy.com:3128` or
|
||||||
|
* `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy.
|
||||||
|
*/
|
||||||
|
server: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
|
||||||
|
*/
|
||||||
|
bypass?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional username to use if HTTP proxy requires authentication.
|
||||||
|
*/
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional password to use if HTTP proxy requires authentication.
|
||||||
|
*/
|
||||||
|
password?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum time in milliseconds to wait for the response. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific user agent to use in this context.
|
||||||
|
*/
|
||||||
|
userAgent?: string;
|
||||||
|
}) => Promise<FetchRequest>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object can be used to launch or connect to Chromium, returning instances of [Browser].
|
||||||
|
*/
|
||||||
|
export const chromium: BrowserType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object can be used to launch or connect to Firefox, returning instances of [Browser].
|
||||||
|
*/
|
||||||
|
export const firefox: BrowserType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selectors can be used to install custom selector engines. See [Working with selectors](https://playwright.dev/docs/selectors) for more
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
export const selectors: Selectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object can be used to launch or connect to WebKit, returning instances of [Browser].
|
||||||
|
*/
|
||||||
|
export const webkit: BrowserType;
|
||||||
/**
|
/**
|
||||||
* Whenever the page sends a request for a network resource the following sequence of events are emitted by [Page]:
|
* Whenever the page sends a request for a network resource the following sequence of events are emitted by [Page]:
|
||||||
* - [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) emitted when the request is
|
* - [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) emitted when the request is
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,8 @@ function listMethods(rootNames, apiFileName) {
|
||||||
function shouldSkipMethodByName(className, methodName) {
|
function shouldSkipMethodByName(className, methodName) {
|
||||||
if (methodName === '_request' && (className === 'BrowserContext' || className === 'Page'))
|
if (methodName === '_request' && (className === 'BrowserContext' || className === 'Page'))
|
||||||
return false;
|
return false;
|
||||||
|
if (methodName === '_newRequest' && className === 'Playwright')
|
||||||
|
return false;
|
||||||
if (methodName.startsWith('_') || methodName === 'T' || methodName === 'toString')
|
if (methodName.startsWith('_') || methodName === 'T' || methodName === 'toString')
|
||||||
return true;
|
return true;
|
||||||
if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(methodName))
|
if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(methodName))
|
||||||
|
|
|
||||||
|
|
@ -110,11 +110,18 @@ class TypesGenerator {
|
||||||
});
|
});
|
||||||
|
|
||||||
const classes = this.documentation.classesArray.filter(cls => !handledClasses.has(cls.name));
|
const classes = this.documentation.classesArray.filter(cls => !handledClasses.has(cls.name));
|
||||||
|
{
|
||||||
|
const playwright = this.documentation.classesArray.find(c => c.name === 'Playwright');
|
||||||
|
playwright.membersArray = playwright.membersArray.filter(member => !['errors', 'devices'].includes(member.name));
|
||||||
|
playwright.index();
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
`// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length).split(path.sep).join(path.posix.sep)}`,
|
`// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length).split(path.sep).join(path.posix.sep)}`,
|
||||||
overrides,
|
overrides,
|
||||||
'',
|
'',
|
||||||
docsOnlyClassMapping ? '' : classes.map(classDesc => this.classToString(classDesc)).join('\n'),
|
docsOnlyClassMapping ? '' : classes.map(classDesc => {
|
||||||
|
return (classDesc.name === 'Playwright') ? this.classBody(classDesc, true) : this.classToString(classDesc);
|
||||||
|
}).join('\n'),
|
||||||
this.objectDefinitionsToString(overrides),
|
this.objectDefinitionsToString(overrides),
|
||||||
'',
|
'',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
@ -215,21 +222,23 @@ class TypesGenerator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Documentation.Class} classDesc
|
* @param {Documentation.Class} classDesc
|
||||||
|
* @param {boolean=} exportMembersAsGlobals
|
||||||
*/
|
*/
|
||||||
classBody(classDesc) {
|
classBody(classDesc, exportMembersAsGlobals) {
|
||||||
const parts = [];
|
let parts = [];
|
||||||
const eventDescriptions = this.createEventDescriptions(classDesc);
|
const eventDescriptions = this.createEventDescriptions(classDesc);
|
||||||
const commentForMethod = {
|
const commentForMethod = {
|
||||||
off: 'Removes an event listener added by `on` or `addListener`.',
|
off: 'Removes an event listener added by `on` or `addListener`.',
|
||||||
removeListener: 'Removes an event listener added by `on` or `addListener`.',
|
removeListener: 'Removes an event listener added by `on` or `addListener`.',
|
||||||
once: 'Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.'
|
once: 'Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.'
|
||||||
}
|
}
|
||||||
|
const indent = exportMembersAsGlobals ? '' : ' ';
|
||||||
for (const method of ['on', 'once', 'addListener', 'removeListener', 'off']) {
|
for (const method of ['on', 'once', 'addListener', 'removeListener', 'off']) {
|
||||||
for (const {eventName, params, comment} of eventDescriptions) {
|
for (const {eventName, params, comment} of eventDescriptions) {
|
||||||
if ((method === 'on' || method === 'addListener') && comment)
|
if ((method === 'on' || method === 'addListener') && comment)
|
||||||
parts.push(this.writeComment(comment, ' '));
|
parts.push(this.writeComment(comment, indent));
|
||||||
else
|
else
|
||||||
parts.push(this.writeComment(commentForMethod[method], ' '));
|
parts.push(this.writeComment(commentForMethod[method], indent));
|
||||||
parts.push(` ${method}(event: '${eventName}', listener: (${params}) => void): this;\n`);
|
parts.push(` ${method}(event: '${eventName}', listener: (${params}) => void): this;\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,20 +251,24 @@ class TypesGenerator {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
for (const {eventName, params, comment, type} of eventDescriptions) {
|
for (const {eventName, params, comment, type} of eventDescriptions) {
|
||||||
if (comment)
|
if (comment)
|
||||||
parts.push(this.writeComment(comment, ' '));
|
parts.push(this.writeComment(comment, indent));
|
||||||
parts.push(` ${member.alias}(event: '${eventName}', optionsOrPredicate?: { predicate?: (${params}) => boolean | Promise<boolean>, timeout?: number } | ((${params}) => boolean | Promise<boolean>)): Promise<${type}>;\n`);
|
parts.push(` ${member.alias}(event: '${eventName}', optionsOrPredicate?: { predicate?: (${params}) => boolean | Promise<boolean>, timeout?: number } | ((${params}) => boolean | Promise<boolean>)): Promise<${type}>;\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join('\n');
|
return parts.join('\n');
|
||||||
}
|
}
|
||||||
const jsdoc = this.memberJSDOC(member, ' ');
|
const jsdoc = this.memberJSDOC(member, indent);
|
||||||
const args = this.argsFromMember(member, ' ', classDesc.name);
|
const args = this.argsFromMember(member, indent, classDesc.name);
|
||||||
let type = this.stringifyComplexType(member.type, ' ', classDesc.name, member.alias);
|
let type = this.stringifyComplexType(member.type, indent, classDesc.name, member.alias);
|
||||||
if (member.async)
|
if (member.async)
|
||||||
type = `Promise<${type}>`;
|
type = `Promise<${type}>`;
|
||||||
// do this late, because we still want object definitions for overridden types
|
// do this late, because we still want object definitions for overridden types
|
||||||
if (!this.hasOwnMethod(classDesc, member.alias))
|
if (!this.hasOwnMethod(classDesc, member.alias))
|
||||||
return '';
|
return '';
|
||||||
|
if (exportMembersAsGlobals) {
|
||||||
|
const memberType = member.kind === 'method' ? `${args} => ${type}` : type;
|
||||||
|
return `${jsdoc}${exportMembersAsGlobals ? 'export const ' : ''}${member.alias}: ${memberType};`
|
||||||
|
}
|
||||||
return `${jsdoc}${member.alias}${args}: ${type};`
|
return `${jsdoc}${member.alias}${args}: ${type};`
|
||||||
}).filter(x => x).join('\n\n'));
|
}).filter(x => x).join('\n\n'));
|
||||||
return parts.join('\n');
|
return parts.join('\n');
|
||||||
|
|
@ -462,8 +475,6 @@ class TypesGenerator {
|
||||||
writeFile(path.join(typesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'server', 'chromium', 'protocol.d.ts'), 'utf8'));
|
writeFile(path.join(typesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'server', 'chromium', 'protocol.d.ts'), 'utf8'));
|
||||||
|
|
||||||
const apiDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
|
const apiDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
|
||||||
// Root module types are overridden.
|
|
||||||
apiDocumentation.classesArray = apiDocumentation.classesArray.filter(cls => cls.name !== 'Playwright');
|
|
||||||
apiDocumentation.index();
|
apiDocumentation.index();
|
||||||
const apiTypesGenerator = new TypesGenerator(apiDocumentation);
|
const apiTypesGenerator = new TypesGenerator(apiDocumentation);
|
||||||
let apiTypes = await apiTypesGenerator.generateTypes(path.join(__dirname, 'overrides.d.ts'));
|
let apiTypes = await apiTypesGenerator.generateTypes(path.join(__dirname, 'overrides.d.ts'));
|
||||||
|
|
|
||||||
5
utils/generate_types/overrides.d.ts
vendored
5
utils/generate_types/overrides.d.ts
vendored
|
|
@ -241,7 +241,6 @@ type AccessibilityNode = {
|
||||||
children?: AccessibilityNode[];
|
children?: AccessibilityNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectors: Selectors;
|
|
||||||
export const devices: Devices & DeviceDescriptor[];
|
export const devices: Devices & DeviceDescriptor[];
|
||||||
|
|
||||||
//@ts-ignore this will be any if electron is not installed
|
//@ts-ignore this will be any if electron is not installed
|
||||||
|
|
@ -343,12 +342,8 @@ export type AndroidKey =
|
||||||
'Copy' |
|
'Copy' |
|
||||||
'Paste';
|
'Paste';
|
||||||
|
|
||||||
export const chromium: BrowserType;
|
|
||||||
export const firefox: BrowserType;
|
|
||||||
export const webkit: BrowserType;
|
|
||||||
export const _electron: Electron;
|
export const _electron: Electron;
|
||||||
export const _android: Android;
|
export const _android: Android;
|
||||||
export const _newRequest: () => Promise<FetchRequest>;
|
|
||||||
|
|
||||||
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
|
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue