diff --git a/remote/test/puppeteer/.eslintrc.js b/remote/test/puppeteer/.eslintrc.js index 3e40356283bde..32b5d16114941 100644 --- a/remote/test/puppeteer/.eslintrc.js +++ b/remote/test/puppeteer/.eslintrc.js @@ -96,6 +96,8 @@ module.exports = { "rules": { "no-unused-vars": 0, "@typescript-eslint/no-unused-vars": 2, + "func-call-spacing": 0, + "@typescript-eslint/func-call-spacing": 2, "semi": 0, "@typescript-eslint/semi": 2, "@typescript-eslint/no-empty-function": 0, diff --git a/remote/test/puppeteer/.travis.yml b/remote/test/puppeteer/.travis.yml index 467f88577b8c1..8da18b9a8579a 100644 --- a/remote/test/puppeteer/.travis.yml +++ b/remote/test/puppeteer/.travis.yml @@ -64,11 +64,19 @@ jobs: - CHROMIUM=true script: - npm run compare-protocol-d-ts - - npm run test-install - npm run lint - npm run test-doclint - npm run ensure-new-docs-up-to-date + # This bot runs separately as it changes package.json to test puppeteer-core + # and we don't want that leaking into other bots and causing issues. + - node_js: "10.19.0" + name: 'Test bundling and install of packages' + env: + - CHROMIUM=true + script: + - npm run test-install + # Runs unit tests on Linux + Firefox - node_js: "10.19.0" name: 'Unit tests: Linux/Firefox' diff --git a/remote/test/puppeteer/README.md b/remote/test/puppeteer/README.md index afbe88db88270..c76cbbbe24e2a 100644 --- a/remote/test/puppeteer/README.md +++ b/remote/test/puppeteer/README.md @@ -6,7 +6,7 @@ -###### [API](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) +###### [API](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) > Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. @@ -37,7 +37,7 @@ npm i puppeteer # or "yarn add puppeteer" ``` -Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, or to download a different browser, see [Environment variables](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#environment-variables). +Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, or to download a different browser, see [Environment variables](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#environment-variables). ### puppeteer-core @@ -63,7 +63,7 @@ Note: Prior to v1.18.1, Puppeteer required at least Node v6.4.0. Versions from v Node 8.9.0+. Starting from v3.0.0 Puppeteer starts to rely on Node 10.18.1+. All examples below use async/await which is only supported in Node v7.6.0 or greater. Puppeteer will be familiar to people using other browser testing frameworks. You create an instance -of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#). +of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#). **Example** - navigating to https://example.com and saving a screenshot as *example.png*: @@ -88,7 +88,7 @@ Execute script on the command line node example.js ``` -Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#pagesetviewportviewport). +Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#pagesetviewportviewport). **Example** - create a PDF. @@ -113,7 +113,7 @@ Execute script on the command line node hn.js ``` -See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#pagepdfoptions) for more information about creating pdfs. +See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#pagepdfoptions) for more information about creating pdfs. **Example** - evaluate script in the context of the page @@ -148,7 +148,7 @@ Execute script on the command line node get-dimensions.js ``` -See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. +See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. @@ -157,7 +157,7 @@ See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/ **1. Uses Headless mode** -Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#puppeteerlaunchoptions) when launching a browser: +Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#puppeteerlaunchoptions) when launching a browser: ```js const browser = await puppeteer.launch({headless: false}); // default is true @@ -173,7 +173,7 @@ pass in the executable's path when creating a `Browser` instance: const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); ``` -You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#puppeteerlaunchoptions) for more information. +You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#puppeteerlaunchoptions) for more information. See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/master/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. @@ -185,7 +185,7 @@ Puppeteer creates its own browser user profile which it **cleans up on every run ## Resources -- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md) +- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md) - [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples/) - [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) @@ -292,9 +292,25 @@ Puppeteer creates its own browser user profile which it **cleans up on every run - debug your test inside chromium like a boss! - + + +## Usage with TypeScript + +We have recently completed a migration to move the Puppeteer source code from JavaScript to TypeScript and we're currently working on shipping type definitions for TypeScript with Puppeteer's npm package. + +Until this work is complete we recommend installing the Puppeteer type definitions from the [DefinitelyTyped](https://definitelytyped.org/) repository: + +```bash +npm install --save-dev @types/puppeteer +``` + +The types that you'll see appearing in the Puppeteer source code are based off the great work of those who have contributed to the `@types/puppeteer` package. We really appreciate the hard work those people put in to providing high quality TypeScript definitions for Puppeteer's users. + + + + ## Contributing to Puppeteer Check out [contributing guide](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) to get an overview of Puppeteer development. @@ -312,7 +328,7 @@ See [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING Official Firefox support is currently experimental. The ongoing collaboration with Mozilla aims to support common end-to-end testing use cases, for which developers expect cross-browser coverage. The Puppeteer team needs input from users to stabilize Firefox support and to bring missing APIs to our attention. -From Puppeteer v2.1.0 onwards you can specify [`puppeteer.launch({product: 'firefox'})`](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#puppeteerlaunchoptions) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches. While [an older experiment](https://www.npmjs.com/package/puppeteer-firefox) required a patched version of Firefox, [the current approach](https://wiki.mozilla.org/Remote) works with “stock” Firefox. +From Puppeteer v2.1.0 onwards you can specify [`puppeteer.launch({product: 'firefox'})`](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#puppeteerlaunchoptions) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches. While [an older experiment](https://www.npmjs.com/package/puppeteer-firefox) required a patched version of Firefox, [the current approach](https://wiki.mozilla.org/Remote) works with “stock” Firefox. We will continue to collaborate with other browser vendors to bring Puppeteer support to browsers such as Safari. This effort includes exploration of a standard for executing cross-browser commands (instead of relying on the non-standard DevTools Protocol used by Chrome). @@ -364,12 +380,12 @@ npm install puppeteer-core@chrome-71 #### Q: Which Chromium version does Puppeteer use? -Look for `chromium_revision` in [package.json](https://github.com/puppeteer/puppeteer/blob/main/package.json). To find the corresponding Chromium commit and version number, search for the revision prefixed by an `r` in [OmahaProxy](https://omahaproxy.appspot.com/)'s "Find Releases" section. +Look for the `chromium` entry in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts). To find the corresponding Chromium commit and version number, search for the revision prefixed by an `r` in [OmahaProxy](https://omahaproxy.appspot.com/)'s "Find Releases" section. #### Q: Which Firefox version does Puppeteer use? -Since Firefox support is experimental, Puppeteer downloads the latest [Firefox Nightly](https://wiki.mozilla.org/Nightly) when the `PUPPETEER_PRODUCT` environment variable is set to `firefox`. That's also why the value of `firefox_revision` in [package.json](https://github.com/puppeteer/puppeteer/blob/main/package.json) is `latest` -- Puppeteer isn't tied to a particular Firefox version. +Since Firefox support is experimental, Puppeteer downloads the latest [Firefox Nightly](https://wiki.mozilla.org/Nightly) when the `PUPPETEER_PRODUCT` environment variable is set to `firefox`. That's also why the value of `firefox` in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts) is `latest` -- Puppeteer isn't tied to a particular Firefox version. To fetch Firefox Nightly as part of Puppeteer installation: @@ -408,7 +424,7 @@ await page.evaluate(() => { You may find that Puppeteer does not behave as expected when controlling pages that incorporate audio and video. (For example, [video playback/screenshots is likely to fail](https://github.com/puppeteer/puppeteer/issues/291).) There are two reasons for this: -* Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/v4.0.0/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.) +* Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/v5.0.0/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.) * Since Puppeteer (in all configurations) controls a desktop version of Chromium/Chrome, features that are only supported by the mobile version of Chrome are not supported. This means that Puppeteer [does not support HTTP Live Streaming (HLS)](https://caniuse.com/#feat=http-live-streaming). #### Q: I am having trouble installing / running Puppeteer in my test environment. Where should I look for help? diff --git a/remote/test/puppeteer/api-extractor.json b/remote/test/puppeteer/api-extractor.json index 76afa3db00369..8cc8240aa41e6 100644 --- a/remote/test/puppeteer/api-extractor.json +++ b/remote/test/puppeteer/api-extractor.json @@ -1,6 +1,6 @@ { "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "mainEntryPointFilePath": "/lib/api-docs-entry.d.ts", + "mainEntryPointFilePath": "/lib/cjs/api-docs-entry.d.ts", "bundledPackages": [ ], "apiReport": { @@ -27,6 +27,9 @@ }, "extractorMessageReporting": { + "ae-internal-missing-underscore": { + "logLevel": "none" + }, "default": { "logLevel": "warning" } diff --git a/remote/test/puppeteer/cjs-entry-core.js b/remote/test/puppeteer/cjs-entry-core.js new file mode 100644 index 0000000000000..efcdb39027f7e --- /dev/null +++ b/remote/test/puppeteer/cjs-entry-core.js @@ -0,0 +1,29 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +/** + * We use `export default puppeteer` in `src/index.ts` to expose the library But + * TypeScript in CJS mode compiles that to `exports.default = `. This means that + * our CJS Node users would have to use `require('puppeteer').default` which + * isn't very nice. + * + * So instead we expose this file as our entry point. This requires the compiled + * Puppeteer output and re-exports the `default` export via `module.exports.` + * This means that we can publish to CJS and ESM whilst maintaining the expected + * import behaviour for CJS and ESM users. + */ +const puppeteerExport = require('./lib/cjs/index-core'); +module.exports = puppeteerExport.default; diff --git a/remote/test/puppeteer/cjs-entry.js b/remote/test/puppeteer/cjs-entry.js new file mode 100644 index 0000000000000..424ffadf1a14e --- /dev/null +++ b/remote/test/puppeteer/cjs-entry.js @@ -0,0 +1,29 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +/** + * We use `export default puppeteer` in `src/index.ts` to expose the library But + * TypeScript in CJS mode compiles that to `exports.default = `. This means that + * our CJS Node users would have to use `require('puppeteer').default` which + * isn't very nice. + * + * So instead we expose this file as our entry point. This requires the compiled + * Puppeteer output and re-exports the `default` export via `module.exports.` + * This means that we can publish to CJS and ESM whilst maintaining the expected + * import behaviour for CJS and ESM users. + */ +const puppeteerExport = require('./lib/cjs/index'); +module.exports = puppeteerExport.default; diff --git a/remote/test/puppeteer/docs/api.md b/remote/test/puppeteer/docs/api.md index 954e31550b4d7..e3d2c8c198d20 100644 --- a/remote/test/puppeteer/docs/api.md +++ b/remote/test/puppeteer/docs/api.md @@ -1,5 +1,5 @@ -# Puppeteer API Tip-Of-Tree +# Puppeteer API v5.0.0 - Interactive Documentation: https://pptr.dev @@ -170,6 +170,8 @@ * [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) * [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options) * [page.workers()](#pageworkers) + * [GeolocationOptions](#geolocationoptions) + * [WaitTimeoutOptions](#waittimeoutoptions) - [class: WebWorker](#class-webworker) * [webWorker.evaluate(pageFunction[, ...args])](#webworkerevaluatepagefunction-args) * [webWorker.evaluateHandle(pageFunction[, ...args])](#webworkerevaluatehandlepagefunction-args) @@ -354,7 +356,7 @@ The Puppeteer API is hierarchical and mirrors the browser structure. > **NOTE** On the following diagram, faded entities are not currently represented in Puppeteer. -![puppeteer overview](../docs/assets/overview.png) +![puppeteer overview](https://user-images.githubusercontent.com/81942/86137523-ab2ba080-baed-11ea-9d4b-30eda784585a.png) - [`Puppeteer`](#class-puppeteer) communicates with the browser using [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). - [`Browser`](#class-browser) instance can own multiple browser contexts. @@ -1454,7 +1456,7 @@ Shortcut for [page.mainFrame().evaluate(pageFunction, ...args)](#frameevaluatepa #### page.evaluateHandle(pageFunction[, ...args]) - `pageFunction` <[function]|[string]> Function to be evaluated in the page context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` -- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle) +- returns: <[Promise]<[JSHandle]|[ElementHandle]>> Promise which resolves to the return value of `pageFunction` as an in-page object. The only difference between `page.evaluate` and `page.evaluateHandle` is that `page.evaluateHandle` returns in-page object (JSHandle). @@ -1473,6 +1475,14 @@ console.log(await resultHandle.jsonValue()); await resultHandle.dispose(); ``` +This function will return a [JSHandle] by default, however if your `pageFunction` returns an HTML element you will get back an `ElementHandle`: + +```js +const button = await page.evaluateHandle(() => document.querySelector('button')) +// button is an ElementHandle, so you can call methods such as click: +await button.click(); +``` + Shortcut for [page.mainFrame().executionContext().evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args). #### page.evaluateOnNewDocument(pageFunction[, ...args]) @@ -1900,10 +1910,7 @@ The extra HTTP headers will be sent with every request the page initiates. > **NOTE** page.setExtraHTTPHeaders does not guarantee the order of headers in the outgoing requests. #### page.setGeolocation(options) -- `options` <[Object]> - - `latitude` <[number]> Latitude between -90 and 90. - - `longitude` <[number]> Longitude between -180 and 180. - - `accuracy` <[number]> Optional non-negative accuracy value. +- `options` <[GeolocationOptions](####GeolocationOptions)> - returns: <[Promise]> Sets the page's geolocation. @@ -2077,8 +2084,7 @@ await page.waitFor(selector => !!document.querySelector(selector), {}, selector) Shortcut for [page.mainFrame().waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args). #### page.waitForFileChooser([options]) -- `options` <[Object]> Optional waiting parameters - - `timeout` <[number]> Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. +- `options` <[WaitTimeoutOptions](####WaitTimeoutOptions)> Optional waiting parameters - returns: <[Promise]<[FileChooser]>> A promise that resolves after a page requests a file picker. > **NOTE** In non-headless Chromium, this method results in the native file picker dialog **not showing up** for the user. @@ -2263,6 +2269,15 @@ This method returns all of the dedicated [WebWorkers](https://developer.mozilla. > **NOTE** This does not contain ServiceWorkers +#### GeolocationOptions +- `latitude` <[number]> Latitude between -90 and 90. +- `longitude` <[number]> Longitude between -180 and 180. +- `accuracy` <[number]> Optional non-negative accuracy value. + +#### WaitTimeoutOptions +- `timeout` <[number]> Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. + + ### class: WebWorker The WebWorker class represents a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). @@ -2291,12 +2306,14 @@ Shortcut for [(await worker.executionContext()).evaluate(pageFunction, ...args)] #### webWorker.evaluateHandle(pageFunction[, ...args]) - `pageFunction` <[function]|[string]> Function to be evaluated in the page context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` -- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle) +- returns: <[Promise]<[JSHandle]|[ElementHandle]>> Promise which resolves to the return value of `pageFunction` as an in-page object. The only difference between `worker.evaluate` and `worker.evaluateHandle` is that `worker.evaluateHandle` returns in-page object (JSHandle). If the function passed to the `worker.evaluateHandle` returns a [Promise], then `worker.evaluateHandle` would wait for the promise to resolve and return its value. +If the function returns an element, the returned handle is an [ElementHandle]. + Shortcut for [(await worker.executionContext()).evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args). #### webWorker.executionContext() @@ -2848,12 +2865,14 @@ await bodyHandle.dispose(); #### frame.evaluateHandle(pageFunction[, ...args]) - `pageFunction` <[function]|[string]> Function to be evaluated in the page context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` -- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle) +- returns: <[Promise]<[JSHandle]|[ElementHandle]>> Promise which resolves to the return value of `pageFunction` as an in-page object. The only difference between `frame.evaluate` and `frame.evaluateHandle` is that `frame.evaluateHandle` returns in-page object (JSHandle). If the function, passed to the `frame.evaluateHandle`, returns a [Promise], then `frame.evaluateHandle` would wait for the promise to resolve and return its value. +If the function returns an element, the returned handle is an [ElementHandle]. + ```js const aWindowHandle = await frame.evaluateHandle(() => Promise.resolve(window)); aWindowHandle; // Handle for the window object. @@ -3177,10 +3196,12 @@ console.log(result); // prints '3'. #### executionContext.evaluateHandle(pageFunction[, ...args]) - `pageFunction` <[function]|[string]> Function to be evaluated in the `executionContext` - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` -- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle) +- returns: <[Promise]<[JSHandle]|[ElementHandle]>> Promise which resolves to the return value of `pageFunction` as an in-page object. The only difference between `executionContext.evaluate` and `executionContext.evaluateHandle` is that `executionContext.evaluateHandle` returns in-page object (JSHandle). +If the function returns an element, the returned handle is an [ElementHandle]. + If the function passed to the `executionContext.evaluateHandle` returns a [Promise], then `executionContext.evaluateHandle` would wait for the promise to resolve and return its value. ```js @@ -3270,12 +3291,14 @@ expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10'); #### jsHandle.evaluateHandle(pageFunction[, ...args]) - `pageFunction` <[function]|[string]> Function to be evaluated - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` -- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle) +- returns: <[Promise]<[JSHandle]|[ElementHandle]>> Promise which resolves to the return value of `pageFunction` as an in-page object. This method passes this handle as the first argument to `pageFunction`. The only difference between `jsHandle.evaluate` and `jsHandle.evaluateHandle` is that `executionContext.evaluateHandle` returns in-page object (JSHandle). +If the function returns an element, the returned handle is an [ElementHandle]. + If the function passed to the `jsHandle.evaluateHandle` returns a [Promise], then `jsHandle.evaluateHandle` would wait for the promise to resolve and return its value. See [Page.evaluateHandle](#pageevaluatehandlepagefunction-args) for more details. @@ -3459,12 +3482,14 @@ expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10'); #### elementHandle.evaluateHandle(pageFunction[, ...args]) - `pageFunction` <[function]|[string]> Function to be evaluated - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` -- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle) +- returns: <[Promise]<[JSHandle]|[ElementHandle]>> Promise which resolves to the return value of `pageFunction` as an in-page object. This method passes this handle as the first argument to `pageFunction`. The only difference between `evaluateHandle.evaluate` and `evaluateHandle.evaluateHandle` is that `executionContext.evaluateHandle` returns in-page object (JSHandle). +If the function returns an element, the returned handle is an [ElementHandle]. + If the function passed to the `evaluateHandle.evaluateHandle` returns a [Promise], then `evaluateHandle.evaluateHandle` would wait for the promise to resolve and return its value. See [Page.evaluateHandle](#pageevaluatehandlepagefunction-args) for more details. @@ -4050,8 +4075,8 @@ This method is identical to `off` and maintained for compatibility with Node's E [Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object" [Page]: #class-page "Page" [Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise" -[HTTPRequest]: #class-request "HTTPRequest" -[HTTPResponse]: #class-response "HTTPRHTTPesponse" +[HTTPRequest]: #class-httprequest "HTTPRequest" +[HTTPResponse]: #class-httpresponse "HTTPResponse" [SecurityDetails]: #class-securitydetails "SecurityDetails" [Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable" [Target]: #class-target "Target" diff --git a/remote/test/puppeteer/docs/troubleshooting.md b/remote/test/puppeteer/docs/troubleshooting.md index 6e7adc2e59bf9..3b37540a7c446 100644 --- a/remote/test/puppeteer/docs/troubleshooting.md +++ b/remote/test/puppeteer/docs/troubleshooting.md @@ -21,7 +21,7 @@ ## Chrome headless doesn't launch on Windows -Some [chrome policies](https://support.google.com/chrome/a/answer/7532015?hl=en) might enforce running Chrome/Chromium +Some [chrome policies](https://support.google.com/chrome/a/answer/7532015) might enforce running Chrome/Chromium with certain extensions. Puppeteer passes `--disable-extensions` flag by default and will fail to launch when such policies are active. diff --git a/remote/test/puppeteer/install.js b/remote/test/puppeteer/install.js index cf56da410ea48..402a72e0d2f60 100644 --- a/remote/test/puppeteer/install.js +++ b/remote/test/puppeteer/install.js @@ -25,221 +25,62 @@ */ const compileTypeScriptIfRequired = require('./typescript-if-required'); -const os = require('os'); - -const firefoxVersions = - 'https://product-details.mozilla.org/1.0/firefox_versions.json'; -const supportedProducts = { - chrome: 'Chromium', - firefox: 'Firefox Nightly', -}; async function download() { await compileTypeScriptIfRequired(); - - const downloadHost = - process.env.PUPPETEER_DOWNLOAD_HOST || - process.env.npm_config_puppeteer_download_host || - process.env.npm_package_config_puppeteer_download_host; - const puppeteer = require('.'); - const product = - process.env.PUPPETEER_PRODUCT || - process.env.npm_config_puppeteer_product || - process.env.npm_package_config_puppeteer_product || - 'chrome'; - const browserFetcher = puppeteer.createBrowserFetcher({ - product, - host: downloadHost, - }); - const revision = await getRevision(); - await fetchBinary(revision); - - function getRevision() { - if (product === 'chrome') { - return ( - process.env.PUPPETEER_CHROMIUM_REVISION || - process.env.npm_config_puppeteer_chromium_revision || - process.env.npm_package_config_puppeteer_chromium_revision || - require('./package.json').puppeteer.chromium_revision - ); - } else if (product === 'firefox') { - puppeteer._preferredRevision = require('./package.json').puppeteer.firefox_revision; - return getFirefoxNightlyVersion(browserFetcher.host()).catch((error) => { - console.error(error); - process.exit(1); - }); - } else { - throw new Error(`Unsupported product ${product}`); - } + // need to ensure TS is compiled before loading the installer + const { downloadBrowser, logPolitely } = require('./lib/cjs/install'); + + if (process.env.PUPPETEER_SKIP_DOWNLOAD) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.' + ); + return; } - - function fetchBinary(revision) { - const revisionInfo = browserFetcher.revisionInfo(revision); - - // Do nothing if the revision is already downloaded. - if (revisionInfo.local) { - logPolitely( - `${supportedProducts[product]} is already in ${revisionInfo.folderPath}; skipping download.` - ); - return; - } - - // Override current environment proxy settings with npm configuration, if any. - const NPM_HTTPS_PROXY = - process.env.npm_config_https_proxy || process.env.npm_config_proxy; - const NPM_HTTP_PROXY = - process.env.npm_config_http_proxy || process.env.npm_config_proxy; - const NPM_NO_PROXY = process.env.npm_config_no_proxy; - - if (NPM_HTTPS_PROXY) process.env.HTTPS_PROXY = NPM_HTTPS_PROXY; - if (NPM_HTTP_PROXY) process.env.HTTP_PROXY = NPM_HTTP_PROXY; - if (NPM_NO_PROXY) process.env.NO_PROXY = NPM_NO_PROXY; - - /** - * @param {!Array} - * @returns {!Promise} - */ - function onSuccess(localRevisions) { - if (os.arch() !== 'arm64') { - logPolitely( - `${supportedProducts[product]} (${revisionInfo.revision}) downloaded to ${revisionInfo.folderPath}` - ); - } - localRevisions = localRevisions.filter( - (revision) => revision !== revisionInfo.revision - ); - const cleanupOldVersions = localRevisions.map((revision) => - browserFetcher.remove(revision) - ); - Promise.all([...cleanupOldVersions]); - } - - /** - * @param {!Error} error - */ - function onError(error) { - console.error( - `ERROR: Failed to set up ${supportedProducts[product]} r${revision}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.` - ); - console.error(error); - process.exit(1); - } - - let progressBar = null; - let lastDownloadedBytes = 0; - function onProgress(downloadedBytes, totalBytes) { - if (!progressBar) { - const ProgressBar = require('progress'); - progressBar = new ProgressBar( - `Downloading ${ - supportedProducts[product] - } r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, - { - complete: '=', - incomplete: ' ', - width: 20, - total: totalBytes, - } - ); - } - const delta = downloadedBytes - lastDownloadedBytes; - lastDownloadedBytes = downloadedBytes; - progressBar.tick(delta); - } - - return browserFetcher - .download(revisionInfo.revision, onProgress) - .then(() => browserFetcher.localRevisions()) - .then(onSuccess) - .catch(onError); + if ( + process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD || + process.env.npm_config_puppeteer_skip_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.' + ); + return; } - - function toMegabytes(bytes) { - const mb = bytes / 1024 / 1024; - return `${Math.round(mb * 10) / 10} Mb`; + if ( + process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD || + process.env.npm_package_config_puppeteer_skip_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.' + ); + return; } - - function getFirefoxNightlyVersion(host) { - const https = require('https'); - const promise = new Promise((resolve, reject) => { - let data = ''; - logPolitely(`Requesting latest Firefox Nightly version from ${host}`); - https - .get(firefoxVersions, (r) => { - if (r.statusCode >= 400) - return reject(new Error(`Got status code ${r.statusCode}`)); - r.on('data', (chunk) => { - data += chunk; - }); - r.on('end', () => { - try { - const versions = JSON.parse(data); - return resolve(versions.FIREFOX_NIGHTLY); - } catch { - return reject(new Error('Firefox version not found')); - } - }); - }) - .on('error', reject); - }); - return promise; + if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.' + ); + return; + } + if ( + process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || + process.env.npm_config_puppeteer_skip_chromium_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.' + ); + return; + } + if ( + process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || + process.env.npm_package_config_puppeteer_skip_chromium_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.' + ); + return; } -} - -function logPolitely(toBeLogged) { - const logLevel = process.env.npm_config_loglevel; - const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1; - - if (!logLevelDisplay) console.log(toBeLogged); -} -if (process.env.PUPPETEER_SKIP_DOWNLOAD) { - logPolitely( - '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.' - ); - return; -} -if ( - process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD || - process.env.npm_config_puppeteer_skip_download -) { - logPolitely( - '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.' - ); - return; -} -if ( - process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD || - process.env.npm_package_config_puppeteer_skip_download -) { - logPolitely( - '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.' - ); - return; -} -if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) { - logPolitely( - '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.' - ); - return; -} -if ( - process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || - process.env.npm_config_puppeteer_skip_chromium_download -) { - logPolitely( - '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.' - ); - return; -} -if ( - process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || - process.env.npm_package_config_puppeteer_skip_chromium_download -) { - logPolitely( - '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.' - ); - return; + downloadBrowser(); } download(); diff --git a/remote/test/puppeteer/moz.yaml b/remote/test/puppeteer/moz.yaml index f84d05fe35d79..4353440366025 100644 --- a/remote/test/puppeteer/moz.yaml +++ b/remote/test/puppeteer/moz.yaml @@ -5,6 +5,6 @@ origin: description: Headless Chrome Node API license: Apache-2.0 name: puppeteer - release: f7f3f2803d628c25c3f8c0a4480d06732335eb79 + release: 6d06adea10572d2667a9d1c5a48193903860341b url: https://github.com/mjzffr/puppeteer.git schema: 1 diff --git a/remote/test/puppeteer/new-docs/puppeteer.boxmodel.md b/remote/test/puppeteer/new-docs/puppeteer.boxmodel.md index ffdd69fa81896..04414d0adac55 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.boxmodel.md +++ b/remote/test/puppeteer/new-docs/puppeteer.boxmodel.md @@ -4,7 +4,6 @@ ## BoxModel interface - Signature: ```typescript diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.download.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.download.md index 15679730e6d64..81725d09ddff9 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.download.md +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.download.md @@ -21,7 +21,7 @@ download(revision: string, progressCallback?: (x: number, y: number) => void): P Returns: -Promise<BrowserFetcherRevisionInfo> +Promise<[BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md)> A promise with revision information when the revision is downloaded and extracted. diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.platform.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.platform.md index 92303f46888af..84e37a79101c2 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.platform.md +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.platform.md @@ -11,7 +11,7 @@ platform(): Platform; ``` Returns: -Platform +[Platform](./puppeteer.platform.md) Returns the current `Platform`. diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.product.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.product.md index 861c9b7864dcf..4066dfce915b0 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.product.md +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.product.md @@ -11,7 +11,7 @@ product(): Product; ``` Returns: -Product +[Product](./puppeteer.product.md) Returns the current `Product`. diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.revisioninfo.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.revisioninfo.md index c72e30ffa3e49..88d4ecadf18d8 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.revisioninfo.md +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcher.revisioninfo.md @@ -18,7 +18,7 @@ revisionInfo(revision: string): BrowserFetcherRevisionInfo; Returns: -BrowserFetcherRevisionInfo +[BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) The revision info for the given revision. diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcheroptions.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcheroptions.md index 71205e55d95ef..58d14dd54b39d 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.browserfetcheroptions.md +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcheroptions.md @@ -4,6 +4,7 @@ ## BrowserFetcherOptions interface + Signature: ```typescript @@ -16,6 +17,6 @@ export interface BrowserFetcherOptions | --- | --- | --- | | [host](./puppeteer.browserfetcheroptions.host.md) | string | | | [path](./puppeteer.browserfetcheroptions.path.md) | string | | -| [platform](./puppeteer.browserfetcheroptions.platform.md) | Platform | | +| [platform](./puppeteer.browserfetcheroptions.platform.md) | [Platform](./puppeteer.platform.md) | | | [product](./puppeteer.browserfetcheroptions.product.md) | string | | diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.executablepath.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.executablepath.md new file mode 100644 index 0000000000000..ef45fac9adaf0 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.executablepath.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [executablePath](./puppeteer.browserfetcherrevisioninfo.executablepath.md) + +## BrowserFetcherRevisionInfo.executablePath property + +Signature: + +```typescript +executablePath: string; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.folderpath.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.folderpath.md new file mode 100644 index 0000000000000..c4ab7a5174672 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.folderpath.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [folderPath](./puppeteer.browserfetcherrevisioninfo.folderpath.md) + +## BrowserFetcherRevisionInfo.folderPath property + +Signature: + +```typescript +folderPath: string; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.local.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.local.md new file mode 100644 index 0000000000000..eed4b9cb20f54 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.local.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [local](./puppeteer.browserfetcherrevisioninfo.local.md) + +## BrowserFetcherRevisionInfo.local property + +Signature: + +```typescript +local: boolean; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.md new file mode 100644 index 0000000000000..d17fc3dfecd2f --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) + +## BrowserFetcherRevisionInfo interface + + +Signature: + +```typescript +export interface BrowserFetcherRevisionInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [executablePath](./puppeteer.browserfetcherrevisioninfo.executablepath.md) | string | | +| [folderPath](./puppeteer.browserfetcherrevisioninfo.folderpath.md) | string | | +| [local](./puppeteer.browserfetcherrevisioninfo.local.md) | boolean | | +| [product](./puppeteer.browserfetcherrevisioninfo.product.md) | string | | +| [revision](./puppeteer.browserfetcherrevisioninfo.revision.md) | string | | +| [url](./puppeteer.browserfetcherrevisioninfo.url.md) | string | | + diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.product.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.product.md new file mode 100644 index 0000000000000..eb32e35bf97c3 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.product.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [product](./puppeteer.browserfetcherrevisioninfo.product.md) + +## BrowserFetcherRevisionInfo.product property + +Signature: + +```typescript +product: string; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.revision.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.revision.md new file mode 100644 index 0000000000000..9fa0d1d57ccf6 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.revision.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [revision](./puppeteer.browserfetcherrevisioninfo.revision.md) + +## BrowserFetcherRevisionInfo.revision property + +Signature: + +```typescript +revision: string; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.url.md b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.url.md new file mode 100644 index 0000000000000..aced0bfccfbec --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browserfetcherrevisioninfo.url.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [url](./puppeteer.browserfetcherrevisioninfo.url.md) + +## BrowserFetcherRevisionInfo.url property + +Signature: + +```typescript +url: string; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.browseroptions.defaultviewport.md b/remote/test/puppeteer/new-docs/puppeteer.browseroptions.defaultviewport.md new file mode 100644 index 0000000000000..5cb4035c05ff6 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browseroptions.defaultviewport.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserOptions](./puppeteer.browseroptions.md) > [defaultViewport](./puppeteer.browseroptions.defaultviewport.md) + +## BrowserOptions.defaultViewport property + +Signature: + +```typescript +defaultViewport?: Viewport; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.browseroptions.ignorehttpserrors.md b/remote/test/puppeteer/new-docs/puppeteer.browseroptions.ignorehttpserrors.md new file mode 100644 index 0000000000000..a1951ddedc9c1 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browseroptions.ignorehttpserrors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserOptions](./puppeteer.browseroptions.md) > [ignoreHTTPSErrors](./puppeteer.browseroptions.ignorehttpserrors.md) + +## BrowserOptions.ignoreHTTPSErrors property + +Signature: + +```typescript +ignoreHTTPSErrors?: boolean; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.browseroptions.md b/remote/test/puppeteer/new-docs/puppeteer.browseroptions.md new file mode 100644 index 0000000000000..3decd6b943566 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browseroptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserOptions](./puppeteer.browseroptions.md) + +## BrowserOptions interface + +Generic browser options that can be passed when launching any browser. + +Signature: + +```typescript +export interface BrowserOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [defaultViewport](./puppeteer.browseroptions.defaultviewport.md) | Viewport | | +| [ignoreHTTPSErrors](./puppeteer.browseroptions.ignorehttpserrors.md) | boolean | | +| [slowMo](./puppeteer.browseroptions.slowmo.md) | number | | + diff --git a/remote/test/puppeteer/new-docs/puppeteer.browseroptions.slowmo.md b/remote/test/puppeteer/new-docs/puppeteer.browseroptions.slowmo.md new file mode 100644 index 0000000000000..27a83884afac7 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.browseroptions.slowmo.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserOptions](./puppeteer.browseroptions.md) > [slowMo](./puppeteer.browseroptions.slowmo.md) + +## BrowserOptions.slowMo property + +Signature: + +```typescript +slowMo?: number; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.args.md b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.args.md new file mode 100644 index 0000000000000..5ae5f7706d127 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.args.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ChromeArgOptions](./puppeteer.chromeargoptions.md) > [args](./puppeteer.chromeargoptions.args.md) + +## ChromeArgOptions.args property + +Signature: + +```typescript +args?: string[]; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.devtools.md b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.devtools.md new file mode 100644 index 0000000000000..b1cc7c89194dd --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.devtools.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ChromeArgOptions](./puppeteer.chromeargoptions.md) > [devtools](./puppeteer.chromeargoptions.devtools.md) + +## ChromeArgOptions.devtools property + +Signature: + +```typescript +devtools?: boolean; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.headless.md b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.headless.md new file mode 100644 index 0000000000000..8636f6e67aa07 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.headless.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ChromeArgOptions](./puppeteer.chromeargoptions.md) > [headless](./puppeteer.chromeargoptions.headless.md) + +## ChromeArgOptions.headless property + +Signature: + +```typescript +headless?: boolean; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.md b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.md new file mode 100644 index 0000000000000..27e52fd0fa1ad --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ChromeArgOptions](./puppeteer.chromeargoptions.md) + +## ChromeArgOptions interface + +Launcher options that only apply to Chrome. + +Signature: + +```typescript +export interface ChromeArgOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [args](./puppeteer.chromeargoptions.args.md) | string\[\] | | +| [devtools](./puppeteer.chromeargoptions.devtools.md) | boolean | | +| [headless](./puppeteer.chromeargoptions.headless.md) | boolean | | +| [userDataDir](./puppeteer.chromeargoptions.userdatadir.md) | string | | + diff --git a/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.userdatadir.md b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.userdatadir.md new file mode 100644 index 0000000000000..8d5469e240ee9 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.chromeargoptions.userdatadir.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ChromeArgOptions](./puppeteer.chromeargoptions.md) > [userDataDir](./puppeteer.chromeargoptions.userdatadir.md) + +## ChromeArgOptions.userDataDir property + +Signature: + +```typescript +userDataDir?: string; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverage._jscoverage.md b/remote/test/puppeteer/new-docs/puppeteer.coverage._jscoverage.md deleted file mode 100644 index d3b3dd4095e24..0000000000000 --- a/remote/test/puppeteer/new-docs/puppeteer.coverage._jscoverage.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [puppeteer](./puppeteer.md) > [Coverage](./puppeteer.coverage.md) > [\_jsCoverage](./puppeteer.coverage._jscoverage.md) - -## Coverage.\_jsCoverage property - -Signature: - -```typescript -_jsCoverage: JSCoverage; -``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverage.md b/remote/test/puppeteer/new-docs/puppeteer.coverage.md index 947f94e9ed27d..eb041d32a4cc1 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.coverage.md +++ b/remote/test/puppeteer/new-docs/puppeteer.coverage.md @@ -4,25 +4,53 @@ ## Coverage class +The Coverage class provides methods to gathers information about parts of JavaScript and CSS that were used by the page. + Signature: ```typescript export declare class Coverage ``` +## Remarks + +To output coverage in a form consumable by [Istanbul](https://github.com/istanbuljs), see [puppeteer-to-istanbul](https://github.com/istanbuljs/puppeteer-to-istanbul). + +## Example + +An example of using JavaScript and CSS coverage to get percentage of initially executed code: + +```js +// Enable both JavaScript and CSS coverage +await Promise.all([ + page.coverage.startJSCoverage(), + page.coverage.startCSSCoverage() +]); +// Navigate to page +await page.goto('https://example.com'); +// Disable both JavaScript and CSS coverage +const [jsCoverage, cssCoverage] = await Promise.all([ + page.coverage.stopJSCoverage(), + page.coverage.stopCSSCoverage(), +]); +let totalBytes = 0; +let usedBytes = 0; +const coverage = [...jsCoverage, ...cssCoverage]; +for (const entry of coverage) { + totalBytes += entry.text.length; + for (const range of entry.ranges) + usedBytes += range.end - range.start - 1; +} +console.log(`Bytes used: ${usedBytes / totalBytes * 100}%`); + +``` + ## Constructors | Constructor | Modifiers | Description | | --- | --- | --- | | [(constructor)(client)](./puppeteer.coverage._constructor_.md) | | Constructs a new instance of the Coverage class | -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [\_cssCoverage](./puppeteer.coverage._csscoverage.md) | | CSSCoverage | | -| [\_jsCoverage](./puppeteer.coverage._jscoverage.md) | | JSCoverage | | - ## Methods | Method | Modifiers | Description | diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverage.startcsscoverage.md b/remote/test/puppeteer/new-docs/puppeteer.coverage.startcsscoverage.md index 5091ba68695b9..673535d72d88c 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.coverage.startcsscoverage.md +++ b/remote/test/puppeteer/new-docs/puppeteer.coverage.startcsscoverage.md @@ -7,18 +7,18 @@ Signature: ```typescript -startCSSCoverage(options?: { - resetOnNavigation?: boolean; - }): Promise; +startCSSCoverage(options?: CSSCoverageOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| options | { resetOnNavigation?: boolean; } | | +| options | [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) | defaults to { resetOnNavigation : true } | Returns: Promise<void> +Promise that resolves when coverage is started. + diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverage.startjscoverage.md b/remote/test/puppeteer/new-docs/puppeteer.coverage.startjscoverage.md index c7b878cdf17bb..cc24db4b1de97 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.coverage.startjscoverage.md +++ b/remote/test/puppeteer/new-docs/puppeteer.coverage.startjscoverage.md @@ -7,19 +7,22 @@ Signature: ```typescript -startJSCoverage(options?: { - resetOnNavigation?: boolean; - reportAnonymousScripts?: boolean; - }): Promise; +startJSCoverage(options?: JSCoverageOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| options | { resetOnNavigation?: boolean; reportAnonymousScripts?: boolean; } | | +| options | [JSCoverageOptions](./puppeteer.jscoverageoptions.md) | defaults to { resetOnNavigation : true, reportAnonymousScripts : false } | Returns: Promise<void> +Promise that resolves when coverage is started. + +## Remarks + +Anonymous scripts are ones that don't have an associated url. These are scripts that are dynamically created on the page using `eval` or `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous scripts will have `__puppeteer_evaluation_script__` as their URL. + diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverage.stopcsscoverage.md b/remote/test/puppeteer/new-docs/puppeteer.coverage.stopcsscoverage.md index 1134d25f091f9..9b9537ce4eb15 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.coverage.stopcsscoverage.md +++ b/remote/test/puppeteer/new-docs/puppeteer.coverage.stopcsscoverage.md @@ -11,5 +11,11 @@ stopCSSCoverage(): Promise; ``` Returns: -Promise<CoverageEntry\[\]> +Promise<[CoverageEntry](./puppeteer.coverageentry.md)\[\]> + +Promise that resolves to the array of coverage reports for all stylesheets. + +## Remarks + +CSS Coverage doesn't include dynamically injected style tags without sourceURLs. diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverage.stopjscoverage.md b/remote/test/puppeteer/new-docs/puppeteer.coverage.stopjscoverage.md index aec5c0dd8ae8e..6f126873878ff 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.coverage.stopjscoverage.md +++ b/remote/test/puppeteer/new-docs/puppeteer.coverage.stopjscoverage.md @@ -11,5 +11,11 @@ stopJSCoverage(): Promise; ``` Returns: -Promise<CoverageEntry\[\]> +Promise<[CoverageEntry](./puppeteer.coverageentry.md)\[\]> + +Promise that resolves to the array of coverage reports for all scripts. + +## Remarks + +JavaScript Coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are reported. diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverageentry.md b/remote/test/puppeteer/new-docs/puppeteer.coverageentry.md new file mode 100644 index 0000000000000..96a6b6742160d --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.coverageentry.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CoverageEntry](./puppeteer.coverageentry.md) + +## CoverageEntry interface + +The CoverageEntry class represents one entry of the coverage report. + +Signature: + +```typescript +export interface CoverageEntry +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [ranges](./puppeteer.coverageentry.ranges.md) | Array<{ start: number; end: number; }> | The covered range as start and end positions. | +| [text](./puppeteer.coverageentry.text.md) | string | The content of the style sheet or script. | +| [url](./puppeteer.coverageentry.url.md) | string | The URL of the style sheet or script. | + diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverageentry.ranges.md b/remote/test/puppeteer/new-docs/puppeteer.coverageentry.ranges.md new file mode 100644 index 0000000000000..2e0cfae91842b --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.coverageentry.ranges.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CoverageEntry](./puppeteer.coverageentry.md) > [ranges](./puppeteer.coverageentry.ranges.md) + +## CoverageEntry.ranges property + +The covered range as start and end positions. + +Signature: + +```typescript +ranges: Array<{ + start: number; + end: number; + }>; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverageentry.text.md b/remote/test/puppeteer/new-docs/puppeteer.coverageentry.text.md new file mode 100644 index 0000000000000..c01b4246d4d43 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.coverageentry.text.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CoverageEntry](./puppeteer.coverageentry.md) > [text](./puppeteer.coverageentry.text.md) + +## CoverageEntry.text property + +The content of the style sheet or script. + +Signature: + +```typescript +text: string; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.coverage._csscoverage.md b/remote/test/puppeteer/new-docs/puppeteer.coverageentry.url.md similarity index 50% rename from remote/test/puppeteer/new-docs/puppeteer.coverage._csscoverage.md rename to remote/test/puppeteer/new-docs/puppeteer.coverageentry.url.md index df7c773d61e3e..cfd89f611fdbd 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.coverage._csscoverage.md +++ b/remote/test/puppeteer/new-docs/puppeteer.coverageentry.url.md @@ -1,11 +1,13 @@ -[Home](./index.md) > [puppeteer](./puppeteer.md) > [Coverage](./puppeteer.coverage.md) > [\_cssCoverage](./puppeteer.coverage._csscoverage.md) +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CoverageEntry](./puppeteer.coverageentry.md) > [url](./puppeteer.coverageentry.url.md) -## Coverage.\_cssCoverage property +## CoverageEntry.url property + +The URL of the style sheet or script. Signature: ```typescript -_cssCoverage: CSSCoverage; +url: string; ``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.csscoverageoptions.md b/remote/test/puppeteer/new-docs/puppeteer.csscoverageoptions.md new file mode 100644 index 0000000000000..875f9f0425850 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.csscoverageoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) + +## CSSCoverageOptions interface + +Set of configurable options for CSS coverage. + +Signature: + +```typescript +export interface CSSCoverageOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [resetOnNavigation](./puppeteer.csscoverageoptions.resetonnavigation.md) | boolean | Whether to reset coverage on every navigation. | + diff --git a/remote/test/puppeteer/new-docs/puppeteer.csscoverageoptions.resetonnavigation.md b/remote/test/puppeteer/new-docs/puppeteer.csscoverageoptions.resetonnavigation.md new file mode 100644 index 0000000000000..7bc036974910c --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.csscoverageoptions.resetonnavigation.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) > [resetOnNavigation](./puppeteer.csscoverageoptions.resetonnavigation.md) + +## CSSCoverageOptions.resetOnNavigation property + +Whether to reset coverage on every navigation. + +Signature: + +```typescript +resetOnNavigation?: boolean; +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.devicesmap.md b/remote/test/puppeteer/new-docs/puppeteer.devicesmap.md new file mode 100644 index 0000000000000..e0b069cc82c61 --- /dev/null +++ b/remote/test/puppeteer/new-docs/puppeteer.devicesmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [devicesMap](./puppeteer.devicesmap.md) + +## devicesMap variable + +Signature: + +```typescript +devicesMap: DevicesMap +``` diff --git a/remote/test/puppeteer/new-docs/puppeteer.elementhandle._.md b/remote/test/puppeteer/new-docs/puppeteer.elementhandle._.md index 6be4b5ecd7b3e..a6d0df975825d 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.elementhandle._.md +++ b/remote/test/puppeteer/new-docs/puppeteer.elementhandle._.md @@ -4,7 +4,7 @@ ## ElementHandle.$() method -The method runs `element.querySelector` within the page. If no element matches the selector, the return value resolves to `null`. +Runs `element.querySelector` within the page. If no element matches the selector, the return value resolves to `null`. Signature: diff --git a/remote/test/puppeteer/new-docs/puppeteer.elementhandle.__.md b/remote/test/puppeteer/new-docs/puppeteer.elementhandle.__.md index 34bfb5096f424..41c9843bd3c41 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.elementhandle.__.md +++ b/remote/test/puppeteer/new-docs/puppeteer.elementhandle.__.md @@ -4,7 +4,7 @@ ## ElementHandle.$$() method -The method runs `element.querySelectorAll` within the page. If no elements match the selector, the return value resolves to `[]`. +Runs `element.querySelectorAll` within the page. If no elements match the selector, the return value resolves to `[]`. Signature: diff --git a/remote/test/puppeteer/new-docs/puppeteer.elementhandle.__eval.md b/remote/test/puppeteer/new-docs/puppeteer.elementhandle.__eval.md index 62184b0bf0cd5..f351b5ef70c81 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.elementhandle.__eval.md +++ b/remote/test/puppeteer/new-docs/puppeteer.elementhandle.__eval.md @@ -11,7 +11,7 @@ If `pageFunction` returns a Promise, then `frame.$$eval` would wait for the prom Signature: ```typescript -$$eval(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise; +$$eval(selector: string, pageFunction: EvaluateFn | string, ...args: SerializableOrJSHandle[]): Promise; ``` ## Parameters @@ -19,8 +19,8 @@ $$eval(selector: string, pageFunction: Function | string | Parameter | Type | Description | | --- | --- | --- | | selector | string | | -| pageFunction | Function \| string | | -| args | unknown\[\] | | +| pageFunction | [EvaluateFn](./puppeteer.evaluatefn.md) \| string | | +| args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | | Returns: diff --git a/remote/test/puppeteer/new-docs/puppeteer.elementhandle._eval.md b/remote/test/puppeteer/new-docs/puppeteer.elementhandle._eval.md index 51719ae1bfd56..4de0423e19c74 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.elementhandle._eval.md +++ b/remote/test/puppeteer/new-docs/puppeteer.elementhandle._eval.md @@ -11,7 +11,7 @@ If `pageFunction` returns a Promise, then `frame.$eval` would wait for the promi Signature: ```typescript -$eval(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise; +$eval(selector: string, pageFunction: (element: Element, ...args: unknown[]) => ReturnType | Promise, ...args: SerializableOrJSHandle[]): Promise>; ``` ## Parameters @@ -19,12 +19,12 @@ $eval(selector: string, pageFunction: Function | string, | Parameter | Type | Description | | --- | --- | --- | | selector | string | | -| pageFunction | Function \| string | | -| args | unknown\[\] | | +| pageFunction | (element: Element, ...args: unknown\[\]) => ReturnType \| Promise<ReturnType> | | +| args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | | Returns: -Promise<ReturnType> +Promise<[WrapElementHandle](./puppeteer.wrapelementhandle.md)<ReturnType>> ## Example diff --git a/remote/test/puppeteer/new-docs/puppeteer.elementhandle.aselement.md b/remote/test/puppeteer/new-docs/puppeteer.elementhandle.aselement.md index 0abe674ad140e..670b0f824b8ab 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.elementhandle.aselement.md +++ b/remote/test/puppeteer/new-docs/puppeteer.elementhandle.aselement.md @@ -7,9 +7,9 @@ Signature: ```typescript -asElement(): ElementHandle | null; +asElement(): ElementHandle | null; ``` Returns: -[ElementHandle](./puppeteer.elementhandle.md) \| null +[ElementHandle](./puppeteer.elementhandle.md)<ElementType> \| null diff --git a/remote/test/puppeteer/new-docs/puppeteer.elementhandle.md b/remote/test/puppeteer/new-docs/puppeteer.elementhandle.md index a28c12deee48c..09e5e97085b5f 100644 --- a/remote/test/puppeteer/new-docs/puppeteer.elementhandle.md +++ b/remote/test/puppeteer/new-docs/puppeteer.elementhandle.md @@ -9,7 +9,7 @@ ElementHandle represents an in-page DOM element. Signature: ```typescript -export declare class ElementHandle extends JSHandle +export declare class ElementHandle extends JSHandle ``` Extends: [JSHandle](./puppeteer.jshandle.md) @@ -30,18 +30,20 @@ const puppeteer = require('puppeteer'); })(); ``` -ElementHandle prevents DOM element from garbage collection unless the handle is [disposed](./puppeteer.jshandle.dispose.md). ElementHandles are auto-disposed when their origin frame gets navigated. +ElementHandle prevents the DOM element from being garbage-collected unless the handle is [disposed](./puppeteer.jshandle.dispose.md). ElementHandles are auto-disposed when their origin frame gets navigated. ElementHandle instances can be used as arguments in [Page.$eval()](./puppeteer.page._eval.md) and [Page.evaluate()](./puppeteer.page.evaluate.md) methods. +If you're using TypeScript, ElementHandle takes a generic argument that denotes the type of element the handle is holding within. For example, if you have a handle to a `` element, you can type it as + * `ElementHandle` and you get some nicer type checks. + * * @public */ -export class ElementHandle extends JSHandle { +export class ElementHandle< + ElementType extends Element = Element +> extends JSHandle { private _page: Page; private _frameManager: FrameManager; @@ -290,7 +347,7 @@ export class ElementHandle extends JSHandle { this._frameManager = frameManager; } - asElement(): ElementHandle | null { + asElement(): ElementHandle | null { return this; } @@ -307,46 +364,48 @@ export class ElementHandle extends JSHandle { } private async _scrollIntoViewIfNeeded(): Promise { - const error = await this.evaluate>( - async (element: HTMLElement, pageJavascriptEnabled: boolean) => { - if (!element.isConnected) return 'Node is detached from document'; - if (element.nodeType !== Node.ELEMENT_NODE) - return 'Node is not of type HTMLElement'; - // force-scroll if page's javascript is disabled. - if (!pageJavascriptEnabled) { - element.scrollIntoView({ - block: 'center', - inline: 'center', - // Chrome still supports behavior: instant but it's not in the spec - // so TS shouts We don't want to make this breaking change in - // Puppeteer yet so we'll ignore the line. - // @ts-ignore - behavior: 'instant', - }); - return false; - } - const visibleRatio = await new Promise((resolve) => { - const observer = new IntersectionObserver((entries) => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); - }); - observer.observe(element); + const error = await this.evaluate< + ( + element: Element, + pageJavascriptEnabled: boolean + ) => Promise + >(async (element, pageJavascriptEnabled) => { + if (!element.isConnected) return 'Node is detached from document'; + if (element.nodeType !== Node.ELEMENT_NODE) + return 'Node is not of type HTMLElement'; + // force-scroll if page's javascript is disabled. + if (!pageJavascriptEnabled) { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // Chrome still supports behavior: instant but it's not in the spec + // so TS shouts We don't want to make this breaking change in + // Puppeteer yet so we'll ignore the line. + // @ts-ignore + behavior: 'instant', }); - if (visibleRatio !== 1.0) { - element.scrollIntoView({ - block: 'center', - inline: 'center', - // Chrome still supports behavior: instant but it's not in the spec - // so TS shouts We don't want to make this breaking change in - // Puppeteer yet so we'll ignore the line. - // @ts-ignore - behavior: 'instant', - }); - } return false; - }, - this._page.isJavaScriptEnabled() - ); + } + const visibleRatio = await new Promise((resolve) => { + const observer = new IntersectionObserver((entries) => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + if (visibleRatio !== 1.0) { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // Chrome still supports behavior: instant but it's not in the spec + // so TS shouts We don't want to make this breaking change in + // Puppeteer yet so we'll ignore the line. + // @ts-ignore + behavior: 'instant', + }); + } + return false; + }, this._page.isJavaScriptEnabled()); if (error) throw new Error(error); } @@ -461,11 +520,9 @@ export class ElementHandle extends JSHandle { '"' ); - /* TODO(jacktfranklin@): once ExecutionContext is TypeScript, and - * its evaluate function is properly typed with generics we can - * return here and remove the typecasting - */ - return this.evaluate((element: HTMLSelectElement, values: string[]) => { + return this.evaluate< + (element: HTMLSelectElement, values: string[]) => string[] + >((element, values) => { if (element.nodeName.toLowerCase() !== 'select') throw new Error('Element is not a ' @@ -531,7 +588,7 @@ export class ElementHandle extends JSHandle { // not actually update the files in that case, so the solution is to eval the element // value to a new FileList directly. if (files.length === 0) { - await this.evaluate((element: HTMLInputElement) => { + await this.evaluate<(element: HTMLInputElement) => void>((element) => { element.files = new DataTransfer().files; // Dispatch events for this case because it should behave akin to a user action. @@ -711,8 +768,8 @@ export class ElementHandle extends JSHandle { } /** - * The method runs `element.querySelector` within the page. If no element matches - * the selector, the return value resolves to `null`. + * Runs `element.querySelector` within the page. If no element matches the selector, + * the return value resolves to `null`. */ async $(selector: string): Promise { const defaultHandler = (element: Element, selector: string) => @@ -730,8 +787,8 @@ export class ElementHandle extends JSHandle { } /** - * The method runs `element.querySelectorAll` within the page. If no elements match - * the selector, the return value resolves to `[]`. + * Runs `element.querySelectorAll` within the page. If no elements match the selector, + * the return value resolves to `[]`. */ async $$(selector: string): Promise { const defaultHandler = (element: Element, selector: string) => @@ -770,22 +827,36 @@ export class ElementHandle extends JSHandle { * expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe('10'); * ``` */ - async $eval( + async $eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] - ): Promise { + pageFunction: ( + element: Element, + ...args: unknown[] + ) => ReturnType | Promise, + ...args: SerializableOrJSHandle[] + ): Promise> { const elementHandle = await this.$(selector); if (!elementHandle) throw new Error( `Error: failed to find element matching selector "${selector}"` ); - const result = await elementHandle.evaluate( - pageFunction, - ...args - ); + const result = await elementHandle.evaluate< + ( + element: Element, + ...args: SerializableOrJSHandle[] + ) => ReturnType | Promise + >(pageFunction, ...args); await elementHandle.dispose(); - return result; + + /** + * This as is a little unfortunate but helps TS understand the behavour of + * `elementHandle.evaluate`. If evalute returns an element it will return an + * ElementHandle instance, rather than the plain object. All the + * WrapElementHandle type does is wrap ReturnType into + * ElementHandle if it is an ElementHandle, or leave it alone as + * ReturnType if it isn't. + */ + return result as WrapElementHandle; } /** @@ -813,8 +884,8 @@ export class ElementHandle extends JSHandle { */ async $$eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { const defaultHandler = (element: Element, selector: string) => Array.from(element.querySelectorAll(selector)); @@ -827,7 +898,7 @@ export class ElementHandle extends JSHandle { queryHandler, updatedSelector ); - const result = await arrayHandle.evaluate( + const result = await arrayHandle.evaluate<(...args: any[]) => ReturnType>( pageFunction, ...args ); @@ -841,19 +912,22 @@ export class ElementHandle extends JSHandle { * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate} */ async $x(expression: string): Promise { - const arrayHandle = await this.evaluateHandle((element, expression) => { - const document = element.ownerDocument || element; - const iterator = document.evaluate( - expression, - element, - null, - XPathResult.ORDERED_NODE_ITERATOR_TYPE - ); - const array = []; - let item; - while ((item = iterator.iterateNext())) array.push(item); - return array; - }, expression); + const arrayHandle = await this.evaluateHandle( + (element: Document, expression: string) => { + const document = element.ownerDocument || element; + const iterator = document.evaluate( + expression, + element, + null, + XPathResult.ORDERED_NODE_ITERATOR_TYPE + ); + const array = []; + let item; + while ((item = iterator.iterateNext())) array.push(item); + return array; + }, + expression + ); const properties = await arrayHandle.getProperties(); await arrayHandle.dispose(); const result = []; @@ -868,16 +942,18 @@ export class ElementHandle extends JSHandle { * Resolves to true if the element is visible in the current viewport. */ async isIntersectingViewport(): Promise { - return await this.evaluate>(async (element) => { - const visibleRatio = await new Promise((resolve) => { - const observer = new IntersectionObserver((entries) => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); + return await this.evaluate<(element: Element) => Promise>( + async (element) => { + const visibleRatio = await new Promise((resolve) => { + const observer = new IntersectionObserver((entries) => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); }); - observer.observe(element); - }); - return visibleRatio > 0; - }); + return visibleRatio > 0; + } + ); } } diff --git a/remote/test/puppeteer/src/common/Page.ts b/remote/test/puppeteer/src/common/Page.ts index 2ded9343f9f21..971bbc9fd4d9c 100644 --- a/remote/test/puppeteer/src/common/Page.ts +++ b/remote/test/puppeteer/src/common/Page.ts @@ -15,6 +15,7 @@ */ import * as fs from 'fs'; +import { promisify } from 'util'; import { EventEmitter } from './EventEmitter'; import * as mime from 'mime'; import { Events } from './Events'; @@ -22,7 +23,7 @@ import { Connection, CDPSession } from './Connection'; import { Dialog } from './Dialog'; import { EmulationManager } from './EmulationManager'; import { Frame, FrameManager } from './FrameManager'; -import { Keyboard, Mouse, Touchscreen, MouseButtonInput } from './Input'; +import { Keyboard, Mouse, Touchscreen, MouseButton } from './Input'; import { Tracing } from './Tracing'; import { assert } from './assert'; import { helper, debugError } from './helper'; @@ -41,9 +42,18 @@ import { FileChooser } from './FileChooser'; import { ConsoleMessage, ConsoleMessageType } from './ConsoleMessage'; import { PuppeteerLifeCycleEvent } from './LifecycleWatcher'; import Protocol from '../protocol'; +import { + EvaluateFn, + SerializableOrJSHandle, + EvaluateHandleFn, + WrapElementHandle, +} from './EvalTypes'; -const writeFileAsync = helper.promisify(fs.writeFile); +const writeFileAsync = promisify(fs.writeFile); +/** + * @public + */ export interface Metrics { Timestamp?: number; Documents?: number; @@ -60,11 +70,56 @@ export interface Metrics { JSHeapTotalSize?: number; } -interface WaitForOptions { +/** + * @public + */ +export interface WaitTimeoutOptions { + /** + * Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to + * disable the timeout. + * + * @remarks + * The default value can be changed by using the + * {@link Page.setDefaultTimeout} method. + */ + timeout?: number; +} + +/** + * @public + */ +export interface WaitForOptions { + /** + * Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to + * disable the timeout. + * + * @remarks + * The default value can be changed by using the + * {@link Page.setDefaultTimeout} or {@link Page.setDefaultNavigationTimeout} + * methods. + */ timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; } +/** + * @public + */ +export interface GeolocationOptions { + /** + * Latitude between -90 and 90. + */ + longitude: number; + /** + * Longitude between -180 and 180. + */ + latitude: number; + /** + * Optional non-negative accuracy value. + */ + accuracy?: number; +} + interface MediaFeature { name: string; value: string; @@ -139,6 +194,8 @@ type VisionDeficiency = /** * All the events that a page instance may emit. + * + * @public */ export const enum PageEmittedEvents { /** @@ -241,7 +298,8 @@ export class Page extends EventEmitter { private _viewport: Viewport | null; private _screenshotTaskQueue: ScreenshotTaskQueue; private _workers = new Map(); - // TODO: improve this typedef - it's a function that takes a file chooser or something? + // TODO: improve this typedef - it's a function that takes a file chooser or + // something? private _fileChooserInterceptors = new Set(); private _disconnectPromise?: Promise; @@ -367,12 +425,19 @@ export class Page extends EventEmitter { for (const interceptor of interceptors) interceptor.call(null, fileChooser); } + /** + * @returns `true` if the page has JavaScript enabled, `false` otherwise. + */ public isJavaScriptEnabled(): boolean { return this._javascriptEnabled; } + /** + * @param options - Optional waiting parameters + * @returns Resolves after a page requests a file picker. + */ async waitForFileChooser( - options: { timeout?: number } = {} + options: WaitTimeoutOptions = {} ): Promise { if (!this._fileChooserInterceptors.size) await this._client.send('Page.setInterceptFileChooserDialog', { @@ -395,11 +460,19 @@ export class Page extends EventEmitter { }); } - async setGeolocation(options: { - longitude: number; - latitude: number; - accuracy?: number; - }): Promise { + /** + * Sets the page's geolocation. + * + * @remarks + * Consider using {@link BrowserContext.overridePermissions} to grant + * permissions for the page to read its geolocation. + * + * @example + * ```js + * await page.setGeolocation({latitude: 59.95, longitude: 30.31667}); + * ``` + */ + async setGeolocation(options: GeolocationOptions): Promise { const { longitude, latitude, accuracy = 0 } = options; if (longitude < -180 || longitude > 180) throw new Error( @@ -420,14 +493,23 @@ export class Page extends EventEmitter { }); } + /** + * @returns A target this page was created from. + */ target(): Target { return this._target; } + /** + * @returns The browser this page belongs to. + */ browser(): Browser { return this._target.browser(); } + /** + * @returns The browser context that the page belongs to + */ browserContext(): BrowserContext { return this._target.browserContext(); } @@ -446,6 +528,9 @@ export class Page extends EventEmitter { ); } + /** + * @returns The page's main frame. + */ mainFrame(): Frame { return this._frameManager.mainFrame(); } @@ -470,40 +555,150 @@ export class Page extends EventEmitter { return this._accessibility; } + /** + * @returns An array of all frames attached to the page. + */ frames(): Frame[] { return this._frameManager.frames(); } + /** + * @returns all of the dedicated + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorkers} + * associated with the page. + */ workers(): WebWorker[] { return Array.from(this._workers.values()); } + /** + * @param value - Whether to enable request interception. + * + * @remarks + * Activating request interception enables {@link HTTPRequest.abort}, + * {@link HTTPRequest.continue} and {@link HTTPRequest.respond} methods. This + * provides the capability to modify network requests that are made by a page. + * + * Once request interception is enabled, every request will stall unless it's + * continued, responded or aborted. + * + * **NOTE** Enabling request interception disables page caching. + * + * @example + * An example of a naïve request interceptor that aborts all image requests: + * ```js + * const puppeteer = require('puppeteer'); + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.setRequestInterception(true); + * page.on('request', interceptedRequest => { + * if (interceptedRequest.url().endsWith('.png') || + * interceptedRequest.url().endsWith('.jpg')) + * interceptedRequest.abort(); + * else + * interceptedRequest.continue(); + * }); + * await page.goto('https://example.com'); + * await browser.close(); + * })(); + * ``` + */ async setRequestInterception(value: boolean): Promise { return this._frameManager.networkManager().setRequestInterception(value); } + /** + * @param enabled - When `true`, enables offline mode for the page. + */ setOfflineMode(enabled: boolean): Promise { return this._frameManager.networkManager().setOfflineMode(enabled); } + /** + * @param timeout - Maximum navigation time in milliseconds. + */ setDefaultNavigationTimeout(timeout: number): void { this._timeoutSettings.setDefaultNavigationTimeout(timeout); } + /** + * @param timeout - Maximum time in milliseconds. + */ setDefaultTimeout(timeout: number): void { this._timeoutSettings.setDefaultTimeout(timeout); } + /** + * Runs `document.querySelector` within the page. If no element matches the + * selector, the return value resolves to `null`. + * + * @remarks + * Shortcut for {@link Frame.$ | Page.mainFrame().$(selector) }. + * + * @param selector - A + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to query page for. + */ async $(selector: string): Promise { return this.mainFrame().$(selector); } - async evaluateHandle( - pageFunction: Function | string, - ...args: unknown[] - ): Promise { + /** + * @remarks + * + * The only difference between {@link Page.evaluate | page.evaluate} and + * `page.evaluateHandle` is that `evaluateHandle` will return the value + * wrapped in an in-page object. + * + * If the function passed to `page.evaluteHandle` returns a Promise, the + * function will wait for the promise to resolve and return its value. + * + * You can pass a string instead of a function (although functions are + * recommended as they are easier to debug and use with TypeScript): + * + * @example + * ``` + * const aHandle = await page.evaluateHandle('document') + * ``` + * + * @example + * {@link JSHandle} instances can be passed as arguments to the `pageFunction`: + * ``` + * const aHandle = await page.evaluateHandle(() => document.body); + * const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle); + * console.log(await resultHandle.jsonValue()); + * await resultHandle.dispose(); + * ``` + * + * Most of the time this function returns a {@link JSHandle}, + * but if `pageFunction` returns a reference to an element, + * you instead get an {@link ElementHandle} back: + * + * @example + * ``` + * const button = await page.evaluateHandle(() => document.querySelector('button')); + * // can call `click` because `button` is an `ElementHandle` + * await button.click(); + * ``` + * + * The TypeScript definitions assume that `evaluateHandle` returns + * a `JSHandle`, but if you know it's going to return an + * `ElementHandle`, pass it as the generic argument: + * + * ``` + * const button = await page.evaluateHandle(...); + * ``` + * + * @param pageFunction - a function that is run within the page + * @param args - arguments to be passed to the pageFunction + */ + async evaluateHandle( + pageFunction: EvaluateHandleFn, + ...args: SerializableOrJSHandle[] + ): Promise { const context = await this.mainFrame().executionContext(); - return context.evaluateHandle(pageFunction, ...args); + return context.evaluateHandle(pageFunction, ...args); } async queryObjects(prototypeHandle: JSHandle): Promise { @@ -511,18 +706,89 @@ export class Page extends EventEmitter { return context.queryObjects(prototypeHandle); } - async $eval( + /** + * This method runs `document.querySelector` within the page and passes the + * result as the first argument to the `pageFunction`. + * + * @remarks + * + * If no element is found matching `selector`, the method will throw an error. + * + * If `pageFunction` returns a promise `$eval` will wait for the promise to + * resolve and then return its value. + * + * @example + * + * ``` + * const searchValue = await page.$eval('#search', el => el.value); + * const preloadHref = await page.$eval('link[rel=preload]', el => el.href); + * const html = await page.$eval('.main-container', el => el.outerHTML); + * ``` + * + * If you are using TypeScript, you may have to provide an explicit type to the + * first argument of the `pageFunction`. + * By default it is typed as `Element`, but you may need to provide a more + * specific sub-type: + * + * @example + * + * ``` + * // if you don't provide HTMLInputElement here, TS will error + * // as `value` is not on `Element` + * const searchValue = await page.$eval('#search', (el: HTMLInputElement) => el.value); + * ``` + * + * The compiler should be able to infer the return type + * from the `pageFunction` you provide. If it is unable to, you can use the generic + * type to tell the compiler what return type you expect from `$eval`: + * + * @example + * + * ``` + * // The compiler can infer the return type in this case, but if it can't + * // or if you want to be more explicit, provide it as the generic type. + * const searchValue = await page.$eval( + * '#search', (el: HTMLInputElement) => el.value + * ); + * ``` + * + * @param selector the + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to query for + * @param pageFunction the function to be evaluated in the page context. Will + * be passed the result of `document.querySelector(selector)` as its first + * argument. + * @param args any additional arguments to pass through to `pageFunction`. + * + * @returns The result of calling `pageFunction`. If it returns an element it + * is wrapped in an {@link ElementHandle}, else the raw value itself is + * returned. + */ + async $eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] - ): Promise { + pageFunction: ( + element: Element, + /* Unfortunately this has to be unknown[] because it's hard to get + * TypeScript to understand that the arguments will be left alone unless + * they are an ElementHandle, in which case they will be unwrapped. + * The nice thing about unknown vs any is that unknown will force the user + * to type the item before using it to avoid errors. + * + * TODO(@jackfranklin): We could fix this by using overloads like + * DefinitelyTyped does: + * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/puppeteer/index.d.ts#L114 + */ + ...args: unknown[] + ) => ReturnType | Promise, + ...args: SerializableOrJSHandle[] + ): Promise> { return this.mainFrame().$eval(selector, pageFunction, ...args); } async $$eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { return this.mainFrame().$$eval(selector, pageFunction, ...args); } @@ -1291,7 +1557,7 @@ export class Page extends EventEmitter { selector: string, options: { delay?: number; - button?: MouseButtonInput; + button?: MouseButton; clickCount?: number; } = {} ): Promise { @@ -1330,7 +1596,7 @@ export class Page extends EventEmitter { timeout?: number; polling?: string | number; } = {}, - ...args: unknown[] + ...args: SerializableOrJSHandle[] ): Promise { return this.mainFrame().waitFor( selectorOrFunctionOrTimeout, @@ -1367,7 +1633,7 @@ export class Page extends EventEmitter { timeout?: number; polling?: string | number; } = {}, - ...args: unknown[] + ...args: SerializableOrJSHandle[] ): Promise { return this.mainFrame().waitForFunction(pageFunction, options, ...args); } diff --git a/remote/test/puppeteer/src/common/Puppeteer.ts b/remote/test/puppeteer/src/common/Puppeteer.ts index e81ca0af9a145..e4dc0e5322a69 100644 --- a/remote/test/puppeteer/src/common/Puppeteer.ts +++ b/remote/test/puppeteer/src/common/Puppeteer.ts @@ -23,9 +23,7 @@ import { ProductLauncher } from '../node/Launcher'; import { BrowserFetcher, BrowserFetcherOptions } from '../node/BrowserFetcher'; import { puppeteerErrors, PuppeteerErrors } from './Errors'; import { ConnectionTransport } from './ConnectionTransport'; - -import { devicesMap } from './DeviceDescriptors'; -import { DevicesMap } from './DeviceDescriptors'; +import { devicesMap, DevicesMap } from './DeviceDescriptors'; import { Browser } from './Browser'; import { registerCustomQueryHandler, @@ -34,19 +32,40 @@ import { clearQueryHandlers, QueryHandler, } from './QueryHandler'; +import { PUPPETEER_REVISIONS } from '../revisions'; /** * The main Puppeteer class + * Puppeteer module provides a method to launch a browser instance. + * + * @remarks + * + * @example + * The following is a typical example of using Puppeteer to drive automation: + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` * @public */ export class Puppeteer { - _projectRoot: string; + private _projectRoot: string; _preferredRevision: string; _isPuppeteerCore: boolean; _changedProduct = false; __productName: string; _lazyLauncher: ProductLauncher; + /** + * @internal + */ constructor( projectRoot: string, preferredRevision: string, @@ -60,6 +79,31 @@ export class Puppeteer { this.__productName = productName; } + /** + * Launches puppeteer and launches a browser instance with given arguments + * and options when specified. + * + * @remarks + * + * @example + * You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: + * ```js + * const browser = await puppeteer.launch({ + * ignoreDefaultArgs: ['--mute-audio'] + * }); + * ``` + * + * **NOTE** Puppeteer can also be used to control the Chrome browser, + * but it works best with the version of Chromium it is bundled with. + * There is no guarantee it will work with any other version. + * Use `executablePath` option with extreme caution. + * If Google Chrome (rather than Chromium) is preferred, a {@link https://www.google.com/chrome/browser/canary.html | Chrome Canary} or {@link https://www.chromium.org/getting-involved/dev-channel | Dev Channel} build is suggested. + * In `puppeteer.launch([options])`, any mention of Chromium also applies to Chrome. + * See {@link https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/ | this article} for a description of the differences between Chromium and Chrome. {@link https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md | This article} describes some differences for Linux users. + * + * @param options - Set of configurable options to set on the browser. + * @returns Promise which resolves to browser instance. + */ launch( options: LaunchOptions & ChromeArgOptions & @@ -69,6 +113,14 @@ export class Puppeteer { return this._launcher.launch(options); } + /** + * This method attaches Puppeteer to an existing browser instance. + * + * @remarks + * + * @param options - Set of configurable options to set on the browser. + * @returns Promise which resolves to browser instance. + */ connect( options: BrowserOptions & { browserWSEndpoint?: string; @@ -81,35 +133,49 @@ export class Puppeteer { return this._launcher.connect(options); } + /** + * @internal + */ + get _productName(): string { + return this.__productName; + } + + // don't need any TSDoc here - because the getter is internal the setter is too. set _productName(name: string) { if (this.__productName !== name) this._changedProduct = true; this.__productName = name; } - get _productName(): string { - return this.__productName; - } - + /** + * @remarks + * + * **NOTE** `puppeteer.executablePath()` is affected by the `PUPPETEER_EXECUTABLE_PATH` + * and `PUPPETEER_CHROMIUM_REVISION` environment variables. + * + * @returns A path where Puppeteer expects to find the bundled browser. + * The browser binary might not be there if the download was skipped with + * the `PUPPETEER_SKIP_DOWNLOAD` environment variable. + */ executablePath(): string { return this._launcher.executablePath(); } + /** + * @internal + */ get _launcher(): ProductLauncher { if ( !this._lazyLauncher || this._lazyLauncher.product !== this._productName || this._changedProduct ) { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-var-requires - const packageJson = require('../../package.json'); switch (this._productName) { case 'firefox': - this._preferredRevision = packageJson.puppeteer.firefox_revision; + this._preferredRevision = PUPPETEER_REVISIONS.firefox; break; case 'chrome': default: - this._preferredRevision = packageJson.puppeteer.chromium_revision; + this._preferredRevision = PUPPETEER_REVISIONS.chromium; } this._changedProduct = false; this._lazyLauncher = Launcher( @@ -122,26 +188,89 @@ export class Puppeteer { return this._lazyLauncher; } + /** + * @returns The name of the browser that is under automation (`"chrome"` or `"firefox"`) + * + * @remarks + * The product is set by the `PUPPETEER_PRODUCT` environment variable or the `product` + * option in `puppeteer.launch([options])` and defaults to `chrome`. + * Firefox support is experimental. + */ get product(): string { return this._launcher.product; } + /** + * @remarks + * @example + * + * ```js + * const puppeteer = require('puppeteer'); + * const iPhone = puppeteer.devices['iPhone 6']; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.emulate(iPhone); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + * @returns a list of devices to be used with `page.emulate(options)`. Actual list of devices can be found in {@link https://github.com/puppeteer/puppeteer/blob/main/src/DeviceDescriptors.ts | src/DeviceDescriptors.ts}. + */ get devices(): DevicesMap { return devicesMap; } + /** + * @remarks + * + * Puppeteer methods might throw errors if they are unable to fulfill a request. + * For example, `page.waitForSelector(selector[, options])` might fail if + * the selector doesn't match any nodes during the given timeframe. + * + * For certain types of errors Puppeteer uses specific error classes. + * These classes are available via `puppeteer.errors` + * @example + * An example of handling a timeout error: + * ```js + * try { + * await page.waitForSelector('.foo'); + * } catch (e) { + * if (e instanceof puppeteer.errors.TimeoutError) { + * // Do something if this is a timeout. + * } + * } + * ``` + */ get errors(): PuppeteerErrors { return puppeteerErrors; } + /** + * + * @param options - Set of configurable options to set on the browser. + * @returns The default flags that Chromium will be launched with. + */ defaultArgs(options: ChromeArgOptions = {}): string[] { return this._launcher.defaultArgs(options); } + /** + * + * @param options - Set of configurable options to specify the settings + * of the BrowserFetcher. + * @returns A new BrowserFetcher instance. + */ createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher { return new BrowserFetcher(this._projectRoot, options); } + /** + * @internal + */ // eslint-disable-next-line @typescript-eslint/camelcase __experimental_registerCustomQueryHandler( name: string, @@ -150,16 +279,25 @@ export class Puppeteer { registerCustomQueryHandler(name, queryHandler); } + /** + * @internal + */ // eslint-disable-next-line @typescript-eslint/camelcase __experimental_unregisterCustomQueryHandler(name: string): void { unregisterCustomQueryHandler(name); } + /** + * @internal + */ // eslint-disable-next-line @typescript-eslint/camelcase __experimental_customQueryHandlers(): Map { return customQueryHandlers(); } + /** + * @internal + */ // eslint-disable-next-line @typescript-eslint/camelcase __experimental_clearQueryHandlers(): void { clearQueryHandlers(); diff --git a/remote/test/puppeteer/src/common/Target.ts b/remote/test/puppeteer/src/common/Target.ts index 5a80e801494ba..1a280a2b6b526 100644 --- a/remote/test/puppeteer/src/common/Target.ts +++ b/remote/test/puppeteer/src/common/Target.ts @@ -22,6 +22,9 @@ import { Browser, BrowserContext } from './Browser'; import { Viewport } from './PuppeteerViewport'; import Protocol from '../protocol'; +/** + * @public + */ export class Target { private _targetInfo: Protocol.Target.TargetInfo; private _browserContext: BrowserContext; diff --git a/remote/test/puppeteer/src/common/Tracing.ts b/remote/test/puppeteer/src/common/Tracing.ts index 673310dd1dfeb..15b0b14dd4f0f 100644 --- a/remote/test/puppeteer/src/common/Tracing.ts +++ b/remote/test/puppeteer/src/common/Tracing.ts @@ -17,7 +17,10 @@ import { assert } from './assert'; import { helper } from './helper'; import { CDPSession } from './Connection'; -interface TracingOptions { +/** + * @public + */ +export interface TracingOptions { path?: string; screenshots?: boolean; categories?: string[]; @@ -35,6 +38,8 @@ interface TracingOptions { * await page.goto('https://www.google.com'); * await page.tracing.stop(); * ``` + * + * @public */ export class Tracing { _client: CDPSession; diff --git a/remote/test/puppeteer/src/common/USKeyboardLayout.ts b/remote/test/puppeteer/src/common/USKeyboardLayout.ts index 69da035557646..feb3a1f7f1bc7 100644 --- a/remote/test/puppeteer/src/common/USKeyboardLayout.ts +++ b/remote/test/puppeteer/src/common/USKeyboardLayout.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +/** + * @internal + */ export interface KeyDefinition { keyCode?: number; shiftKeyCode?: number; @@ -25,6 +28,12 @@ export interface KeyDefinition { location?: number; } +/** + * All the valid keys that can be passed to functions that take user input, such + * as {@link Keyboard.press | keyboard.press } + * + * @public + */ export type KeyInput = | '0' | '1' @@ -282,6 +291,9 @@ export type KeyInput = | 'VolumeDown' | 'VolumeUp'; +/** + * @internal + */ export const keyDefinitions: Readonly> = { '0': { keyCode: 48, key: '0', code: 'Digit0' }, '1': { keyCode: 49, key: '1', code: 'Digit1' }, diff --git a/remote/test/puppeteer/src/common/WebWorker.ts b/remote/test/puppeteer/src/common/WebWorker.ts index 4b9e4ab3ff7cc..c74e14899f372 100644 --- a/remote/test/puppeteer/src/common/WebWorker.ts +++ b/remote/test/puppeteer/src/common/WebWorker.ts @@ -19,12 +19,20 @@ import { ExecutionContext } from './ExecutionContext'; import { JSHandle } from './JSHandle'; import { CDPSession } from './Connection'; import Protocol from '../protocol'; +import { EvaluateHandleFn, SerializableOrJSHandle } from './EvalTypes'; +/** + * @internal + */ type ConsoleAPICalledCallback = ( eventType: string, handles: JSHandle[], trace: Protocol.Runtime.StackTrace ) => void; + +/** + * @internal + */ type ExceptionThrownCallback = ( details: Protocol.Runtime.ExceptionDetails ) => void; @@ -123,8 +131,8 @@ export class WebWorker extends EventEmitter { * non-serializable value, then `worker.evaluate` resolves to `undefined`. * DevTools Protocol also supports transferring some additional values that * are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`, and - * bigint literals. Shortcut for `await - * worker.executionContext()).evaluate(pageFunction, ...args)`. + * bigint literals. + * Shortcut for `await worker.executionContext()).evaluate(pageFunction, ...args)`. * * @param pageFunction - Function to be evaluated in the worker context. * @param args - Arguments to pass to `pageFunction`. @@ -152,11 +160,11 @@ export class WebWorker extends EventEmitter { * @param args - Arguments to pass to `pageFunction`. * @returns Promise which resolves to the return value of `pageFunction`. */ - async evaluateHandle( - pageFunction: Function | string, - ...args: any[] + async evaluateHandle( + pageFunction: EvaluateHandleFn, + ...args: SerializableOrJSHandle[] ): Promise { - return (await this._executionContextPromise).evaluateHandle( + return (await this._executionContextPromise).evaluateHandle( pageFunction, ...args ); diff --git a/remote/test/puppeteer/src/common/helper.ts b/remote/test/puppeteer/src/common/helper.ts index 71ff4536599f4..20c0529c72ce8 100644 --- a/remote/test/puppeteer/src/common/helper.ts +++ b/remote/test/puppeteer/src/common/helper.ts @@ -268,7 +268,6 @@ async function readProtocolStream( } export const helper = { - promisify, evaluationString, readProtocolStream, waitWithTimeout, diff --git a/remote/test/puppeteer/src/environment.ts b/remote/test/puppeteer/src/environment.ts new file mode 100644 index 0000000000000..0d2a7a1f125cb --- /dev/null +++ b/remote/test/puppeteer/src/environment.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +export const isNode = typeof document === 'undefined'; diff --git a/remote/test/puppeteer/src/index-core.ts b/remote/test/puppeteer/src/index-core.ts new file mode 100644 index 0000000000000..692279f1273f9 --- /dev/null +++ b/remote/test/puppeteer/src/index-core.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2017 Google Inc. 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 { initializePuppeteer } from './initialize'; + +const puppeteer = initializePuppeteer('puppeteer-core'); +export default puppeteer; diff --git a/remote/test/puppeteer/src/index.ts b/remote/test/puppeteer/src/index.ts index 6adf9e0b36bf9..bcabc49357841 100644 --- a/remote/test/puppeteer/src/index.ts +++ b/remote/test/puppeteer/src/index.ts @@ -15,20 +15,6 @@ */ import { initializePuppeteer } from './initialize'; -import * as path from 'path'; -const puppeteer = initializePuppeteer({ - packageJson: require(path.join(__dirname, '..', 'package.json')), - rootDirectory: path.join(__dirname, '..'), -}); - -/* - * Has to be CJS here rather than ESM such that the output file ends with - * module.exports = puppeteer. - * - * If this was export default puppeteer the output would be: - * exports.default = puppeteer - * And therefore consuming via require('puppeteer') would break / require the user - * to access require('puppeteer').default; - */ -export = puppeteer; +const puppeteer = initializePuppeteer('puppeteer'); +export default puppeteer; diff --git a/remote/test/puppeteer/src/initialize.ts b/remote/test/puppeteer/src/initialize.ts index 73f0c624c38b0..429379c844c4c 100644 --- a/remote/test/puppeteer/src/initialize.ts +++ b/remote/test/puppeteer/src/initialize.ts @@ -21,28 +21,19 @@ const api = require('./api'); import { helper } from './common/helper'; import { Puppeteer } from './common/Puppeteer'; +import { PUPPETEER_REVISIONS } from './revisions'; +import pkgDir from 'pkg-dir'; -interface InitOptions { - packageJson: { - puppeteer: { - chromium_revision: string; - firefox_revision: string; - }; - name: string; - }; - rootDirectory: string; -} - -export const initializePuppeteer = (options: InitOptions): Puppeteer => { - const { packageJson, rootDirectory } = options; +export const initializePuppeteer = (packageName: string): Puppeteer => { + const puppeteerRootDirectory = pkgDir.sync(__dirname); for (const className in api) { if (typeof api[className] === 'function') helper.installAsyncStackHooks(api[className]); } - let preferredRevision = packageJson.puppeteer.chromium_revision; - const isPuppeteerCore = packageJson.name === 'puppeteer-core'; + let preferredRevision = PUPPETEER_REVISIONS.chromium; + const isPuppeteerCore = packageName === 'puppeteer-core'; // puppeteer-core ignores environment variables const product = isPuppeteerCore ? undefined @@ -50,10 +41,10 @@ export const initializePuppeteer = (options: InitOptions): Puppeteer => { process.env.npm_config_puppeteer_product || process.env.npm_package_config_puppeteer_product; if (!isPuppeteerCore && product === 'firefox') - preferredRevision = packageJson.puppeteer.firefox_revision; + preferredRevision = PUPPETEER_REVISIONS.firefox; const puppeteer = new Puppeteer( - rootDirectory, + puppeteerRootDirectory, preferredRevision, isPuppeteerCore, product diff --git a/remote/test/puppeteer/src/install.ts b/remote/test/puppeteer/src/install.ts new file mode 100644 index 0000000000000..57872e93ccaa9 --- /dev/null +++ b/remote/test/puppeteer/src/install.ts @@ -0,0 +1,176 @@ +/** + * Copyright 2020 Google Inc. 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 os from 'os'; +import https from 'https'; +import ProgressBar from 'progress'; +import puppeteer from './index'; +import { PUPPETEER_REVISIONS } from './revisions'; + +const firefoxVersions = + 'https://product-details.mozilla.org/1.0/firefox_versions.json'; + +const supportedProducts = { + chrome: 'Chromium', + firefox: 'Firefox Nightly', +} as const; + +export async function downloadBrowser() { + const downloadHost = + process.env.PUPPETEER_DOWNLOAD_HOST || + process.env.npm_config_puppeteer_download_host || + process.env.npm_package_config_puppeteer_download_host; + const product = + process.env.PUPPETEER_PRODUCT || + process.env.npm_config_puppeteer_product || + process.env.npm_package_config_puppeteer_product || + 'chrome'; + const browserFetcher = puppeteer.createBrowserFetcher({ + product, + host: downloadHost, + }); + const revision = await getRevision(); + await fetchBinary(revision); + + function getRevision() { + if (product === 'chrome') { + return ( + process.env.PUPPETEER_CHROMIUM_REVISION || + process.env.npm_config_puppeteer_chromium_revision || + PUPPETEER_REVISIONS.chromium + ); + } else if (product === 'firefox') { + puppeteer._preferredRevision = PUPPETEER_REVISIONS.firefox; + return getFirefoxNightlyVersion(browserFetcher.host()).catch((error) => { + console.error(error); + process.exit(1); + }); + } else { + throw new Error(`Unsupported product ${product}`); + } + } + + function fetchBinary(revision) { + const revisionInfo = browserFetcher.revisionInfo(revision); + + // Do nothing if the revision is already downloaded. + if (revisionInfo.local) { + logPolitely( + `${supportedProducts[product]} is already in ${revisionInfo.folderPath}; skipping download.` + ); + return; + } + + // Override current environment proxy settings with npm configuration, if any. + const NPM_HTTPS_PROXY = + process.env.npm_config_https_proxy || process.env.npm_config_proxy; + const NPM_HTTP_PROXY = + process.env.npm_config_http_proxy || process.env.npm_config_proxy; + const NPM_NO_PROXY = process.env.npm_config_no_proxy; + + if (NPM_HTTPS_PROXY) process.env.HTTPS_PROXY = NPM_HTTPS_PROXY; + if (NPM_HTTP_PROXY) process.env.HTTP_PROXY = NPM_HTTP_PROXY; + if (NPM_NO_PROXY) process.env.NO_PROXY = NPM_NO_PROXY; + + function onSuccess(localRevisions: string[]): void { + if (os.arch() !== 'arm64') { + logPolitely( + `${supportedProducts[product]} (${revisionInfo.revision}) downloaded to ${revisionInfo.folderPath}` + ); + } + localRevisions = localRevisions.filter( + (revision) => revision !== revisionInfo.revision + ); + const cleanupOldVersions = localRevisions.map((revision) => + browserFetcher.remove(revision) + ); + Promise.all([...cleanupOldVersions]); + } + + function onError(error: Error) { + console.error( + `ERROR: Failed to set up ${supportedProducts[product]} r${revision}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.` + ); + console.error(error); + process.exit(1); + } + + let progressBar = null; + let lastDownloadedBytes = 0; + function onProgress(downloadedBytes, totalBytes) { + if (!progressBar) { + progressBar = new ProgressBar( + `Downloading ${ + supportedProducts[product] + } r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, + { + complete: '=', + incomplete: ' ', + width: 20, + total: totalBytes, + } + ); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); + } + + return browserFetcher + .download(revisionInfo.revision, onProgress) + .then(() => browserFetcher.localRevisions()) + .then(onSuccess) + .catch(onError); + } + + function toMegabytes(bytes) { + const mb = bytes / 1024 / 1024; + return `${Math.round(mb * 10) / 10} Mb`; + } + + function getFirefoxNightlyVersion(host) { + const promise = new Promise((resolve, reject) => { + let data = ''; + logPolitely(`Requesting latest Firefox Nightly version from ${host}`); + https + .get(firefoxVersions, (r) => { + if (r.statusCode >= 400) + return reject(new Error(`Got status code ${r.statusCode}`)); + r.on('data', (chunk) => { + data += chunk; + }); + r.on('end', () => { + try { + const versions = JSON.parse(data); + return resolve(versions.FIREFOX_NIGHTLY); + } catch { + return reject(new Error('Firefox version not found')); + } + }); + }) + .on('error', reject); + }); + return promise; + } +} + +export function logPolitely(toBeLogged) { + const logLevel = process.env.npm_config_loglevel; + const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1; + + // eslint-disable-next-line no-console + if (!logLevelDisplay) console.log(toBeLogged); +} diff --git a/remote/test/puppeteer/src/node/BrowserFetcher.ts b/remote/test/puppeteer/src/node/BrowserFetcher.ts index 9f5df3cda6ce9..f76cc4c2f35f3 100644 --- a/remote/test/puppeteer/src/node/BrowserFetcher.ts +++ b/remote/test/puppeteer/src/node/BrowserFetcher.ts @@ -24,12 +24,13 @@ import * as http from 'http'; import extractZip from 'extract-zip'; import { debug } from '../common/Debug'; +import { promisify } from 'util'; import removeRecursive from 'rimraf'; import * as URL from 'url'; import ProxyAgent from 'https-proxy-agent'; import { getProxyForUrl } from 'proxy-from-env'; import { assert } from '../common/assert'; -import { helper } from '../common/helper'; + const debugFetcher = debug(`puppeteer:fetcher`); const downloadURLs = { @@ -61,12 +62,14 @@ const browserConfig = { /** * Supported platforms. + * @public */ -type Platform = 'linux' | 'mac' | 'win32' | 'win64'; +export type Platform = 'linux' | 'mac' | 'win32' | 'win64'; /** * Supported products. + * @public */ -type Product = 'chrome' | 'firefox'; +export type Product = 'chrome' | 'firefox'; function archiveName( product: Product, @@ -116,10 +119,10 @@ function handleArm64(): void { } }); } -const readdirAsync = helper.promisify(fs.readdir.bind(fs)); -const mkdirAsync = helper.promisify(fs.mkdir.bind(fs)); -const unlinkAsync = helper.promisify(fs.unlink.bind(fs)); -const chmodAsync = helper.promisify(fs.chmod.bind(fs)); +const readdirAsync = promisify(fs.readdir.bind(fs)); +const mkdirAsync = promisify(fs.mkdir.bind(fs)); +const unlinkAsync = promisify(fs.unlink.bind(fs)); +const chmodAsync = promisify(fs.chmod.bind(fs)); function existsAsync(filePath: string): Promise { return new Promise((resolve) => { @@ -127,6 +130,9 @@ function existsAsync(filePath: string): Promise { }); } +/** + * @public + */ export interface BrowserFetcherOptions { platform?: Platform; product?: string; @@ -134,7 +140,10 @@ export interface BrowserFetcherOptions { host?: string; } -interface BrowserFetcherRevisionInfo { +/** + * @public + */ +export interface BrowserFetcherRevisionInfo { folderPath: string; executablePath: string; url: string; diff --git a/remote/test/puppeteer/src/node/BrowserRunner.ts b/remote/test/puppeteer/src/node/BrowserRunner.ts index c3ae7f47e5fdc..b7b6d8b437943 100644 --- a/remote/test/puppeteer/src/node/BrowserRunner.ts +++ b/remote/test/puppeteer/src/node/BrowserRunner.ts @@ -26,8 +26,9 @@ import { WebSocketTransport } from '../common/WebSocketTransport'; import { PipeTransport } from './PipeTransport'; import * as readline from 'readline'; import { TimeoutError } from '../common/Errors'; +import { promisify } from 'util'; -const removeFolderAsync = helper.promisify(removeFolder); +const removeFolderAsync = promisify(removeFolder); const debugLauncher = debug('puppeteer:launcher'); const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary. This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser. diff --git a/remote/test/puppeteer/src/node/LaunchOptions.ts b/remote/test/puppeteer/src/node/LaunchOptions.ts index 81548c9c26ed0..487020ec27158 100644 --- a/remote/test/puppeteer/src/node/LaunchOptions.ts +++ b/remote/test/puppeteer/src/node/LaunchOptions.ts @@ -16,6 +16,11 @@ import { Viewport } from '../common/PuppeteerViewport'; +/** + * Launcher options that only apply to Chrome. + * + * @public + */ export interface ChromeArgOptions { headless?: boolean; args?: string[]; @@ -23,6 +28,10 @@ export interface ChromeArgOptions { devtools?: boolean; } +/** + * Generic launch options that can be passed when launching any browser. + * @public + */ export interface LaunchOptions { executablePath?: string; ignoreDefaultArgs?: boolean | string[]; @@ -35,6 +44,10 @@ export interface LaunchOptions { pipe?: boolean; } +/** + * Generic browser options that can be passed when launching any browser. + * @public + */ export interface BrowserOptions { ignoreHTTPSErrors?: boolean; defaultViewport?: Viewport; diff --git a/remote/test/puppeteer/src/node/Launcher.ts b/remote/test/puppeteer/src/node/Launcher.ts index d95d6b0fc2413..eb165f19e8ee8 100644 --- a/remote/test/puppeteer/src/node/Launcher.ts +++ b/remote/test/puppeteer/src/node/Launcher.ts @@ -24,13 +24,14 @@ import { BrowserFetcher } from './BrowserFetcher'; import { Connection } from '../common/Connection'; import { Browser } from '../common/Browser'; import { assert } from '../common/assert'; -import { helper, debugError } from '../common/helper'; +import { debugError } from '../common/helper'; import { ConnectionTransport } from '../common/ConnectionTransport'; import { WebSocketTransport } from '../common/WebSocketTransport'; import { BrowserRunner } from './BrowserRunner'; +import { promisify } from 'util'; -const mkdtempAsync = helper.promisify(fs.mkdtemp); -const writeFileAsync = helper.promisify(fs.writeFile); +const mkdtempAsync = promisify(fs.mkdtemp); +const writeFileAsync = promisify(fs.writeFile); import { ChromeArgOptions, @@ -38,6 +39,10 @@ import { BrowserOptions, } from './LaunchOptions'; +/** + * Describes a launcher - a class that is able to create and launch a browser instance. + * @public + */ export interface ProductLauncher { launch(object); connect(object); @@ -46,6 +51,9 @@ export interface ProductLauncher { product: string; } +/** + * @internal + */ class ChromeLauncher implements ProductLauncher { _projectRoot: string; _preferredRevision: string; @@ -266,6 +274,9 @@ class ChromeLauncher implements ProductLauncher { } } +/** + * @internal + */ class FirefoxLauncher implements ProductLauncher { _projectRoot: string; _preferredRevision: string; @@ -765,7 +776,10 @@ function resolveExecutablePath( return { executablePath: revisionInfo.executablePath, missingText }; } -function Launcher( +/** + * @internal + */ +export default function Launcher( projectRoot: string, preferredRevision: string, isPuppeteerCore: boolean, @@ -802,5 +816,3 @@ function Launcher( ); } } - -export default Launcher; diff --git a/remote/test/puppeteer/src/revisions.ts b/remote/test/puppeteer/src/revisions.ts new file mode 100644 index 0000000000000..9b307e9dca682 --- /dev/null +++ b/remote/test/puppeteer/src/revisions.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +type Revisions = Readonly<{ + readonly chromium: string; + readonly firefox: string; +}>; + +export const PUPPETEER_REVISIONS: Revisions = { + chromium: '756035', + firefox: 'latest', +}; diff --git a/remote/test/puppeteer/test/click.spec.ts b/remote/test/puppeteer/test/click.spec.ts index e7411b1bd8a7f..9dd77a44d539a 100644 --- a/remote/test/puppeteer/test/click.spec.ts +++ b/remote/test/puppeteer/test/click.spec.ts @@ -322,7 +322,7 @@ describe('Page.click', function () { server.CROSS_PROCESS_PREFIX + '/input/button.html' ); const frame = page.frames()[1]; - await frame.$eval('button', (button) => + await frame.$eval('button', (button: HTMLElement) => button.style.setProperty('position', 'fixed') ); await frame.click('button'); diff --git a/remote/test/puppeteer/test/elementhandle.spec.ts b/remote/test/puppeteer/test/elementhandle.spec.ts index f4d7603a9c936..46de86c65c0c7 100644 --- a/remote/test/puppeteer/test/elementhandle.spec.ts +++ b/remote/test/puppeteer/test/elementhandle.spec.ts @@ -182,17 +182,11 @@ describe('ElementHandle specs', function () { const { page, server } = getTestState(); await page.goto(server.PREFIX + '/shadow.html'); - const buttonHandle = await page.evaluateHandle( + const buttonHandle = await page.evaluateHandle( // @ts-expect-error button is expected to be in the page's scope. () => button ); - // TODO (@jackfranklin): TS types are off here. evaluateHandle returns a - // JSHandle but that doesn't have a click() method. In this case it seems - // to return an ElementHandle. I'm not sure if the tests are wrong here - // and should use evaluate or if the type of evaluateHandle - // should change to enable the user to tell us they are expecting an - // ElementHandle rather than the default JSHandle. - await (buttonHandle as ElementHandle).click(); + await buttonHandle.click(); expect( await page.evaluate( // @ts-expect-error clicked is expected to be in the page's scope. diff --git a/remote/test/puppeteer/test/headful.spec.ts b/remote/test/puppeteer/test/headful.spec.ts index 23bc4f86a76ed..88237c0d2dad4 100644 --- a/remote/test/puppeteer/test/headful.spec.ts +++ b/remote/test/puppeteer/test/headful.spec.ts @@ -22,7 +22,7 @@ import expect from 'expect'; import { getTestState, describeChromeOnly, - itFailsWindowsUntilDate, + itFailsWindows, } from './mocha-utils'; import rimraf from 'rimraf'; @@ -91,15 +91,10 @@ describeChromeOnly('headful tests', function () { expect(pages).toEqual(['about:blank']); await browser.close(); }); - itFailsWindowsUntilDate( - /* We have deferred fixing this test on Windows in favour of - * getting all other Windows tests running on CI. Putting this - * date in to force us to come back and debug properly in the - * future. - */ - new Date('2020-07-01'), + itFailsWindows( 'headless should be able to read cookies written by headful', async () => { + /* Needs investigation into why but this fails consistently on Windows CI. */ const { server, puppeteer } = getTestState(); const userDataDir = await mkdtempAsync(TMP_FOLDER); diff --git a/remote/test/puppeteer/test/input.spec.ts b/remote/test/puppeteer/test/input.spec.ts index 5bf1e76e54609..b5fa2d04e5ccb 100644 --- a/remote/test/puppeteer/test/input.spec.ts +++ b/remote/test/puppeteer/test/input.spec.ts @@ -140,7 +140,7 @@ describe('input tests', function () { const [fileChooser1, fileChooser2] = await Promise.all([ page.waitForFileChooser(), page.waitForFileChooser(), - page.$eval('input', (input) => input.click()), + page.$eval('input', (input: HTMLInputElement) => input.click()), ]); expect(fileChooser1 === fileChooser2).toBe(true); }); @@ -161,10 +161,18 @@ describe('input tests', function () { chooser.accept([FILE_TO_UPLOAD]), new Promise((x) => page.once('metrics', x)), ]); - expect(await page.$eval('input', (input) => input.files.length)).toBe(1); - expect(await page.$eval('input', (input) => input.files[0].name)).toBe( - 'file-to-upload.txt' - ); + expect( + await page.$eval( + 'input', + (input: HTMLInputElement) => input.files.length + ) + ).toBe(1); + expect( + await page.$eval( + 'input', + (input: HTMLInputElement) => input.files[0].name + ) + ).toBe('file-to-upload.txt'); }); it('should be able to read selected file', async () => { const { page } = getTestState(); @@ -174,7 +182,7 @@ describe('input tests', function () { .waitForFileChooser() .then((chooser) => chooser.accept([FILE_TO_UPLOAD])); expect( - await page.$eval('input', async (picker) => { + await page.$eval('input', async (picker: HTMLInputElement) => { picker.click(); await new Promise((x) => (picker.oninput = x)); const reader = new FileReader(); @@ -192,7 +200,7 @@ describe('input tests', function () { .waitForFileChooser() .then((chooser) => chooser.accept([FILE_TO_UPLOAD])); expect( - await page.$eval('input', async (picker) => { + await page.$eval('input', async (picker: HTMLInputElement) => { picker.click(); await new Promise((x) => (picker.oninput = x)); return picker.files.length; @@ -200,7 +208,7 @@ describe('input tests', function () { ).toBe(1); page.waitForFileChooser().then((chooser) => chooser.accept([])); expect( - await page.$eval('input', async (picker) => { + await page.$eval('input', async (picker: HTMLInputElement) => { picker.click(); await new Promise((x) => (picker.oninput = x)); return picker.files.length; @@ -247,7 +255,7 @@ describe('input tests', function () { await page.setContent(``); const [fileChooser] = await Promise.all([ page.waitForFileChooser(), - page.$eval('input', (input) => input.click()), + page.$eval('input', (input: HTMLInputElement) => input.click()), ]); await fileChooser.accept([]); let error = null; @@ -268,13 +276,13 @@ describe('input tests', function () { await page.setContent(``); const [fileChooser1] = await Promise.all([ page.waitForFileChooser(), - page.$eval('input', (input) => input.click()), + page.$eval('input', (input: HTMLInputElement) => input.click()), ]); await fileChooser1.cancel(); // If this resolves, than we successfully canceled file chooser. await Promise.all([ page.waitForFileChooser(), - page.$eval('input', (input) => input.click()), + page.$eval('input', (input: HTMLInputElement) => input.click()), ]); }); it('should fail when canceling file chooser twice', async () => { @@ -283,7 +291,7 @@ describe('input tests', function () { await page.setContent(``); const [fileChooser] = await Promise.all([ page.waitForFileChooser(), - page.$eval('input', (input) => input.click()), + page.$eval('input', (input: HTMLInputElement) => input.click()), ]); await fileChooser.cancel(); let error = null; diff --git a/remote/test/puppeteer/test/jshandle.spec.ts b/remote/test/puppeteer/test/jshandle.spec.ts index 2123076dff28f..486d9c46073d7 100644 --- a/remote/test/puppeteer/test/jshandle.spec.ts +++ b/remote/test/puppeteer/test/jshandle.spec.ts @@ -52,6 +52,7 @@ describe('JSHandle', function () { const aHandle = await page.evaluateHandle(() => document.body); let error = null; await page + // @ts-expect-error we are deliberately passing a bad type here (nested object) .evaluateHandle((opts) => opts.elem.querySelector('p'), { elem: aHandle, }) diff --git a/remote/test/puppeteer/test/keyboard.spec.ts b/remote/test/puppeteer/test/keyboard.spec.ts index 4452ff93dd738..2e32bae724b48 100644 --- a/remote/test/puppeteer/test/keyboard.spec.ts +++ b/remote/test/puppeteer/test/keyboard.spec.ts @@ -350,9 +350,12 @@ describe('Keyboard', function () { await page.goto(server.PREFIX + '/input/textarea.html'); await page.type('textarea', '👹 Tokyo street Japan 🇯🇵'); - expect(await page.$eval('textarea', (textarea) => textarea.value)).toBe( - '👹 Tokyo street Japan 🇯🇵' - ); + expect( + await page.$eval( + 'textarea', + (textarea: HTMLInputElement) => textarea.value + ) + ).toBe('👹 Tokyo street Japan 🇯🇵'); }); it('should type emoji into an iframe', async () => { const { page, server } = getTestState(); @@ -366,9 +369,12 @@ describe('Keyboard', function () { const frame = page.frames()[1]; const textarea = await frame.$('textarea'); await textarea.type('👹 Tokyo street Japan 🇯🇵'); - expect(await frame.$eval('textarea', (textarea) => textarea.value)).toBe( - '👹 Tokyo street Japan 🇯🇵' - ); + expect( + await frame.$eval( + 'textarea', + (textarea: HTMLInputElement) => textarea.value + ) + ).toBe('👹 Tokyo street Japan 🇯🇵'); }); it('should press the meta key', async () => { const { page, isFirefox } = getTestState(); diff --git a/remote/test/puppeteer/test/launcher.spec.ts b/remote/test/puppeteer/test/launcher.spec.ts index 9bd77d1249345..ac383741f90c0 100644 --- a/remote/test/puppeteer/test/launcher.spec.ts +++ b/remote/test/puppeteer/test/launcher.spec.ts @@ -17,7 +17,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import sinon from 'sinon'; -import { helper } from '../src/common/helper'; +import { promisify } from 'util'; import { getTestState, itOnlyRegularInstall, @@ -28,10 +28,10 @@ import expect from 'expect'; import rimraf from 'rimraf'; import { Page } from '../src/common/Page'; -const rmAsync = helper.promisify(rimraf); -const mkdtempAsync = helper.promisify(fs.mkdtemp); -const readFileAsync = helper.promisify(fs.readFile); -const statAsync = helper.promisify(fs.stat); +const rmAsync = promisify(rimraf); +const mkdtempAsync = promisify(fs.mkdtemp); +const readFileAsync = promisify(fs.readFile); +const statAsync = promisify(fs.stat); const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); describe('Launcher specs', function () { diff --git a/remote/test/puppeteer/test/mocha-utils.ts b/remote/test/puppeteer/test/mocha-utils.ts index 9bab15fc56851..517172738c8f7 100644 --- a/remote/test/puppeteer/test/mocha-utils.ts +++ b/remote/test/puppeteer/test/mocha-utils.ts @@ -170,6 +170,13 @@ export const itFailsWindowsUntilDate = ( return it(description, body); }; +export const itFailsWindows = (description: string, body: Mocha.Func) => { + if (os.platform() === 'win32') { + return xit(description, body); + } + return it(description, body); +}; + export const describeFailsFirefox = ( description: string, body: (this: Mocha.Suite) => void diff --git a/remote/test/puppeteer/test/page.spec.ts b/remote/test/puppeteer/test/page.spec.ts index cfa5724bfae0b..73bf61c357974 100644 --- a/remote/test/puppeteer/test/page.spec.ts +++ b/remote/test/puppeteer/test/page.spec.ts @@ -209,7 +209,7 @@ describe('Page', function () { ); const [popup] = await Promise.all([ new Promise((x) => page.once('popup', x)), - page.$eval('a', (a) => a.click()), + page.$eval('a', (a: HTMLAnchorElement) => a.click()), ]); expect(await page.evaluate(() => !!window.opener)).toBe(false); expect(await popup.evaluate(() => !!window.opener)).toBe(false); @@ -1614,7 +1614,7 @@ describe('Page', function () { await page.select('select', 'blue', 'black', 'magenta'); await page.select('select'); expect( - await page.$eval('select', (select) => + await page.$eval('select', (select: HTMLSelectElement) => Array.from(select.options).every( (option: HTMLOptionElement) => !option.selected ) @@ -1628,7 +1628,7 @@ describe('Page', function () { await page.select('select', 'blue', 'black', 'magenta'); await page.select('select'); expect( - await page.$eval('select', (select) => + await page.$eval('select', (select: HTMLSelectElement) => Array.from(select.options).every( (option: HTMLOptionElement) => !option.selected ) diff --git a/remote/test/puppeteer/test/queryselector.spec.ts b/remote/test/puppeteer/test/queryselector.spec.ts index 5dbf06f8952fc..db2e36e8ada0e 100644 --- a/remote/test/puppeteer/test/queryselector.spec.ts +++ b/remote/test/puppeteer/test/queryselector.spec.ts @@ -49,7 +49,7 @@ describe('querySelector', function () { const divHandle = await page.$('div'); const text = await page.$eval( 'section', - (e, div) => e.textContent + div.textContent, + (e, div: HTMLElement) => e.textContent + div.textContent, divHandle ); expect(text).toBe('hello world'); @@ -174,7 +174,10 @@ describe('querySelector', function () { '
10
' ); const tweet = await page.$('.tweet'); - const content = await tweet.$eval('.like', (node) => node.innerText); + const content = await tweet.$eval( + '.like', + (node: HTMLElement) => node.innerText + ); expect(content).toBe('100'); }); @@ -185,7 +188,10 @@ describe('querySelector', function () { '
not-a-child-div
a-child-div
'; await page.setContent(htmlContent); const elementHandle = await page.$('#myId'); - const content = await elementHandle.$eval('.a', (node) => node.innerText); + const content = await elementHandle.$eval( + '.a', + (node: HTMLElement) => node.innerText + ); expect(content).toBe('a-child-div'); }); @@ -197,7 +203,7 @@ describe('querySelector', function () { await page.setContent(htmlContent); const elementHandle = await page.$('#myId'); const errorMessage = await elementHandle - .$eval('.a', (node) => node.innerText) + .$eval('.a', (node: HTMLElement) => node.innerText) .catch((error) => error.message); expect(errorMessage).toBe( `Error: failed to find element matching selector ".a"` diff --git a/remote/test/puppeteer/test/requestinterception.spec.ts b/remote/test/puppeteer/test/requestinterception.spec.ts index 60876982a0d60..394d90b81c5a0 100644 --- a/remote/test/puppeteer/test/requestinterception.spec.ts +++ b/remote/test/puppeteer/test/requestinterception.spec.ts @@ -64,7 +64,7 @@ describe('request interception', function () { `); await Promise.all([ - page.$eval('form', (form) => form.submit()), + page.$eval('form', (form: HTMLFormElement) => form.submit()), page.waitForNavigation(), ]); }); @@ -453,7 +453,7 @@ describe('request interception', function () { page.on('request', async (r) => (request = r)); page.$eval( 'iframe', - (frame, url) => (frame.src = url), + (frame: HTMLIFrameElement, url: string) => (frame.src = url), server.EMPTY_PAGE ), // Wait for request interception. diff --git a/remote/test/puppeteer/tsconfig-esm.json b/remote/test/puppeteer/tsconfig-esm.json new file mode 100644 index 0000000000000..06d8e25d98bab --- /dev/null +++ b/remote/test/puppeteer/tsconfig-esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./lib/esm", + "module": "ES2015", + }, +} diff --git a/remote/test/puppeteer/tsconfig.json b/remote/test/puppeteer/tsconfig.json index 8b25d93823ed2..5a62ace19cb13 100644 --- a/remote/test/puppeteer/tsconfig.json +++ b/remote/test/puppeteer/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "allowJs": true, "checkJs": true, - "outDir": "./lib", + "outDir": "./lib/cjs", "target": "ESNext", "moduleResolution": "node", "module": "CommonJS", diff --git a/remote/test/puppeteer/utils/check_availability.js b/remote/test/puppeteer/utils/check_availability.js index 8cafc14025f3a..1a5f2cab49d84 100755 --- a/remote/test/puppeteer/utils/check_availability.js +++ b/remote/test/puppeteer/utils/check_availability.js @@ -17,8 +17,6 @@ const assert = require('assert'); const https = require('https'); - -const packageJSON = require('../package.json'); const BrowserFetcher = require('../lib/BrowserFetcher').BrowserFetcher; const SUPPORTER_PLATFORMS = ['linux', 'mac', 'win32', 'win64']; @@ -163,7 +161,10 @@ async function checkRollCandidate() { stableLinuxInfo.versions[0].branch_base_position, 10 ); - const currentRevision = parseInt(packageJSON.puppeteer.chromium_revision, 10); + const currentRevision = parseInt( + require('../lib/cjs/revisions').PUPPETEER_REVISIONS.chromium, + 10 + ); checkRangeAvailability({ fromRevision: stableLinuxRevision, diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/index.js b/remote/test/puppeteer/utils/doclint/check_public_api/index.js index b41c27852d0dc..d6d04a9147fd3 100644 --- a/remote/test/puppeteer/utils/doclint/check_public_api/index.js +++ b/remote/test/puppeteer/utils/doclint/check_public_api/index.js @@ -25,6 +25,10 @@ const EXCLUDE_PROPERTIES = new Set([ 'Page.create', 'JSHandle.toString', 'TimeoutError.name', + /* This isn't an actual property, but a TypeScript generic. + * DocLint incorrectly parses it as a property. + */ + 'ElementHandle.ElementType', ]); /** @@ -710,6 +714,41 @@ function compareDocumentations(actual, expected) { expectedName: 'Object', }, ], + [ + 'Method Coverage.startCSSCoverage() options', + { + actualName: 'Object', + expectedName: 'CSSCoverageOptions', + }, + ], + [ + 'Method Coverage.startJSCoverage() options', + { + actualName: 'Object', + expectedName: 'JSCoverageOptions', + }, + ], + [ + 'Method Mouse.click() options.button', + { + actualName: '"left"|"right"|"middle"', + expectedName: 'MouseButton', + }, + ], + [ + 'Method Frame.click() options.button', + { + actualName: '"left"|"right"|"middle"', + expectedName: 'MouseButton', + }, + ], + [ + 'Method Page.click() options.button', + { + actualName: '"left"|"right"|"middle"', + expectedName: 'MouseButton', + }, + ], ]); const expectedForSource = expectedNamingMismatches.get(source); diff --git a/remote/test/puppeteer/utils/doclint/cli.js b/remote/test/puppeteer/utils/doclint/cli.js index 51387b5a494d9..ef551ac080f84 100755 --- a/remote/test/puppeteer/utils/doclint/cli.js +++ b/remote/test/puppeteer/utils/doclint/cli.js @@ -71,8 +71,9 @@ async function run() { const jsSources = [ ...(await Source.readdir(path.join(PROJECT_DIR, 'lib'))), - ...(await Source.readdir(path.join(PROJECT_DIR, 'lib', 'common'))), - ...(await Source.readdir(path.join(PROJECT_DIR, 'lib', 'node'))), + ...(await Source.readdir(path.join(PROJECT_DIR, 'lib', 'cjs'))), + ...(await Source.readdir(path.join(PROJECT_DIR, 'lib', 'cjs', 'common'))), + ...(await Source.readdir(path.join(PROJECT_DIR, 'lib', 'cjs', 'node'))), ]; const allSrcCode = [...jsSources, ...tsSourcesNoDefinitions]; messages.push(...(await checkPublicAPI(page, mdSources, allSrcCode))); diff --git a/remote/test/puppeteer/utils/prepare_puppeteer_core.js b/remote/test/puppeteer/utils/prepare_puppeteer_core.js index b534dbfd13102..e1e9a64f2f4ed 100755 --- a/remote/test/puppeteer/utils/prepare_puppeteer_core.js +++ b/remote/test/puppeteer/utils/prepare_puppeteer_core.js @@ -23,4 +23,5 @@ const json = require(packagePath); json.name = 'puppeteer-core'; delete json.scripts.install; +json.main = './cjs-entry-core.js'; fs.writeFileSync(packagePath, JSON.stringify(json, null, ' '));