From 79eb7744bc60c594a3d461d724c6abbea4f0a4d6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 22 Sep 2021 12:44:22 -0700 Subject: [PATCH] feat(fetch): support options in playwright._newRequest (#9061) --- docs/src/api/class-playwright.md | 29 +++++ src/client/playwright.ts | 10 +- src/client/types.ts | 2 +- src/common/types.ts | 17 +++ src/dispatchers/playwrightDispatcher.ts | 14 +-- src/protocol/channels.ts | 28 +++++ src/protocol/protocol.yml | 19 ++++ src/protocol/validator.ts | 14 +++ src/server/fetch.ts | 51 ++++++--- tests/browsercontext-fetch.spec.ts | 22 ---- tests/global-fetch.spec.ts | 145 ++++++++++++++++++++++++ types/types.d.ts | 99 +++++++++++++++- utils/doclint/missingDocs.js | 2 + utils/generate_types/index.js | 33 ++++-- utils/generate_types/overrides.d.ts | 5 - 15 files changed, 418 insertions(+), 72 deletions(-) create mode 100644 tests/global-fetch.spec.ts diff --git a/docs/src/api/class-playwright.md b/docs/src/api/class-playwright.md index ae09bb1771..2676e9f5c8 100644 --- a/docs/src/api/class-playwright.md +++ b/docs/src/api/class-playwright.md @@ -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 - type: <[BrowserType]> diff --git a/src/client/playwright.ts b/src/client/playwright.ts index 087baf6db3..a01c2b8e25 100644 --- a/src/client/playwright.ts +++ b/src/client/playwright.ts @@ -20,13 +20,14 @@ import util from 'util'; import * as channels from '../protocol/channels'; import { TimeoutError } from '../utils/errors'; import { createSocket } from '../utils/netUtils'; +import { headersObjectToArray } from '../utils/utils'; import { Android } from './android'; import { BrowserType } from './browserType'; import { ChannelOwner } from './channelOwner'; import { Electron } from './electron'; import { FetchRequest } from './fetch'; import { Selectors, SelectorsOwner, sharedSelectors } from './selectors'; -import { Size } from './types'; +import { NewRequestOptions, Size } from './types'; const dnsLookupAsync = util.promisify(dns.lookup); type DeviceDescriptor = { @@ -69,9 +70,12 @@ export class Playwright extends ChannelOwner { + async _newRequest(options: NewRequestOptions = {}): Promise { 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); }); } diff --git a/src/client/types.ts b/src/client/types.ts index d6f0eef5a9..7be13ff00a 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -17,7 +17,7 @@ import * as channels from '../protocol/channels'; 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'; export interface Logger { diff --git a/src/common/types.ts b/src/common/types.ts index 1e9fc72322..ff7f5f8e34 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -22,3 +22,20 @@ export type URLMatch = string | RegExp | ((url: URL) => boolean); export type TimeoutOptions = { timeout?: number }; export type NameValue = { name: string, value: string }; 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; +}; \ No newline at end of file diff --git a/src/dispatchers/playwrightDispatcher.ts b/src/dispatchers/playwrightDispatcher.ts index 4defffdbf3..cbbf47036d 100644 --- a/src/dispatchers/playwrightDispatcher.ts +++ b/src/dispatchers/playwrightDispatcher.ts @@ -16,18 +16,18 @@ import net, { AddressInfo } from 'net'; import * as channels from '../protocol/channels'; +import { GlobalFetchRequest } from '../server/fetch'; 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 { BrowserTypeDispatcher } from './browserTypeDispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher'; 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 { SelectorsDispatcher } from './selectorsDispatcher'; export class PlaywrightDispatcher extends Dispatcher implements channels.PlaywrightChannel { private _socksProxy: SocksProxy | undefined; @@ -75,7 +75,7 @@ export class PlaywrightDispatcher extends Dispatcher { - const request = new GlobalFetchRequest(this._object); + const request = new GlobalFetchRequest(this._object, params); return { request: FetchRequestDispatcher.from(this._scope, request) }; } } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index a647917f90..12e8079435 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -322,10 +322,38 @@ export type PlaywrightSocksEndOptions = { }; export type PlaywrightSocksEndResult = void; export type PlaywrightNewRequestParams = { + baseURL?: string, + userAgent?: string, ignoreHTTPSErrors?: boolean, + extraHTTPHeaders?: NameValue[], + httpCredentials?: { + username: string, + password: string, + }, + proxy?: { + server: string, + bypass?: string, + username?: string, + password?: string, + }, + timeout?: number, }; export type PlaywrightNewRequestOptions = { + baseURL?: string, + userAgent?: string, ignoreHTTPSErrors?: boolean, + extraHTTPHeaders?: NameValue[], + httpCredentials?: { + username: string, + password: string, + }, + proxy?: { + server: string, + bypass?: string, + username?: string, + password?: string, + }, + timeout?: number, }; export type PlaywrightNewRequestResult = { request: FetchRequestChannel, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 628a947958..536cafbcc7 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -455,7 +455,26 @@ Playwright: newRequest: parameters: + baseURL: string? + userAgent: string? 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: request: FetchRequest diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 900fb99a62..ff6354b840 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -195,7 +195,21 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { uid: tString, }); scheme.PlaywrightNewRequestParams = tObject({ + baseURL: tOptional(tString), + userAgent: tOptional(tString), 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({ name: tString, diff --git a/src/server/fetch.ts b/src/server/fetch.ts index 7050e92984..4ed476ff0c 100644 --- a/src/server/fetch.ts +++ b/src/server/fetch.ts @@ -14,24 +14,25 @@ * 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 https from 'https'; -import { BrowserContext } from './browserContext'; -import * as types from './types'; +import { HttpsProxyAgent } from 'https-proxy-agent'; 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 { BrowserContext } from './browserContext'; +import { MultipartFormData } from './formData'; import { SdkObject } from './instrumentation'; import { Playwright } from './playwright'; +import * as types 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; extraHTTPHeaders?: HeadersArray; httpCredentials?: HTTPCredentials; @@ -336,8 +337,29 @@ export class BrowserContextFetchRequest extends FetchRequest { export class GlobalFetchRequest extends FetchRequest { - constructor(playwright: Playwright) { + private readonly _options: FetchRequestOptions; + constructor(playwright: Playwright, options: Omit & { extraHTTPHeaders?: NameValue[] }) { 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() { @@ -345,14 +367,7 @@ export class GlobalFetchRequest extends FetchRequest { } _defaultOptions(): FetchRequestOptions { - return { - userAgent: '', - extraHTTPHeaders: undefined, - proxy: undefined, - timeoutSettings: new TimeoutSettings(), - ignoreHTTPSErrors: false, - baseURL: undefined, - }; + return this._options; } async _addCookies(cookies: types.SetNetworkCookieParam[]): Promise { diff --git a/tests/browsercontext-fetch.spec.ts b/tests/browsercontext-fetch.spec.ts index b721a347fd..edabf0a462 100644 --- a/tests/browsercontext-fetch.spec.ts +++ b/tests/browsercontext-fetch.spec.ts @@ -42,19 +42,6 @@ it.afterAll(() => { 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}) => { const response = await context._request.get(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'); }); -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}) { const error = await context._request.get({} as any).catch(e => e); expect(error.message).toContain('First argument must be either URL string or Request'); diff --git a/tests/global-fetch.spec.ts b/tests/global-fetch.spec.ts new file mode 100644 index 0000000000..b5021d822f --- /dev/null +++ b/tests/global-fetch.spec.ts @@ -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); +}); + diff --git a/types/types.d.ts b/types/types.d.ts index dc2fe06a80..8ca2d97689 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -10360,7 +10360,6 @@ type AccessibilityNode = { children?: AccessibilityNode[]; } -export const selectors: Selectors; export const devices: Devices & DeviceDescriptor[]; //@ts-ignore this will be any if electron is not installed @@ -10669,12 +10668,8 @@ export type AndroidKey = 'Copy' | 'Paste'; -export const chromium: BrowserType; -export const firefox: BrowserType; -export const webkit: BrowserType; export const _electron: Electron; export const _android: Android; -export const _newRequest: () => Promise; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; @@ -13227,6 +13222,100 @@ export interface Mouse { wheel(deltaX: number, deltaY: number): Promise; } +/** + * **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; + +/** + * 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]: * - [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) emitted when the request is diff --git a/utils/doclint/missingDocs.js b/utils/doclint/missingDocs.js index 17d2d43d73..655f247756 100644 --- a/utils/doclint/missingDocs.js +++ b/utils/doclint/missingDocs.js @@ -116,6 +116,8 @@ function listMethods(rootNames, apiFileName) { function shouldSkipMethodByName(className, methodName) { if (methodName === '_request' && (className === 'BrowserContext' || className === 'Page')) return false; + if (methodName === '_newRequest' && className === 'Playwright') + return false; if (methodName.startsWith('_') || methodName === 'T' || methodName === 'toString') return true; if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(methodName)) diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index f6ba643efd..8dee323843 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -110,11 +110,18 @@ class TypesGenerator { }); 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 [ `// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length).split(path.sep).join(path.posix.sep)}`, 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), '', ].join('\n'); @@ -215,21 +222,23 @@ class TypesGenerator { /** * @param {Documentation.Class} classDesc + * @param {boolean=} exportMembersAsGlobals */ - classBody(classDesc) { - const parts = []; + classBody(classDesc, exportMembersAsGlobals) { + let parts = []; const eventDescriptions = this.createEventDescriptions(classDesc); const commentForMethod = { off: '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.' } + const indent = exportMembersAsGlobals ? '' : ' '; for (const method of ['on', 'once', 'addListener', 'removeListener', 'off']) { for (const {eventName, params, comment} of eventDescriptions) { if ((method === 'on' || method === 'addListener') && comment) - parts.push(this.writeComment(comment, ' ')); + parts.push(this.writeComment(comment, indent)); else - parts.push(this.writeComment(commentForMethod[method], ' ')); + parts.push(this.writeComment(commentForMethod[method], indent)); parts.push(` ${method}(event: '${eventName}', listener: (${params}) => void): this;\n`); } } @@ -242,20 +251,24 @@ class TypesGenerator { const parts = []; for (const {eventName, params, comment, type} of eventDescriptions) { if (comment) - parts.push(this.writeComment(comment, ' ')); + parts.push(this.writeComment(comment, indent)); parts.push(` ${member.alias}(event: '${eventName}', optionsOrPredicate?: { predicate?: (${params}) => boolean | Promise, timeout?: number } | ((${params}) => boolean | Promise)): Promise<${type}>;\n`); } return parts.join('\n'); } - const jsdoc = this.memberJSDOC(member, ' '); - const args = this.argsFromMember(member, ' ', classDesc.name); - let type = this.stringifyComplexType(member.type, ' ', classDesc.name, member.alias); + const jsdoc = this.memberJSDOC(member, indent); + const args = this.argsFromMember(member, indent, classDesc.name); + let type = this.stringifyComplexType(member.type, indent, classDesc.name, member.alias); if (member.async) type = `Promise<${type}>`; // do this late, because we still want object definitions for overridden types if (!this.hasOwnMethod(classDesc, member.alias)) 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};` }).filter(x => x).join('\n\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')); 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(); const apiTypesGenerator = new TypesGenerator(apiDocumentation); let apiTypes = await apiTypesGenerator.generateTypes(path.join(__dirname, 'overrides.d.ts')); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 0bd921c2cb..680c0b9f8e 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -241,7 +241,6 @@ type AccessibilityNode = { children?: AccessibilityNode[]; } -export const selectors: Selectors; export const devices: Devices & DeviceDescriptor[]; //@ts-ignore this will be any if electron is not installed @@ -343,12 +342,8 @@ export type AndroidKey = 'Copy' | 'Paste'; -export const chromium: BrowserType; -export const firefox: BrowserType; -export const webkit: BrowserType; export const _electron: Electron; export const _android: Android; -export const _newRequest: () => Promise; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {};