diff --git a/.eslintrc.js b/.eslintrc.js index a1de9853eeffd..a119974e1fed2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -166,6 +166,8 @@ module.exports = { default: 'array-simple', }, ], + // By default this is a warning but we want it to error. + '@typescript-eslint/explicit-module-boundary-types': 2, }, }, { diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 65edf9aa5b24f..51e5d7e8dd8c2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,9 +14,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # Include all major maintenance + active LTS Node.js versions. + # Include all major maintenance + active LTS + current Node.js versions. # https://github.com/nodejs/Release#release-schedule - node: [10, 12, 14] + node: [12, 14, 16] steps: - name: Checkout uses: actions/checkout@v2 @@ -42,16 +42,20 @@ jobs: - name: Run code checks run: | + npm run ensure-pinned-deps npm run lint npm run generate-docs npm run ensure-correct-devtools-protocol-revision npm run test-types-file - name: Run unit tests + uses: nick-invision/retry@v2 env: CHROMIUM: true - run: | - xvfb-run --auto-servernum npm run unit + with: + max_attempts: 3 + command: xvfb-run --auto-servernum npm run unit + timeout_minutes: 10 - name: Run unit tests with coverage env: @@ -61,11 +65,15 @@ jobs: xvfb-run --auto-servernum npm run assert-unit-coverage - name: Run unit tests on Firefox + uses: nick-invision/retry@v2 env: FIREFOX: true MOZ_WEBRENDER: 0 - run: | - xvfb-run --auto-servernum npm run funit + with: + max_attempts: 3 + timeout_minutes: 10 + command: xvfb-run --auto-servernum npm run funit + - name: Run browser tests run: | npm run test-browser @@ -93,7 +101,7 @@ jobs: with: # Test only the oldest maintenance LTS Node.js version. # https://github.com/nodejs/Release#release-schedule - node-version: 10 + node-version: 12 - name: Install dependencies run: | @@ -114,11 +122,11 @@ jobs: npm run unit - name: Run unit tests on Firefox - env: - FIREFOX: true - MOZ_WEBRENDER: 0 - run: | - npm run funit + uses: nick-invision/retry@v2 + with: + max_attempts: 3 + timeout_minutes: 10 + command: npm run funit windows: # https://github.com/actions/virtual-environments#available-environments @@ -134,7 +142,7 @@ jobs: with: # Test only the oldest maintenance LTS Node.js version. # https://github.com/nodejs/Release#release-schedule - node-version: 10 + node-version: 12 - name: Install dependencies run: | @@ -157,8 +165,11 @@ jobs: npm run unit - name: Run unit tests on Firefox + uses: nick-invision/retry@v2 env: FIREFOX: true MOZ_WEBRENDER: 0 - run: | - npm run funit + with: + max_attempts: 3 + timeout_minutes: 10 + command: npm run funit diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ae72fa6b0e3..6c675eae370d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0](https://github.com/puppeteer/puppeteer/compare/v9.1.1...v10.0.0) (2021-05-31) + + +### ⚠ BREAKING CHANGES + +* Node.js 10 is no longer supported. + +### Features + +* **chromium:** roll to Chromium 92.0.4512.0 (r884014) ([#7288](https://github.com/puppeteer/puppeteer/issues/7288)) ([f863f4b](https://github.com/puppeteer/puppeteer/commit/f863f4bfe015e57ea1f9fbb322f1cedee468b857)) +* **requestinterception:** remove cacheSafe flag ([#7217](https://github.com/puppeteer/puppeteer/issues/7217)) ([d01aa6c](https://github.com/puppeteer/puppeteer/commit/d01aa6c84a1e41f15ffed3a8d36ad26a404a7187)) +* expose other sessions from connection ([#6863](https://github.com/puppeteer/puppeteer/issues/6863)) ([cb285a2](https://github.com/puppeteer/puppeteer/commit/cb285a237921259eac99ade1d8b5550e068a55eb)) +* **launcher:** add new launcher option `waitForInitialPage` ([#7105](https://github.com/puppeteer/puppeteer/issues/7105)) ([2605309](https://github.com/puppeteer/puppeteer/commit/2605309f74b43da160cda4d214016e4422bf7676)), closes [#3630](https://github.com/puppeteer/puppeteer/issues/3630) + + +### Bug Fixes + +* added comments for browsercontext, startCSSCoverage, and startJSCoverage. ([#7264](https://github.com/puppeteer/puppeteer/issues/7264)) ([b750397](https://github.com/puppeteer/puppeteer/commit/b75039746ac6bddf1411538242b5e70b0f2e6e8a)) +* modified comment for method product, platform and newPage ([#7262](https://github.com/puppeteer/puppeteer/issues/7262)) ([159d283](https://github.com/puppeteer/puppeteer/commit/159d2835450697dabea6f9adf6e67d158b5b8ae3)) +* **requestinterception:** fix font loading issue ([#7060](https://github.com/puppeteer/puppeteer/issues/7060)) ([c9978d2](https://github.com/puppeteer/puppeteer/commit/c9978d20d5584c9fd2dc902e4b4ac86ed8ea5d6e)), closes [/github.com/puppeteer/puppeteer/pull/6996#issuecomment-811546501](https://github.com/puppeteer//github.com/puppeteer/puppeteer/pull/6996/issues/issuecomment-811546501) [/github.com/puppeteer/puppeteer/pull/6996#issuecomment-813797393](https://github.com/puppeteer//github.com/puppeteer/puppeteer/pull/6996/issues/issuecomment-813797393) [#7038](https://github.com/puppeteer/puppeteer/issues/7038) + + +* drop support for Node.js 10 ([#7200](https://github.com/puppeteer/puppeteer/issues/7200)) ([97c9fe2](https://github.com/puppeteer/puppeteer/commit/97c9fe2520723d45a5a86da06b888ae888d400be)), closes [#6753](https://github.com/puppeteer/puppeteer/issues/6753) + ### [9.1.1](https://github.com/puppeteer/puppeteer/compare/v9.1.0...v9.1.1) (2021-05-05) diff --git a/README.md b/README.md index 34e20813405a0..d40f06c038669 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ -###### [API](https://github.com/puppeteer/puppeteer/blob/v9.1.1/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/v10.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. @@ -41,7 +41,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/v9.1.1/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/v10.0.0/docs/api.md#environment-variables). ### puppeteer-core @@ -66,7 +66,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/v9.1.1/docs/api.md#). +of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#). **Example** - navigating to https://example.com and saving a screenshot as _example.png_: @@ -91,7 +91,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/v9.1.1/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/v10.0.0/docs/api.md#pagesetviewportviewport). **Example** - create a PDF. @@ -118,7 +118,7 @@ Execute script on the command line node hn.js ``` -See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#pagepdfoptions) for more information about creating pdfs. +See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#pagepdfoptions) for more information about creating pdfs. **Example** - evaluate script in the context of the page @@ -153,7 +153,7 @@ Execute script on the command line node get-dimensions.js ``` -See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/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/v10.0.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. @@ -163,7 +163,7 @@ See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/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/v9.1.1/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/v10.0.0/docs/api.md#puppeteerlaunchoptions) when launching a browser: ```js const browser = await puppeteer.launch({ headless: false }); // default is true @@ -179,7 +179,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/v9.1.1/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/v10.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. @@ -191,7 +191,7 @@ Puppeteer creates its own browser user profile which it **cleans up on every run ## Resources -- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md) +- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v10.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) @@ -333,7 +333,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/v9.1.1/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/v10.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). @@ -433,7 +433,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/v9.1.1/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/v10.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/docs/api.md b/docs/api.md index c807a23fb86d4..e5c4c74446e2e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,4 +1,4 @@ -# Puppeteer API v9.1.1 +# Puppeteer API v10.0.0 @@ -10,6 +10,7 @@ - Releases per Chromium version: + * Chromium 92.0.4512.0 - [Puppeteer v10.0.0](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md) * Chromium 91.0.4469.0 - [Puppeteer v9.0.0](https://github.com/puppeteer/puppeteer/blob/v9.0.0/docs/api.md) * Chromium 90.0.4427.0 - [Puppeteer v8.0.0](https://github.com/puppeteer/puppeteer/blob/v8.0.0/docs/api.md) * Chromium 90.0.4403.0 - [Puppeteer v7.0.0](https://github.com/puppeteer/puppeteer/blob/v7.0.0/docs/api.md) @@ -174,7 +175,7 @@ * [page.setGeolocation(options)](#pagesetgeolocationoptions) * [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) * [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled) - * [page.setRequestInterception(value[, cacheSafe])](#pagesetrequestinterceptionvalue-cachesafe) + * [page.setRequestInterception(value)](#pagesetrequestinterceptionvalue) * [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) * [page.setViewport(viewport)](#pagesetviewportviewport) * [page.tap(selector)](#pagetapselector) @@ -357,6 +358,7 @@ * [target.url()](#targeturl) * [target.worker()](#targetworker) - [class: CDPSession](#class-cdpsession) + * [cdpSession.connection()](#cdpsessionconnection) * [cdpSession.detach()](#cdpsessiondetach) * [cdpSession.send(method[, ...paramArgs])](#cdpsessionsendmethod-paramargs) - [class: Coverage](#class-coverage) @@ -625,6 +627,7 @@ try { - `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`. - `extraPrefsFirefox` <[Object]> Additional [preferences](https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/Preference_reference) that can be passed to Firefox (see `PUPPETEER_PRODUCT`) - `targetFilter` Use this function to decide if Puppeteer should connect to the given target. If a `targetFilter` is provided, Puppeteer only connects to targets for which `targetFilter` returns `true`. By default, Puppeteer connects to all available targets. + - `waitForInitialPage` <[boolean]> Whether to wait for the initial page to be ready. Defaults to `true`. - returns: <[Promise]<[Browser]>> Promise which resolves to browser instance. You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: @@ -2187,6 +2190,8 @@ This setting will change the default maximum time for the following methods and The extra HTTP headers will be sent with every request the page initiates. +> **NOTE** All HTTP header names are lowercased. (HTTP headers are case-insensitive, so this shouldn’t impact your server code.) + > **NOTE** page.setExtraHTTPHeaders does not guarantee the order of headers in the outgoing requests. #### page.setGeolocation(options) @@ -2214,10 +2219,9 @@ await page.setGeolocation({ latitude: 59.95, longitude: 30.31667 }); - `enabled` <[boolean]> When `true`, enables offline mode for the page. - returns: <[Promise]> -#### page.setRequestInterception(value[, cacheSafe]) +#### page.setRequestInterception(value) - `value` <[boolean]> Whether to enable request interception. -- `cacheSafe` <[boolean]> Whether to trust browser caching. If set to false, enabling request interception disables page caching. Defaults to false. - returns: <[Promise]> Activating request interception enables `request.abort`, `request.continue` and @@ -4557,6 +4561,12 @@ await client.send('Animation.setPlaybackRate', { }); ``` +#### cdpSession.connection() + +- returns: <[Connection]> + +Returns the underlying connection associated with the session. Can be used to obtain other related sessions. + #### cdpSession.detach() - returns: <[Promise]> diff --git a/package.json b/package.json index 4c276e0056467..7222bbc64e947 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "puppeteer", - "version": "9.1.1", + "version": "10.0.0", "description": "A high-level API to control headless Chrome over the DevTools Protocol", "main": "./cjs-entry.js", "types": "lib/types.d.ts", @@ -24,8 +24,8 @@ "eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)", "eslint-fix": "eslint --ext js --ext ts --fix .", "commitlint": "commitlint --from=HEAD~1", - "markdownlint": "prettier --check **/README.md docs/api.md docs/troubleshooting.md", - "markdownlint-fix": "prettier --write **/README.md docs/api.md docs/troubleshooting.md", + "markdownlint": "prettier --check **/README.md docs/troubleshooting.md", + "markdownlint-fix": "prettier --write **/README.md docs/troubleshooting.md", "lint": "npm run eslint && npm run build && npm run doc && npm run commitlint && npm run markdownlint", "doc": "node utils/doclint/cli.js", "clean-lib": "rimraf lib", @@ -38,6 +38,7 @@ "generate-d-ts": "api-extractor run --local --verbose", "generate-docs": "npm run generate-d-ts && api-documenter markdown -i temp -o new-docs", "ensure-correct-devtools-protocol-revision": "ts-node -s scripts/ensure-correct-devtools-protocol-package", + "ensure-pinned-deps": "ts-node -s scripts/ensure-pinned-deps", "test-types-file": "ts-node -s scripts/test-ts-definition-files.ts", "release": "node utils/remove_version_suffix.js && standard-version --commit-all" }, @@ -55,65 +56,66 @@ "author": "The Chromium Authors", "license": "Apache-2.0", "dependencies": { - "debug": "^4.1.0", - "devtools-protocol": "0.0.869402", - "extract-zip": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "pkg-dir": "^4.2.0", - "progress": "^2.0.1", - "proxy-from-env": "^1.1.0", - "rimraf": "^3.0.2", - "tar-fs": "^2.0.0", - "unbzip2-stream": "^1.3.3", - "ws": "^7.2.3" + "debug": "4.3.1", + "devtools-protocol": "0.0.883894", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.0", + "node-fetch": "2.6.1", + "pkg-dir": "4.2.0", + "progress": "2.0.1", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.0.0", + "unbzip2-stream": "1.3.3", + "ws": "7.4.6" }, "devDependencies": { - "@commitlint/cli": "^11.0.0", - "@commitlint/config-conventional": "^11.0.0", - "@microsoft/api-documenter": "^7.12.7", - "@microsoft/api-extractor": "^7.13.1", + "@commitlint/cli": "11.0.0", + "@commitlint/config-conventional": "11.0.0", + "@microsoft/api-documenter": "7.13.8", + "@microsoft/api-extractor": "7.15.1", "@types/debug": "0.0.31", - "@types/mime": "^2.0.0", - "@types/mocha": "^7.0.2", - "@types/node": "^14.0.13", - "@types/proxy-from-env": "^1.0.1", - "@types/rimraf": "^2.0.2", - "@types/sinon": "^9.0.4", - "@types/tar-fs": "^1.16.2", - "@types/ws": "^7.2.4", - "@typescript-eslint/eslint-plugin": "^4.4.0", - "@typescript-eslint/parser": "^4.4.0", - "@web/test-runner": "^0.12.15", - "commonmark": "^0.28.1", - "cross-env": "^7.0.2", - "eslint": "^7.10.0", - "eslint-config-prettier": "^6.12.0", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-mocha": "^8.0.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-unicorn": "^22.0.0", - "esprima": "^4.0.0", - "expect": "^25.2.7", - "husky": "^4.3.0", - "jpeg-js": "^0.3.7", - "mime": "^2.0.3", - "minimist": "^1.2.0", - "mocha": "^8.2.0", - "ncp": "^2.0.0", - "pixelmatch": "^4.0.2", - "pngjs": "^5.0.0", - "prettier": "^2.1.2", - "sinon": "^9.0.2", - "source-map-support": "^0.5.19", - "standard-version": "^9.0.0", - "text-diff": "^1.0.1", - "ts-node": "^9.0.0", - "typescript": "^4.1.5" + "@types/mime": "2.0.3", + "@types/mocha": "7.0.2", + "@types/node": "14.14.45", + "@types/proxy-from-env": "1.0.1", + "@types/rimraf": "2.0.2", + "@types/sinon": "9.0.11", + "@types/tar-fs": "1.16.2", + "@types/ws": "7.4.4", + "@typescript-eslint/eslint-plugin": "4.23.0", + "@typescript-eslint/parser": "4.23.0", + "@web/test-runner": "0.12.20", + "commonmark": "0.29.3", + "cross-env": "7.0.3", + "eslint": "7.26.0", + "eslint-config-prettier": "8.3.0", + "eslint-plugin-import": "2.22.1", + "eslint-plugin-mocha": "8.1.0", + "eslint-plugin-prettier": "3.4.0", + "eslint-plugin-unicorn": "22.0.0", + "esprima": "4.0.0", + "expect": "25.2.7", + "husky": "4.3.8", + "jpeg-js": "0.3.7", + "mime": "2.5.2", + "minimist": "1.2.0", + "mocha": "8.4.0", + "ncp": "2.0.0", + "pixelmatch": "4.0.2", + "pngjs": "5.0.0", + "prettier": "2.3.0", + "sinon": "9.2.4", + "source-map-support": "0.5.19", + "standard-version": "9.3.0", + "text-diff": "1.0.1", + "ts-node": "9.1.1", + "typescript": "4.2.4" }, "husky": { "hooks": { - "commit-msg": "commitlint --env HUSKY_GIT_PARAMS" + "commit-msg": "commitlint --env HUSKY_GIT_PARAMS", + "pre-push": "npm run ensure-pinned-deps" } } } diff --git a/scripts/ensure-pinned-deps.ts b/scripts/ensure-pinned-deps.ts new file mode 100644 index 0000000000000..e51c8c60f047c --- /dev/null +++ b/scripts/ensure-pinned-deps.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2021 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 packageJson from '../package.json'; + +const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; + +const invalidDeps = new Map(); + +for (const [depKey, depValue] of Object.entries(allDeps)) { + if (/[0-9]/.test(depValue[0])) { + continue; + } + + invalidDeps.set(depKey, depValue); +} + +if (invalidDeps.size > 0) { + console.error('Found non-pinned dependencies in package.json:'); + console.log([...invalidDeps.keys()].map((k) => ` ${k}`).join('\n')); + process.exit(1); +} + +process.exit(0); diff --git a/scripts/test-ts-definition-files.ts b/scripts/test-ts-definition-files.ts index e65fbfa079c36..7e4e83737218a 100644 --- a/scripts/test-ts-definition-files.ts +++ b/scripts/test-ts-definition-files.ts @@ -10,6 +10,10 @@ const EXPECTED_ERRORS = new Map([ "bad.ts(6,35): error TS2551: Property 'launh' does not exist on type", "bad.ts(8,29): error TS2551: Property 'devics' does not exist on type", 'bad.ts(12,39): error TS2554: Expected 0 arguments, but got 1.', + "bad.ts(20,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn", + "bad.ts(20,34): error TS2339: Property 'innerText' does not exist on type 'number'.", + "bad.ts(24,45): error TS2344: Type '(x: number) => string' does not satisfy the constraint 'EvaluateFn'.", + "bad.ts(27,34): error TS2339: Property 'innerText' does not exist on type 'number'.", ], ], [ @@ -35,6 +39,8 @@ const EXPECTED_ERRORS = new Map([ "bad.js(7,29): error TS2551: Property 'devics' does not exist on type", 'bad.js(11,39): error TS2554: Expected 0 arguments, but got 1.', "bad.js(15,9): error TS2322: Type 'ElementHandle | null' is not assignable to type 'ElementHandle'", + "bad.js(22,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn'.", + "bad.js(22,26): error TS2339: Property 'innerText' does not exist on type 'number'.", ], ], [ @@ -67,6 +73,13 @@ const EXPECTED_ERRORS = new Map([ ]); const PROJECT_FOLDERS = [...EXPECTED_ERRORS.keys()]; +if (!process.env.CI) { + console.log(`IMPORTANT: this script assumes you have compiled Puppeteer +and its types file before running. Make sure you have run: +=> npm run tsc && npm run generate-d-ts +before executing this script locally.`); +} + function packPuppeteer() { console.log('Packing Puppeteer'); const result = spawnSync('npm', ['pack'], { diff --git a/src/common/Accessibility.ts b/src/common/Accessibility.ts index 69684a47705e9..cf9a37fc5c284 100644 --- a/src/common/Accessibility.ts +++ b/src/common/Accessibility.ts @@ -437,10 +437,11 @@ class AXNode { properties.get(key) as boolean; for (const booleanProperty of booleanProperties) { - // WebArea's treat focus differently than other nodes. They report whether + // RootWebArea's treat focus differently than other nodes. They report whether // their frame has focus, not whether focus is specifically on the root // node. - if (booleanProperty === 'focused' && this._role === 'WebArea') continue; + if (booleanProperty === 'focused' && this._role === 'RootWebArea') + continue; const value = getBooleanPropertyValue(booleanProperty); if (!value) continue; node[booleanProperty] = getBooleanPropertyValue(booleanProperty); diff --git a/src/common/AriaQueryHandler.ts b/src/common/AriaQueryHandler.ts index 9d8cf964a7c3e..8b2e4c921c274 100644 --- a/src/common/AriaQueryHandler.ts +++ b/src/common/AriaQueryHandler.ts @@ -32,7 +32,7 @@ async function queryAXTree( role, }); const filteredNodes: Protocol.Accessibility.AXNode[] = nodes.filter( - (node: Protocol.Accessibility.AXNode) => node.role.value !== 'text' + (node: Protocol.Accessibility.AXNode) => node.role.value !== 'StaticText' ); return filteredNodes; } @@ -52,7 +52,8 @@ function parseAriaSelector(selector: string): ariaQueryOption { const normalize = (value: string): string => value.replace(/ +/g, ' ').trim(); const knownAttributes = new Set(['name', 'role']); const queryOptions: ariaQueryOption = {}; - const attributeRegexp = /\[\s*(?\w+)\s*=\s*"(?\\.|[^"\\]*)"\s*\]/g; + const attributeRegexp = + /\[\s*(?\w+)\s*=\s*"(?\\.|[^"\\]*)"\s*\]/g; const defaultName = selector.replace( attributeRegexp, (_, attribute: string, value: string) => { diff --git a/src/common/Browser.ts b/src/common/Browser.ts index ea40dd317a262..d9c9d29737a1f 100644 --- a/src/common/Browser.ts +++ b/src/common/Browser.ts @@ -417,7 +417,8 @@ export class Browser extends EventEmitter { } /** - * Creates a {@link Page} in the default browser context. + * Promise which resolves to a new {@link Page} object. The Page is created in + * a default browser context. */ async newPage(): Promise { return this._defaultContext.newPage(); @@ -722,9 +723,8 @@ export class BrowserContext extends EventEmitter { permissions: Permission[] ): Promise { const protocolPermissions = permissions.map((permission) => { - const protocolPermission = WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get( - permission - ); + const protocolPermission = + WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission); if (!protocolPermission) throw new Error('Unknown permission: ' + permission); return protocolPermission; diff --git a/src/common/BrowserConnector.ts b/src/common/BrowserConnector.ts index 58cdf4ce6ace0..3a707b5f300da 100644 --- a/src/common/BrowserConnector.ts +++ b/src/common/BrowserConnector.ts @@ -89,16 +89,14 @@ export const connectToBrowser = async ( connection = new Connection('', transport, slowMo); } else if (browserWSEndpoint) { const WebSocketClass = await getWebSocketTransportClass(); - const connectionTransport: ConnectionTransport = await WebSocketClass.create( - browserWSEndpoint - ); + const connectionTransport: ConnectionTransport = + await WebSocketClass.create(browserWSEndpoint); connection = new Connection(browserWSEndpoint, connectionTransport, slowMo); } else if (browserURL) { const connectionURL = await getWSEndpoint(browserURL); const WebSocketClass = await getWebSocketTransportClass(); - const connectionTransport: ConnectionTransport = await WebSocketClass.create( - connectionURL - ); + const connectionTransport: ConnectionTransport = + await WebSocketClass.create(connectionURL); connection = new Connection(connectionURL, connectionTransport, slowMo); } diff --git a/src/common/Connection.ts b/src/common/Connection.ts index 4f11556a28a05..34aab7df5c093 100644 --- a/src/common/Connection.ts +++ b/src/common/Connection.ts @@ -125,11 +125,21 @@ export class Connection extends EventEmitter { sessionId ); this._sessions.set(sessionId, session); + this.emit('sessionattached', session); + const parentSession = this._sessions.get(object.sessionId); + if (parentSession) { + parentSession.emit('sessionattached', session); + } } else if (object.method === 'Target.detachedFromTarget') { const session = this._sessions.get(object.params.sessionId); if (session) { session._onClosed(); this._sessions.delete(object.params.sessionId); + this.emit('sessiondetached', session); + const parentSession = this._sessions.get(object.sessionId); + if (parentSession) { + parentSession.emit('sessiondetached', session); + } } } if (object.sessionId) { @@ -253,6 +263,10 @@ export class CDPSession extends EventEmitter { this._sessionId = sessionId; } + connection(): Connection { + return this._connection; + } + send( method: T, ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] @@ -270,11 +284,7 @@ export class CDPSession extends EventEmitter { const id = this._connection._rawSend({ sessionId: this._sessionId, method, - /* TODO(jacktfranklin@): once this Firefox bug is solved - * we no longer need the `|| {}` check - * https://bugzilla.mozilla.org/show_bug.cgi?id=1631570 - */ - params: params || {}, + params, }); return new Promise((resolve, reject) => { diff --git a/src/common/Coverage.ts b/src/common/Coverage.ts index c4910d45803df..06354a3da0a81 100644 --- a/src/common/Coverage.ts +++ b/src/common/Coverage.ts @@ -123,8 +123,8 @@ export class Coverage { } /** - * @param options - defaults to - * `{ resetOnNavigation : true, reportAnonymousScripts : false }` + * @param options - Set of configurable options for coverage defaults to `{ + * resetOnNavigation : true, reportAnonymousScripts : false }` * @returns Promise that resolves when coverage is started. * * @remarks @@ -150,7 +150,8 @@ export class Coverage { } /** - * @param options - defaults to `{ resetOnNavigation : true }` + * @param options - Set of configurable options for coverage, defaults to `{ + * resetOnNavigation : true }` * @returns Promise that resolves when coverage is started. */ async startCSSCoverage(options: CSSCoverageOptions = {}): Promise { @@ -192,10 +193,8 @@ export class JSCoverage { } = {} ): Promise { assert(!this._enabled, 'JSCoverage is already enabled'); - const { - resetOnNavigation = true, - reportAnonymousScripts = false, - } = options; + const { resetOnNavigation = true, reportAnonymousScripts = false } = + options; this._resetOnNavigation = resetOnNavigation; this._reportAnonymousScripts = reportAnonymousScripts; this._enabled = true; diff --git a/src/common/DOMWorld.ts b/src/common/DOMWorld.ts index 7906d131b4178..69f927f345ed2 100644 --- a/src/common/DOMWorld.ts +++ b/src/common/DOMWorld.ts @@ -484,9 +484,8 @@ export class DOMWorld { selector: string, options: WaitForSelectorOptions ): Promise { - const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( - selector - ); + const { updatedSelector, queryHandler } = + getQueryHandlerAndSelector(selector); return queryHandler.waitFor(this, updatedSelector, options); } @@ -687,10 +686,8 @@ export class DOMWorld { options: { polling?: string | number; timeout?: number } = {}, ...args: SerializableOrJSHandle[] ): Promise { - const { - polling = 'raf', - timeout = this._timeoutSettings.timeout(), - } = options; + const { polling = 'raf', timeout = this._timeoutSettings.timeout() } = + options; const waitTaskOptions: WaitTaskOptions = { domWorld: this, predicateBody: pageFunction, diff --git a/src/common/EmulationManager.ts b/src/common/EmulationManager.ts index f3130ef3c5a10..baae0b5c7105d 100644 --- a/src/common/EmulationManager.ts +++ b/src/common/EmulationManager.ts @@ -31,9 +31,10 @@ export class EmulationManager { const width = viewport.width; const height = viewport.height; const deviceScaleFactor = viewport.deviceScaleFactor || 1; - const screenOrientation: Protocol.Emulation.ScreenOrientation = viewport.isLandscape - ? { angle: 90, type: 'landscapePrimary' } - : { angle: 0, type: 'portraitPrimary' }; + const screenOrientation: Protocol.Emulation.ScreenOrientation = + viewport.isLandscape + ? { angle: 90, type: 'landscapePrimary' } + : { angle: 0, type: 'portraitPrimary' }; const hasTouch = viewport.hasTouch || false; await Promise.all([ diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index e75377130f77b..91cf7e0904be9 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -21,7 +21,7 @@ export interface CommonEventEmitter { */ addListener(event: EventType, handler: Handler): CommonEventEmitter; removeListener(event: EventType, handler: Handler): CommonEventEmitter; - emit(event: EventType, eventData?: any): boolean; + emit(event: EventType, eventData?: unknown): boolean; once(event: EventType, handler: Handler): CommonEventEmitter; listenerCount(event: string): number; @@ -55,7 +55,7 @@ export class EventEmitter implements CommonEventEmitter { * Bind an event listener to fire when an event occurs. * @param event - the event type you'd like to listen to. Can be a string or symbol. * @param handler - the function to be called when the event occurs. - * @returns `this` to enable you to chain calls. + * @returns `this` to enable you to chain method calls. */ on(event: EventType, handler: Handler): EventEmitter { this.emitter.on(event, handler); @@ -66,7 +66,7 @@ export class EventEmitter implements CommonEventEmitter { * Remove an event listener from firing. * @param event - the event type you'd like to stop listening to. * @param handler - the function that should be removed. - * @returns `this` to enable you to chain calls. + * @returns `this` to enable you to chain method calls. */ off(event: EventType, handler: Handler): EventEmitter { this.emitter.off(event, handler); @@ -75,7 +75,7 @@ export class EventEmitter implements CommonEventEmitter { /** * Remove an event listener. - * @deprecated please use `off` instead. + * @deprecated please use {@link EventEmitter.off} instead. */ removeListener(event: EventType, handler: Handler): EventEmitter { this.off(event, handler); @@ -84,7 +84,7 @@ export class EventEmitter implements CommonEventEmitter { /** * Add an event listener. - * @deprecated please use `on` instead. + * @deprecated please use {@link EventEmitter.on} instead. */ addListener(event: EventType, handler: Handler): EventEmitter { this.on(event, handler); @@ -98,7 +98,7 @@ export class EventEmitter implements CommonEventEmitter { * @param eventData - any data you'd like to emit with the event * @returns `true` if there are any listeners, `false` if there are not. */ - emit(event: EventType, eventData?: any): boolean { + emit(event: EventType, eventData?: unknown): boolean { this.emitter.emit(event, eventData); return this.eventListenersCount(event) > 0; } @@ -107,7 +107,7 @@ export class EventEmitter implements CommonEventEmitter { * Like `on` but the listener will only be fired once and then it will be removed. * @param event - the event you'd like to listen to * @param handler - the handler function to run when the event occurs - * @returns `this` to enable you to chain calls. + * @returns `this` to enable you to chain method calls. */ once(event: EventType, handler: Handler): EventEmitter { const onceHandler: Handler = (eventData) => { @@ -132,7 +132,7 @@ export class EventEmitter implements CommonEventEmitter { * Removes all listeners. If given an event argument, it will remove only * listeners for that event. * @param event - the event to remove listeners for. - * @returns `this` to enable you to chain calls. + * @returns `this` to enable you to chain method calls. */ removeAllListeners(event?: EventType): EventEmitter { if (event) { diff --git a/src/common/ExecutionContext.ts b/src/common/ExecutionContext.ts index e8e2fbe48661b..f51b8a42ed5b3 100644 --- a/src/common/ExecutionContext.ts +++ b/src/common/ExecutionContext.ts @@ -267,10 +267,8 @@ export class ExecutionContext { error.message += ' Are you passing a nested JSHandle?'; throw error; } - const { - exceptionDetails, - result: remoteObject, - } = await callFunctionOnPromise.catch(rewriteError); + const { exceptionDetails, result: remoteObject } = + await callFunctionOnPromise.catch(rewriteError); if (exceptionDetails) throw new Error( 'Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails) diff --git a/src/common/FileChooser.ts b/src/common/FileChooser.ts index eb3f4d91c811c..d1cbea73a38ad 100644 --- a/src/common/FileChooser.ts +++ b/src/common/FileChooser.ts @@ -76,7 +76,7 @@ export class FileChooser { /** * Closes the file chooser without selecting any files. */ - cancel() { + cancel(): void { assert( !this._handled, 'Cannot cancel FileChooser which is already handled!' diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index 7b581ee936fcb..7c4dcd7ed9cb1 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -191,14 +191,16 @@ export class HTTPRequest { } /** - * @returns the response for this request, if a response has been received. + * @returns A matching `HTTPResponse` object, or null if the response has not + * been received yet. */ response(): HTTPResponse | null { return this._response; } /** - * @returns the frame that initiated the request. + * @returns the frame that initiated the request, or null if navigating to + * error pages. */ frame(): Frame | null { return this._frame; @@ -212,6 +214,7 @@ export class HTTPRequest { } /** + * A `redirectChain` is a chain of requests initiated to fetch a resource. * @remarks * * `redirectChain` is shared between all the requests of the same chain. diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index ece483f265808..db83587be5aa9 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -105,7 +105,7 @@ export function createJSHandle( * * @public */ -export class JSHandle { +export class JSHandle { /** * @internal */ @@ -154,7 +154,7 @@ export class JSHandle { * ``` */ - async evaluate( + async evaluate>( pageFunction: T | string, ...args: SerializableOrJSHandle[] ): Promise>> { @@ -193,7 +193,7 @@ export class JSHandle { */ async getProperty(propertyName: string): Promise { const objectHandle = await this.evaluateHandle( - (object: HTMLElement, propertyName: string) => { + (object: Element, propertyName: string) => { const result = { __proto__: null }; result[propertyName] = object[propertyName]; return result; @@ -237,8 +237,8 @@ export class JSHandle { } /** - * Returns a JSON representation of the object. - * + * @returns Returns a JSON representation of the object.If the object has a + * `toJSON` function, it will not be called. * @remarks * * The JSON is generated by running {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify | JSON.stringify} @@ -259,12 +259,13 @@ export class JSHandle { } /** - * Returns either `null` or the object handle itself, if the object handle is - * an instance of {@link ElementHandle}. + * @returns Either `null` or the object handle itself, if the object + * handle is an instance of {@link ElementHandle}. */ asElement(): ElementHandle | null { - // This always returns null, but subclasses can override this and return an - // ElementHandle. + /* This always returns null, but subclasses can override this and return an + ElementHandle. + */ return null; } @@ -328,7 +329,7 @@ export class JSHandle { */ export class ElementHandle< ElementType extends Element = Element -> extends JSHandle { +> extends JSHandle { private _page: Page; private _frameManager: FrameManager; @@ -521,24 +522,25 @@ export class ElementHandle< '"' ); - return this.evaluate< - (element: HTMLSelectElement, values: string[]) => string[] - >((element, values) => { - if (element.nodeName.toLowerCase() !== 'select') - throw new Error('Element is not a element.'); + + const options = Array.from(element.options); + element.value = undefined; + for (const option of options) { + option.selected = values.includes(option.value); + if (option.selected && !element.multiple) break; + } + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + return options + .filter((option) => option.selected) + .map((option) => option.value); + }, + values + ); } /** @@ -549,9 +551,14 @@ export class ElementHandle< * relative to the {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory} */ async uploadFile(...filePaths: string[]): Promise { - const isMultiple = await this.evaluate< - (element: HTMLInputElement) => boolean - >((element) => element.multiple); + const isMultiple = await this.evaluate<(element: Element) => boolean>( + (element) => { + if (!(element instanceof HTMLInputElement)) { + throw new Error('uploadFile can only be called on an input element.'); + } + return element.multiple; + } + ); assert( filePaths.length <= 1 || isMultiple, 'Multiple file uploads only work with ' @@ -562,8 +569,10 @@ export class ElementHandle< `JSHandle#uploadFile can only be used in Node environments.` ); } - // This import is only needed for `uploadFile`, so keep it scoped here to avoid paying - // the cost unnecessarily. + /* + This import is only needed for `uploadFile`, so keep it scoped here to + avoid paying the cost unnecessarily. + */ const path = await import('path'); const fs = await helper.importFSModule(); // Locate all files and confirm that they exist. @@ -584,11 +593,12 @@ export class ElementHandle< const { node } = await this._client.send('DOM.describeNode', { objectId }); const { backendNodeId } = node; - // The zero-length array is a special case, it seems that DOM.setFileInputFiles does - // not actually update the files in that case, so the solution is to eval the element - // value to a new FileList directly. + /* The zero-length array is a special case, it seems that + DOM.setFileInputFiles does 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) => void>((element) => { + await (this as ElementHandle).evaluate((element) => { element.files = new DataTransfer().files; // Dispatch events for this case because it should behave akin to a user action. @@ -619,7 +629,7 @@ export class ElementHandle< * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element. */ async focus(): Promise { - await this.evaluate<(element: HTMLElement) => void>((element) => + await (this as ElementHandle).evaluate((element) => element.focus() ); } @@ -776,9 +786,8 @@ export class ElementHandle< async $( selector: string ): Promise | null> { - const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( - selector - ); + const { updatedSelector, queryHandler } = + getQueryHandlerAndSelector(selector); return queryHandler.queryOne(this, updatedSelector); } @@ -789,9 +798,8 @@ export class ElementHandle< async $$( selector: string ): Promise>> { - const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( - selector - ); + const { updatedSelector, queryHandler } = + getQueryHandlerAndSelector(selector); return queryHandler.queryAll(this, updatedSelector); } @@ -873,9 +881,8 @@ export class ElementHandle< ) => ReturnType | Promise, ...args: SerializableOrJSHandle[] ): Promise> { - const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( - selector - ); + const { updatedSelector, queryHandler } = + getQueryHandlerAndSelector(selector); const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector); const result = await arrayHandle.evaluate< ( @@ -976,8 +983,9 @@ export interface PressOptions { } function computeQuadArea(quad: Array<{ x: number; y: number }>): number { - // Compute sum of all directed areas of adjacent triangles - // https://en.wikipedia.org/wiki/Polygon#Simple_polygons + /* Compute sum of all directed areas of adjacent triangles + https://en.wikipedia.org/wiki/Polygon#Simple_polygons + */ let area = 0; for (let i = 0; i < quad.length; ++i) { const p1 = quad[i]; diff --git a/src/common/NetworkManager.ts b/src/common/NetworkManager.ts index e29a06f94e2a8..0fdb778d4d54e 100644 --- a/src/common/NetworkManager.ts +++ b/src/common/NetworkManager.ts @@ -69,19 +69,53 @@ export class NetworkManager extends EventEmitter { _client: CDPSession; _ignoreHTTPSErrors: boolean; _frameManager: FrameManager; - _requestIdToRequest = new Map(); + + /* + * There are four possible orders of events: + * A. `_onRequestWillBeSent` + * B. `_onRequestWillBeSent`, `_onRequestPaused` + * C. `_onRequestPaused`, `_onRequestWillBeSent` + * D. `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused` + * (see crbug.com/1196004) + * + * For `_onRequest` we need the event from `_onRequestWillBeSent` and + * optionally the `interceptionId` from `_onRequestPaused`. + * + * If request interception is disabled, call `_onRequest` once per call to + * `_onRequestWillBeSent`. + * If request interception is enabled, call `_onRequest` once per call to + * `_onRequestPaused` (once per `interceptionId`). + * + * Events are stored to allow for subsequent events to call `_onRequest`. + * + * Note that (chains of) redirect requests have the same `requestId` (!) as + * the original request. We have to anticipate series of events like these: + * A. `_onRequestWillBeSent`, + * `_onRequestWillBeSent`, ... + * B. `_onRequestWillBeSent`, `_onRequestPaused`, + * `_onRequestWillBeSent`, `_onRequestPaused`, ... + * C. `_onRequestWillBeSent`, `_onRequestPaused`, + * `_onRequestPaused`, `_onRequestWillBeSent`, ... + * D. `_onRequestPaused`, `_onRequestWillBeSent`, + * `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`, ... + * (see crbug.com/1196004) + */ _requestIdToRequestWillBeSentEvent = new Map< string, Protocol.Network.RequestWillBeSentEvent >(); + _requestIdToRequestPausedEvent = new Map< + string, + Protocol.Fetch.RequestPausedEvent + >(); + _requestIdToRequest = new Map(); + _extraHTTPHeaders: Record = {}; _credentials?: Credentials = null; _attemptedAuthentications = new Set(); _userRequestInterceptionEnabled = false; - _userRequestInterceptionCacheSafe = false; _protocolRequestInterceptionEnabled = false; _userCacheDisabled = false; - _requestIdToInterceptionId = new Map(); _emulatedNetworkConditions: InternalNetworkConditions = { offline: false, upload: -1, @@ -193,12 +227,8 @@ export class NetworkManager extends EventEmitter { await this._updateProtocolCacheDisabled(); } - async setRequestInterception( - value: boolean, - cacheSafe = false - ): Promise { + async setRequestInterception(value: boolean): Promise { this._userRequestInterceptionEnabled = value; - this._userRequestInterceptionCacheSafe = cacheSafe; await this._updateProtocolRequestInterception(); } @@ -222,12 +252,13 @@ export class NetworkManager extends EventEmitter { } } + _cacheDisabled(): boolean { + return this._userCacheDisabled; + } + async _updateProtocolCacheDisabled(): Promise { await this._client.send('Network.setCacheDisabled', { - cacheDisabled: - this._userCacheDisabled || - (this._userRequestInterceptionEnabled && - !this._userRequestInterceptionCacheSafe), + cacheDisabled: this._cacheDisabled(), }); } @@ -238,13 +269,17 @@ export class NetworkManager extends EventEmitter { !event.request.url.startsWith('data:') ) { const requestId = event.requestId; - const interceptionId = this._requestIdToInterceptionId.get(requestId); - if (interceptionId) { + const requestPausedEvent = + this._requestIdToRequestPausedEvent.get(requestId); + + this._requestIdToRequestWillBeSentEvent.set(requestId, event); + + if (requestPausedEvent) { + const interceptionId = requestPausedEvent.requestId; this._onRequest(event, interceptionId); - this._requestIdToInterceptionId.delete(requestId); - } else { - this._requestIdToRequestWillBeSentEvent.set(event.requestId, event); + this._requestIdToRequestPausedEvent.delete(requestId); } + return; } this._onRequest(event, null); @@ -288,14 +323,29 @@ export class NetworkManager extends EventEmitter { const requestId = event.networkId; const interceptionId = event.requestId; - if (requestId && this._requestIdToRequestWillBeSentEvent.has(requestId)) { - const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get( - requestId - ); + + if (!requestId) { + return; + } + + let requestWillBeSentEvent = + this._requestIdToRequestWillBeSentEvent.get(requestId); + + // redirect requests have the same `requestId`, + if ( + requestWillBeSentEvent && + (requestWillBeSentEvent.request.url !== event.request.url || + requestWillBeSentEvent.request.method !== event.request.method) + ) { + this._requestIdToRequestWillBeSentEvent.delete(requestId); + requestWillBeSentEvent = null; + } + + if (requestWillBeSentEvent) { this._onRequest(requestWillBeSentEvent, interceptionId); this._requestIdToRequestWillBeSentEvent.delete(requestId); } else { - this._requestIdToInterceptionId.set(requestId, interceptionId); + this._requestIdToRequestPausedEvent.set(requestId, event); } } @@ -346,8 +396,7 @@ export class NetworkManager extends EventEmitter { response._resolveBody( new Error('Response body is unavailable for redirect responses') ); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); + this._forgetRequest(request, false); this.emit(NetworkManagerEmittedEvents.Response, response); this.emit(NetworkManagerEmittedEvents.RequestFinished, request); } @@ -361,6 +410,19 @@ export class NetworkManager extends EventEmitter { this.emit(NetworkManagerEmittedEvents.Response, response); } + _forgetRequest(request: HTTPRequest, events: boolean): void { + const requestId = request._requestId; + const interceptionId = request._interceptionId; + + this._requestIdToRequest.delete(requestId); + this._attemptedAuthentications.delete(interceptionId); + + if (events) { + this._requestIdToRequestWillBeSentEvent.delete(requestId); + this._requestIdToRequestPausedEvent.delete(requestId); + } + } + _onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void { const request = this._requestIdToRequest.get(event.requestId); // For certain requestIds we never receive requestWillBeSent event. @@ -370,8 +432,7 @@ export class NetworkManager extends EventEmitter { // Under certain conditions we never get the Network.responseReceived // event from protocol. @see https://crbug.com/883475 if (request.response()) request.response()._resolveBody(null); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); + this._forgetRequest(request, true); this.emit(NetworkManagerEmittedEvents.RequestFinished, request); } @@ -383,8 +444,7 @@ export class NetworkManager extends EventEmitter { request._failureText = event.errorText; const response = request.response(); if (response) response._resolveBody(null); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); + this._forgetRequest(request, true); this.emit(NetworkManagerEmittedEvents.RequestFailed, request); } } diff --git a/src/common/Page.ts b/src/common/Page.ts index 9a1eafc49876a..7119ce6759f42 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -488,8 +488,16 @@ export class Page extends EventEmitter { this._viewport = null; client.on('Target.attachedToTarget', (event) => { - if (event.targetInfo.type !== 'worker') { + if ( + event.targetInfo.type !== 'worker' && + event.targetInfo.type !== 'iframe' + ) { // If we don't detach from service workers, they will never die. + // We still want to attach to workers for emitting events. + // We still want to attach to iframes so sessions may interact with them. + // We detach from all other types out of an abundance of caution. + // See https://source.chromium.org/chromium/chromium/src/+/master:content/browser/devtools/devtools_agent_host_impl.cc?q=f:devtools%20-f:out%20%22::kTypePage%5B%5D%22&ss=chromium + // for the complete list of available types. client .send('Target.detachFromTarget', { sessionId: event.sessionId, @@ -758,8 +766,6 @@ export class Page extends EventEmitter { /** * @param value - Whether to enable request interception. - * @param cacheSafe - Whether to trust browser caching. If set to false, - * enabling request interception disables page caching. Defaults to false. * * @remarks * Activating request interception enables {@link HTTPRequest.abort}, @@ -789,13 +795,8 @@ export class Page extends EventEmitter { * })(); * ``` */ - async setRequestInterception( - value: boolean, - cacheSafe = false - ): Promise { - return this._frameManager - .networkManager() - .setRequestInterception(value, cacheSafe); + async setRequestInterception(value: boolean): Promise { + return this._frameManager.networkManager().setRequestInterception(value); } /** @@ -834,7 +835,7 @@ export class Page extends EventEmitter { * @remarks * Shortcut for {@link Frame.$ | Page.mainFrame().$(selector) }. * - * @param selector - A + * @param selector - A `selector` to query page for * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} * to query page for. */ @@ -1819,11 +1820,15 @@ export class Page extends EventEmitter { clip = { x: 0, y: 0, width, height, scale: 1 }; if (!captureBeyondViewport) { - const { isMobile = false, deviceScaleFactor = 1, isLandscape = false } = - this._viewport || {}; - const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape - ? { angle: 90, type: 'landscapePrimary' } - : { angle: 0, type: 'portraitPrimary' }; + const { + isMobile = false, + deviceScaleFactor = 1, + isLandscape = false, + } = this._viewport || {}; + const screenOrientation: Protocol.Emulation.ScreenOrientation = + isLandscape + ? { angle: 90, type: 'landscapePrimary' } + : { angle: 0, type: 'portraitPrimary' }; await this._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, diff --git a/src/common/QueryHandler.ts b/src/common/QueryHandler.ts index b7984067eeb31..3009b60b66c59 100644 --- a/src/common/QueryHandler.ts +++ b/src/common/QueryHandler.ts @@ -215,9 +215,10 @@ export function clearCustomQueryHandlers(): void { /** * @internal */ -export function getQueryHandlerAndSelector( - selector: string -): { updatedSelector: string; queryHandler: InternalQueryHandler } { +export function getQueryHandlerAndSelector(selector: string): { + updatedSelector: string; + queryHandler: InternalQueryHandler; +} { const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector); if (!hasCustomQueryHandler) return { updatedSelector: selector, queryHandler: _defaultHandler }; diff --git a/src/common/Target.ts b/src/common/Target.ts index 0e8296a633ac6..42506f1b1fa99 100644 --- a/src/common/Target.ts +++ b/src/common/Target.ts @@ -191,6 +191,9 @@ export class Target { return this._browserContext.browser(); } + /** + * Get the browser context the target belongs to. + */ browserContext(): BrowserContext { return this._browserContext; } diff --git a/src/common/WebWorker.ts b/src/common/WebWorker.ts index 87fb307bab5d9..8131e8a9efdbb 100644 --- a/src/common/WebWorker.ts +++ b/src/common/WebWorker.ts @@ -95,7 +95,7 @@ export class WebWorker extends EventEmitter { this._executionContextCallback(executionContext); }); - // This might fail if the target is closed before we recieve all execution contexts. + // This might fail if the target is closed before we receive all execution contexts. this._client.send('Runtime.enable').catch(debugError); this._client.on('Runtime.consoleAPICalled', (event) => consoleAPICalled( @@ -151,7 +151,7 @@ export class WebWorker extends EventEmitter { /** * 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 + * function passed to the `worker.evaluateHandle` returns a `Promise`, then * `worker.evaluateHandle` would wait for the promise to resolve and return * its value. Shortcut for * `await worker.executionContext()).evaluateHandle(pageFunction, ...args)` diff --git a/src/node/BrowserFetcher.ts b/src/node/BrowserFetcher.ts index c1792aa73903e..20301e2462142 100644 --- a/src/node/BrowserFetcher.ts +++ b/src/node/BrowserFetcher.ts @@ -58,8 +58,7 @@ const browserConfig = { destination: '.local-chromium', }, firefox: { - host: - 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central', + host: 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central', destination: '.local-firefox', }, } as const; @@ -221,14 +220,16 @@ export class BrowserFetcher { } /** - * @returns Returns the current `Platform`. + * @returns Returns the current `Platform`, which is one of `mac`, `linux`, + * `win32` or `win64`. */ platform(): Platform { return this._platform; } /** - * @returns Returns the current `Product`. + * @returns Returns the current `Product`, which is one of `chrome` or + * `firefox`. */ product(): Product { return this._product; diff --git a/src/node/BrowserRunner.ts b/src/node/BrowserRunner.ts index 2a29aca602984..554f14ab884ef 100644 --- a/src/node/BrowserRunner.ts +++ b/src/node/BrowserRunner.ts @@ -62,14 +62,8 @@ export class BrowserRunner { } start(options: LaunchOptions): void { - const { - handleSIGINT, - handleSIGTERM, - handleSIGHUP, - dumpio, - env, - pipe, - } = options; + const { handleSIGINT, handleSIGTERM, handleSIGHUP, dumpio, env, pipe } = + options; let stdio: Array<'ignore' | 'pipe'> = ['pipe', 'pipe', 'pipe']; if (pipe) { if (dumpio) stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe']; diff --git a/src/node/LaunchOptions.ts b/src/node/LaunchOptions.ts index 306edd6eb94b0..c211c565034d2 100644 --- a/src/node/LaunchOptions.ts +++ b/src/node/LaunchOptions.ts @@ -110,6 +110,12 @@ export interface LaunchOptions { * {@link https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/Preference_reference | Additional preferences } that can be passed when launching with Firefox. */ extraPrefsFirefox?: Record; + /** + * Whether to wait for the initial page to be ready. + * Useful when a user explicitly disables that (e.g. `--no-startup-window` for Chrome). + * @defaultValue true + */ + waitForInitialPage?: boolean; } /** diff --git a/src/node/Launcher.ts b/src/node/Launcher.ts index 2ea2709c59ba9..669d04d7ad089 100644 --- a/src/node/Launcher.ts +++ b/src/node/Launcher.ts @@ -75,6 +75,7 @@ class ChromeLauncher implements ProductLauncher { defaultViewport = { width: 800, height: 600 }, slowMo = 0, timeout = 30000, + waitForInitialPage = true, } = options; const profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-'); @@ -147,7 +148,8 @@ class ChromeLauncher implements ProductLauncher { runner.proc, runner.close.bind(runner) ); - await browser.waitForTarget((t) => t.type() === 'page'); + if (waitForInitialPage) + await browser.waitForTarget((t) => t.type() === 'page'); return browser; } catch (error) { runner.kill(); @@ -245,6 +247,7 @@ class FirefoxLauncher implements ProductLauncher { slowMo = 0, timeout = 30000, extraPrefsFirefox = {}, + waitForInitialPage = true, } = options; const firefoxArguments = []; @@ -313,7 +316,8 @@ class FirefoxLauncher implements ProductLauncher { runner.proc, runner.close.bind(runner) ); - await browser.waitForTarget((t) => t.type() === 'page'); + if (waitForInitialPage) + await browser.waitForTarget((t) => t.type() === 'page'); return browser; } catch (error) { runner.kill(); @@ -583,9 +587,10 @@ class FirefoxLauncher implements ProductLauncher { } } -function resolveExecutablePath( - launcher: ChromeLauncher | FirefoxLauncher -): { executablePath: string; missingText?: string } { +function resolveExecutablePath(launcher: ChromeLauncher | FirefoxLauncher): { + executablePath: string; + missingText?: string; +} { let downloadPath: string; // puppeteer-core doesn't take into account PUPPETEER_* env variables. if (!launcher._isPuppeteerCore) { diff --git a/src/node/Puppeteer.ts b/src/node/Puppeteer.ts index 84170f89f0287..fd4dbe1d8c1e5 100644 --- a/src/node/Puppeteer.ts +++ b/src/node/Puppeteer.ts @@ -84,12 +84,8 @@ export class PuppeteerNode extends Puppeteer { productName?: Product; } & CommonPuppeteerSettings ) { - const { - projectRoot, - preferredRevision, - productName, - ...commonSettings - } = settings; + const { projectRoot, preferredRevision, productName, ...commonSettings } = + settings; super(commonSettings); this._projectRoot = projectRoot; this.__productName = productName; diff --git a/src/node/install.ts b/src/node/install.ts index 373cfbd558de6..d7e751f44ef87 100644 --- a/src/node/install.ts +++ b/src/node/install.ts @@ -26,7 +26,7 @@ const supportedProducts = { firefox: 'Firefox Nightly', } as const; -export async function downloadBrowser() { +export async function downloadBrowser(): Promise { const downloadHost = process.env.PUPPETEER_DOWNLOAD_HOST || process.env.npm_config_puppeteer_download_host || @@ -178,7 +178,7 @@ export async function downloadBrowser() { } } -export function logPolitely(toBeLogged) { +export function logPolitely(toBeLogged: unknown): void { const logLevel = process.env.npm_config_loglevel; const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1; diff --git a/src/revisions.ts b/src/revisions.ts index 521f35bd5370c..3b86c4ea9bf54 100644 --- a/src/revisions.ts +++ b/src/revisions.ts @@ -20,6 +20,6 @@ type Revisions = Readonly<{ }>; export const PUPPETEER_REVISIONS: Revisions = { - chromium: '869685', + chromium: '884014', firefox: 'latest', }; diff --git a/test-ts-types/js-cjs-import-esm-output/package.json b/test-ts-types/js-cjs-import-esm-output/package.json index 063237ea8a3d0..d2aa4f9434afd 100644 --- a/test-ts-types/js-cjs-import-esm-output/package.json +++ b/test-ts-types/js-cjs-import-esm-output/package.json @@ -7,7 +7,7 @@ "compile": "../../node_modules/.bin/tsc" }, "devDependencies": { - "typescript": "^4.1.3" + "typescript": "4.2.4" }, "dependencies": { "puppeteer": "file:../../puppeteer.tgz" diff --git a/test-ts-types/js-esm-import-cjs-output/bad.js b/test-ts-types/js-esm-import-cjs-output/bad.js index ba7b56133d4fe..cda9916d8303d 100644 --- a/test-ts-types/js-esm-import-cjs-output/bad.js +++ b/test-ts-types/js-esm-import-cjs-output/bad.js @@ -14,5 +14,12 @@ async function run() { */ const div = await page.$('div'); console.log('got a div!', div); + const contentsOfDiv = await div.evaluate( + /** + * @param {number} divElem + * @returns number + */ + (divElem) => divElem.innerText + ); } run(); diff --git a/test-ts-types/js-esm-import-cjs-output/package.json b/test-ts-types/js-esm-import-cjs-output/package.json index 063237ea8a3d0..d2aa4f9434afd 100644 --- a/test-ts-types/js-esm-import-cjs-output/package.json +++ b/test-ts-types/js-esm-import-cjs-output/package.json @@ -7,7 +7,7 @@ "compile": "../../node_modules/.bin/tsc" }, "devDependencies": { - "typescript": "^4.1.3" + "typescript": "4.2.4" }, "dependencies": { "puppeteer": "file:../../puppeteer.tgz" diff --git a/test-ts-types/js-esm-import-esm-output/package.json b/test-ts-types/js-esm-import-esm-output/package.json index 063237ea8a3d0..d2aa4f9434afd 100644 --- a/test-ts-types/js-esm-import-esm-output/package.json +++ b/test-ts-types/js-esm-import-esm-output/package.json @@ -7,7 +7,7 @@ "compile": "../../node_modules/.bin/tsc" }, "devDependencies": { - "typescript": "^4.1.3" + "typescript": "4.2.4" }, "dependencies": { "puppeteer": "file:../../puppeteer.tgz" diff --git a/test-ts-types/ts-cjs-import-cjs-output/package.json b/test-ts-types/ts-cjs-import-cjs-output/package.json index 063237ea8a3d0..d2aa4f9434afd 100644 --- a/test-ts-types/ts-cjs-import-cjs-output/package.json +++ b/test-ts-types/ts-cjs-import-cjs-output/package.json @@ -7,7 +7,7 @@ "compile": "../../node_modules/.bin/tsc" }, "devDependencies": { - "typescript": "^4.1.3" + "typescript": "4.2.4" }, "dependencies": { "puppeteer": "file:../../puppeteer.tgz" diff --git a/test-ts-types/ts-esm-import-cjs-output/package.json b/test-ts-types/ts-esm-import-cjs-output/package.json index 063237ea8a3d0..d2aa4f9434afd 100644 --- a/test-ts-types/ts-esm-import-cjs-output/package.json +++ b/test-ts-types/ts-esm-import-cjs-output/package.json @@ -7,7 +7,7 @@ "compile": "../../node_modules/.bin/tsc" }, "devDependencies": { - "typescript": "^4.1.3" + "typescript": "4.2.4" }, "dependencies": { "puppeteer": "file:../../puppeteer.tgz" diff --git a/test-ts-types/ts-esm-import-esm-output/bad.ts b/test-ts-types/ts-esm-import-esm-output/bad.ts index 4aeb970709652..d08e6f450ade4 100644 --- a/test-ts-types/ts-esm-import-esm-output/bad.ts +++ b/test-ts-types/ts-esm-import-esm-output/bad.ts @@ -10,9 +10,21 @@ async function run() { const browser2 = await puppeteer.launch(); // 'foo' is invalid argument const page = await browser2.newPage('foo'); - const div = (await page.$('div')) as puppeteer.ElementHandle< - HTMLAnchorElement - >; + const div = (await page.$( + 'div' + )) as puppeteer.ElementHandle; console.log('got a div!', div); + const contentsOfDiv = await div.evaluate( + // Bad: the type system will know here that divElem is an HTMLAnchorElement + // and won't let me tell it it's a number + (divElem: number) => divElem.innerText + ); + // Bad: the type system will know here that divElem is an HTMLAnchorElement + // and won't let me tell it it's a number via the generic + const contentsOfDiv2 = await div.evaluate<(x: number) => string>( + // Bad: now I've forced it to be a number (which is an error also) + // I can't call `innerText` on it. + (divElem: number) => divElem.innerText + ); } run(); diff --git a/test-ts-types/ts-esm-import-esm-output/good.ts b/test-ts-types/ts-esm-import-esm-output/good.ts index ed7764140d8b6..af19f0efb6351 100644 --- a/test-ts-types/ts-esm-import-esm-output/good.ts +++ b/test-ts-types/ts-esm-import-esm-output/good.ts @@ -9,5 +9,7 @@ async function run() { const page = await browser.newPage(); const div = (await page.$('div')) as ElementHandle; console.log('got a div!', div); + + const contentsOfDiv = await div.evaluate((divElem) => divElem.innerText); } run(); diff --git a/test-ts-types/ts-esm-import-esm-output/package.json b/test-ts-types/ts-esm-import-esm-output/package.json index 063237ea8a3d0..d2aa4f9434afd 100644 --- a/test-ts-types/ts-esm-import-esm-output/package.json +++ b/test-ts-types/ts-esm-import-esm-output/package.json @@ -7,7 +7,7 @@ "compile": "../../node_modules/.bin/tsc" }, "devDependencies": { - "typescript": "^4.1.3" + "typescript": "4.2.4" }, "dependencies": { "puppeteer": "file:../../puppeteer.tgz" diff --git a/test/CDPSession.spec.ts b/test/CDPSession.spec.ts index 2ebf10fd96694..d5999d3d70e13 100644 --- a/test/CDPSession.spec.ts +++ b/test/CDPSession.spec.ts @@ -103,4 +103,11 @@ describeChromeOnly('Target.createCDPSession', function () { await client.send('ThisCommand.DoesNotExist'); } }); + + it('should expose the underlying connection', async () => { + const { page } = getTestState(); + + const client = await page.target().createCDPSession(); + expect(client.connection()).toBeTruthy(); + }); }); diff --git a/test/accessibility.spec.ts b/test/accessibility.spec.ts index 605915b8781e2..5198e255a9cff 100644 --- a/test/accessibility.spec.ts +++ b/test/accessibility.spec.ts @@ -87,10 +87,10 @@ describeFailsFirefox('Accessibility', function () { ], } : { - role: 'WebArea', + role: 'RootWebArea', name: 'Accessibility Test', children: [ - { role: 'text', name: 'Hello World' }, + { role: 'StaticText', name: 'Hello World' }, { role: 'heading', name: 'Inputs', level: 1 }, { role: 'textbox', name: 'Empty input', focused: true }, { role: 'textbox', name: 'readonly input', readonly: true }, @@ -148,7 +148,7 @@ describeFailsFirefox('Accessibility', function () { name: '', children: [ { - role: 'text', + role: 'StaticText', name: 'hi', }, ], @@ -230,7 +230,7 @@ describeFailsFirefox('Accessibility', function () { ], } : { - role: 'WebArea', + role: 'RootWebArea', name: '', children: [ { @@ -263,7 +263,7 @@ describeFailsFirefox('Accessibility', function () { name: 'Edit this image: ', }, { - role: 'text', + role: 'StaticText', name: 'my fake image', }, ], @@ -274,7 +274,7 @@ describeFailsFirefox('Accessibility', function () { value: 'Edit this image: ', children: [ { - role: 'text', + role: 'StaticText', name: 'Edit this image:', }, { @@ -300,7 +300,7 @@ describeFailsFirefox('Accessibility', function () { value: 'Edit this image: my fake image', children: [ { - role: 'text', + role: 'StaticText', name: 'my fake image', }, ], @@ -309,9 +309,10 @@ describeFailsFirefox('Accessibility', function () { role: 'textbox', name: '', value: 'Edit this image: ', + multiline: true, children: [ { - role: 'text', + role: 'StaticText', name: 'Edit this image:', }, { @@ -336,28 +337,7 @@ describeFailsFirefox('Accessibility', function () { role: 'textbox', name: '', value: 'Edit this image:', - }); - }); - it('plain text field without role should not have content', async () => { - const { page } = getTestState(); - - await page.setContent(` -
Edit this image:my fake image
`); - const snapshot = await page.accessibility.snapshot(); - expect(snapshot.children[0]).toEqual({ - role: 'generic', - name: '', - }); - }); - it('plain text field with tabindex and without role should not have content', async () => { - const { page } = getTestState(); - - await page.setContent(` -
Edit this image:my fake image
`); - const snapshot = await page.accessibility.snapshot(); - expect(snapshot.children[0]).toEqual({ - role: 'generic', - name: '', + multiline: true, }); }); }); @@ -502,7 +482,7 @@ describeFailsFirefox('Accessibility', function () { { role: 'button', name: 'My Button', - children: [{ role: 'text', name: 'My Button' }], + children: [{ role: 'StaticText', name: 'My Button' }], }, ], }); diff --git a/test/assets/cached/one-style-font.css b/test/assets/cached/one-style-font.css new file mode 100644 index 0000000000000..6178de0350e98 --- /dev/null +++ b/test/assets/cached/one-style-font.css @@ -0,0 +1,9 @@ +@font-face { + font-family: 'one-style'; + src: url('./one-style.woff') format('woff'); +} + +body { + background-color: pink; + font-family: 'one-style', sans-serif; +} diff --git a/test/assets/cached/one-style-font.html b/test/assets/cached/one-style-font.html new file mode 100644 index 0000000000000..8e7236dfb35e3 --- /dev/null +++ b/test/assets/cached/one-style-font.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/click.spec.ts b/test/click.spec.ts index 75779bab0e1d1..cb8179834b9a9 100644 --- a/test/click.spec.ts +++ b/test/click.spec.ts @@ -74,10 +74,9 @@ describe('Page.click', function () { const { page } = getTestState(); const newPage = await page.browser().newPage(); - await Promise.all([ - newPage.close(), - newPage.mouse.click(1, 2), - ]).catch(() => {}); + await Promise.all([newPage.close(), newPage.mouse.click(1, 2)]).catch( + () => {} + ); }); it('should click the button after navigation ', async () => { const { page, server } = getTestState(); diff --git a/test/cookies.spec.ts b/test/cookies.spec.ts index 22e152fd9721e..ad7339b0d390a 100644 --- a/test/cookies.spec.ts +++ b/test/cookies.spec.ts @@ -231,13 +231,12 @@ describe('Cookie specs', () => { value: 'bar', } ); - expectCookieEquals( - await page.evaluate(() => { - const cookies = document.cookie.split(';'); - return cookies.map((cookie) => cookie.trim()).sort(); - }), - ['foo=bar', 'password=123456'] - ); + const cookieStrings = await page.evaluate(() => { + const cookies = document.cookie.split(';'); + return cookies.map((cookie) => cookie.trim()).sort(); + }); + + expect(cookieStrings).toEqual(['foo=bar', 'password=123456']); }); it('should have |expires| set to |-1| for session cookies', async () => { const { page, server } = getTestState(); @@ -475,11 +474,8 @@ describe('Cookie specs', () => { itFailsFirefox( 'should set secure same-site cookies from a frame', async () => { - const { - httpsServer, - puppeteer, - defaultBrowserOptions, - } = getTestState(); + const { httpsServer, puppeteer, defaultBrowserOptions } = + getTestState(); const browser = await puppeteer.launch({ ...defaultBrowserOptions, diff --git a/test/coverage.spec.ts b/test/coverage.spec.ts index b2a8730f0ddab..a5ff0dc0f57df 100644 --- a/test/coverage.spec.ts +++ b/test/coverage.spec.ts @@ -125,6 +125,17 @@ describe('Coverage specs', function () { JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':/') ).toBeGolden('jscoverage-involved.txt'); }); + // @see https://crbug.com/990945 + xit('should not hang when there is a debugger statement', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); describe('resetOnNavigation', function () { it('should report scripts across navigations when disabled', async () => { const { page, server } = getTestState(); @@ -146,17 +157,6 @@ describe('Coverage specs', function () { expect(coverage.length).toBe(0); }); }); - // @see https://crbug.com/990945 - xit('should not hang when there is a debugger statement', async () => { - const { page, server } = getTestState(); - - await page.coverage.startJSCoverage(); - await page.goto(server.EMPTY_PAGE); - await page.evaluate(() => { - debugger; // eslint-disable-line no-debugger - }); - await page.coverage.stopJSCoverage(); - }); }); describeChromeOnly('CSSCoverage', function () { diff --git a/test/elementhandle.spec.ts b/test/elementhandle.spec.ts index 592ba8d569071..f65027fcea61d 100644 --- a/test/elementhandle.spec.ts +++ b/test/elementhandle.spec.ts @@ -171,7 +171,8 @@ describe('ElementHandle specs', function () { }); describe('ElementHandle.click', function () { - it('should work', async () => { + // See https://github.com/puppeteer/puppeteer/issues/7175 + itFailsFirefox('should work', async () => { const { page, server } = getTestState(); await page.goto(server.PREFIX + '/input/button.html'); @@ -319,7 +320,8 @@ describe('ElementHandle specs', function () { ) ); } - const handlerNamesAfterUnregistering = puppeteer.customQueryHandlerNames(); + const handlerNamesAfterUnregistering = + puppeteer.customQueryHandlerNames(); expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy(); }); it('should throw with invalid query names', () => { diff --git a/test/emulation.spec.ts b/test/emulation.spec.ts index 82d263baf12a6..eb6f1b957ca84 100644 --- a/test/emulation.spec.ts +++ b/test/emulation.spec.ts @@ -386,4 +386,26 @@ describe('Emulation', () => { expect(error.message).toBe('Unsupported vision deficiency: invalid'); }); }); + + describeFailsFirefox('Page.emulateNetworkConditions', function () { + it('should change navigator.connection.effectiveType', async () => { + const { page, puppeteer } = getTestState(); + + const slow3G = puppeteer.networkConditions['Slow 3G']; + const fast3G = puppeteer.networkConditions['Fast 3G']; + + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('4g'); + await page.emulateNetworkConditions(fast3G); + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('3g'); + await page.emulateNetworkConditions(slow3G); + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('2g'); + await page.emulateNetworkConditions(null); + }); + }); }); diff --git a/test/headful.spec.ts b/test/headful.spec.ts index 119823eb50b42..62441450c4339 100644 --- a/test/headful.spec.ts +++ b/test/headful.spec.ts @@ -42,9 +42,11 @@ describeChromeOnly('headful tests', function () { let headfulOptions; let headlessOptions; let extensionOptions; + let forcedOopifOptions; + const browsers = []; beforeEach(() => { - const { defaultBrowserOptions } = getTestState(); + const { server, defaultBrowserOptions } = getTestState(); headfulOptions = Object.assign({}, defaultBrowserOptions, { headless: false, }); @@ -59,12 +61,43 @@ describeChromeOnly('headful tests', function () { `--load-extension=${extensionPath}`, ], }); + + forcedOopifOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + devtools: true, + args: [ + `--host-rules=MAP oopifdomain 127.0.0.1`, + `--isolate-origins=${server.PREFIX.replace( + 'localhost', + 'oopifdomain' + )}`, + ], + }); + }); + + async function launchBrowser(puppeteer, options) { + const browser = await puppeteer.launch(options); + browsers.push(browser); + return browser; + } + + afterEach(() => { + for (const i in browsers) { + const browser = browsers[i]; + if (browser.isConnected()) { + browser.close(); + } + delete browsers[i]; + } }); describe('HEADFUL', function () { it('background_page target type should be available', async () => { const { puppeteer } = getTestState(); - const browserWithExtension = await puppeteer.launch(extensionOptions); + const browserWithExtension = await launchBrowser( + puppeteer, + extensionOptions + ); const page = await browserWithExtension.newPage(); const backgroundPageTarget = await browserWithExtension.waitForTarget( (target) => target.type() === 'background_page' @@ -75,7 +108,10 @@ describeChromeOnly('headful tests', function () { }); it('target.page() should return a background_page', async function () { const { puppeteer } = getTestState(); - const browserWithExtension = await puppeteer.launch(extensionOptions); + const browserWithExtension = await launchBrowser( + puppeteer, + extensionOptions + ); const backgroundPageTarget = await browserWithExtension.waitForTarget( (target) => target.type() === 'background_page' ); @@ -86,7 +122,7 @@ describeChromeOnly('headful tests', function () { }); it('should have default url when launching browser', async function () { const { puppeteer } = getTestState(); - const browser = await puppeteer.launch(extensionOptions); + const browser = await launchBrowser(puppeteer, extensionOptions); const pages = (await browser.pages()).map((page) => page.url()); expect(pages).toEqual(['about:blank']); await browser.close(); @@ -99,7 +135,8 @@ describeChromeOnly('headful tests', function () { const userDataDir = await mkdtempAsync(TMP_FOLDER); // Write a cookie in headful chrome - const headfulBrowser = await puppeteer.launch( + const headfulBrowser = await launchBrowser( + puppeteer, Object.assign({ userDataDir }, headfulOptions) ); const headfulPage = await headfulBrowser.newPage(); @@ -111,7 +148,8 @@ describeChromeOnly('headful tests', function () { ); await headfulBrowser.close(); // Read the cookie from headless chrome - const headlessBrowser = await puppeteer.launch( + const headlessBrowser = await launchBrowser( + puppeteer, Object.assign({ userDataDir }, headlessOptions) ); const headlessPage = await headlessBrowser.newPage(); @@ -128,7 +166,7 @@ describeChromeOnly('headful tests', function () { const { server, puppeteer } = getTestState(); // https://google.com is isolated by default in Chromium embedder. - const browser = await puppeteer.launch(headfulOptions); + const browser = await launchBrowser(puppeteer, headfulOptions); const page = await browser.newPage(); await page.goto(server.EMPTY_PAGE); await page.setRequestInterception(true); @@ -147,10 +185,62 @@ describeChromeOnly('headful tests', function () { expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']); await browser.close(); }); + it('OOPIF: should expose events within OOPIFs', async () => { + const { server, puppeteer } = getTestState(); + + const browser = await launchBrowser(puppeteer, forcedOopifOptions); + const page = await browser.newPage(); + + // Setup our session listeners to observe OOPIF activity. + const session = await page.target().createCDPSession(); + const networkEvents = []; + const otherSessions = []; + await session.send('Target.setAutoAttach', { + autoAttach: true, + flatten: true, + waitForDebuggerOnStart: true, + }); + session.on('sessionattached', async (session) => { + otherSessions.push(session); + + session.on('Network.requestWillBeSent', (params) => + networkEvents.push(params) + ); + await session.send('Network.enable'); + await session.send('Runtime.runIfWaitingForDebugger'); + }); + + // Navigate to the empty page and add an OOPIF iframe with at least one request. + await page.goto(server.EMPTY_PAGE); + await page.evaluate((frameUrl) => { + const frame = document.createElement('iframe'); + frame.setAttribute('src', frameUrl); + document.body.appendChild(frame); + return new Promise((x, y) => { + frame.onload = x; + frame.onerror = y; + }); + }, server.PREFIX.replace('localhost', 'oopifdomain') + '/one-style.html'); + await page.waitForSelector('iframe'); + + // Ensure we found the iframe session. + expect(otherSessions).toHaveLength(1); + + // Resume the iframe and trigger another request. + const iframeSession = otherSessions[0]; + await iframeSession.send('Runtime.evaluate', { + expression: `fetch('/fetch')`, + awaitPromise: true, + }); + await browser.close(); + + const requests = networkEvents.map((event) => event.request.url); + expect(requests).toContain(`http://oopifdomain:${server.PORT}/fetch`); + }); it('should close browser with beforeunload page', async () => { const { server, puppeteer } = getTestState(); - const browser = await puppeteer.launch(headfulOptions); + const browser = await launchBrowser(puppeteer, headfulOptions); const page = await browser.newPage(); await page.goto(server.PREFIX + '/beforeunload.html'); // We have to interact with a page so that 'beforeunload' handlers @@ -161,13 +251,14 @@ describeChromeOnly('headful tests', function () { it('should open devtools when "devtools: true" option is given', async () => { const { puppeteer } = getTestState(); - const browser = await puppeteer.launch( + const browser = await launchBrowser( + puppeteer, Object.assign({ devtools: true }, headfulOptions) ); const context = await browser.createIncognitoBrowserContext(); await Promise.all([ context.newPage(), - context.waitForTarget((target) => target.url().includes('devtools://')), + browser.waitForTarget((target) => target.url().includes('devtools://')), ]); await browser.close(); }); @@ -176,7 +267,7 @@ describeChromeOnly('headful tests', function () { describe('Page.bringToFront', function () { it('should work', async () => { const { puppeteer } = getTestState(); - const browser = await puppeteer.launch(headfulOptions); + const browser = await launchBrowser(puppeteer, headfulOptions); const page1 = await browser.newPage(); const page2 = await browser.newPage(); diff --git a/test/launcher.spec.ts b/test/launcher.spec.ts index c302db28c6b2f..b4347929802dd 100644 --- a/test/launcher.spec.ts +++ b/test/launcher.spec.ts @@ -21,6 +21,7 @@ import { promisify } from 'util'; import Protocol from 'devtools-protocol'; import { getTestState, + itChromeOnly, itFailsFirefox, itOnlyRegularInstall, } from './mocha-utils'; // eslint-disable-line import/extensions @@ -430,6 +431,24 @@ describe('Launcher specs', function () { expect(screenshot).toBeInstanceOf(Buffer); await browser.close(); }); + itChromeOnly( + 'should launch Chrome properly with --no-startup-window and waitForInitialPage=false', + async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = { + args: ['--no-startup-window'], + waitForInitialPage: false, + // This is needed to prevent Puppeteer from adding an initial blank page. + // See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200 + ignoreDefaultArgs: true, + ...defaultBrowserOptions, + }; + const browser = await puppeteer.launch(options); + const pages = await browser.pages(); + expect(pages.length).toBe(0); + await browser.close(); + } + ); }); describe('Puppeteer.launch', function () { @@ -512,11 +531,8 @@ describe('Launcher specs', function () { ]); }); it('should support ignoreHTTPSErrors option', async () => { - const { - httpsServer, - puppeteer, - defaultBrowserOptions, - } = getTestState(); + const { httpsServer, puppeteer, defaultBrowserOptions } = + getTestState(); const originalBrowser = await puppeteer.launch(defaultBrowserOptions); const browserWSEndpoint = originalBrowser.wsEndpoint(); diff --git a/test/mocha-utils.ts b/test/mocha-utils.ts index 6d713f1c13889..238addfef23ac 100644 --- a/test/mocha-utils.ts +++ b/test/mocha-utils.ts @@ -31,6 +31,7 @@ import rimraf from 'rimraf'; import expect from 'expect'; import { trackCoverage } from './coverage-utils.js'; +import Protocol from 'devtools-protocol'; const setupServer = async () => { const assetsPath = path.join(__dirname, 'assets'); @@ -177,7 +178,10 @@ export const itFailsWindowsUntilDate = ( return it(description, body); }; -export const itFailsWindows = (description: string, body: Mocha.Func) => { +export const itFailsWindows = ( + description: string, + body: Mocha.Func +): Mocha.Test => { if (os.platform() === 'win32') { return xit(description, body); } @@ -217,7 +221,7 @@ console.log( }` ); -export const setupTestBrowserHooks = () => { +export const setupTestBrowserHooks = (): void => { before(async () => { const browser = await puppeteer.launch(defaultBrowserOptions); state.browser = browser; @@ -229,7 +233,7 @@ export const setupTestBrowserHooks = () => { }); }; -export const setupTestPageAndContextHooks = () => { +export const setupTestPageAndContextHooks = (): void => { beforeEach(async () => { state.context = await state.browser.createIncognitoBrowserContext(); state.page = await state.context.newPage(); @@ -244,7 +248,7 @@ export const setupTestPageAndContextHooks = () => { export const mochaHooks = { beforeAll: [ - async () => { + async (): Promise => { const { server, httpsServer } = await setupServer(); state.puppeteer = puppeteer; @@ -259,13 +263,13 @@ export const mochaHooks = { coverageHooks.beforeAll, ], - beforeEach: async () => { + beforeEach: async (): Promise => { state.server.reset(); state.httpsServer.reset(); }, afterAll: [ - async () => { + async (): Promise => { await state.server.stop(); state.server = null; await state.httpsServer.stop(); @@ -274,12 +278,15 @@ export const mochaHooks = { coverageHooks.afterAll, ], - afterEach: () => { + afterEach: (): void => { sinon.restore(); }, }; -export const expectCookieEquals = (cookies, expectedCookies) => { +export const expectCookieEquals = ( + cookies: Protocol.Network.Cookie[], + expectedCookies: Array> +): void => { const { isChrome } = getTestState(); if (!isChrome) { // Only keep standard properties when testing on a browser other than Chrome. diff --git a/test/page.spec.ts b/test/page.spec.ts index 89664124ae878..b947b6f8b37fe 100644 --- a/test/page.spec.ts +++ b/test/page.spec.ts @@ -314,7 +314,7 @@ describe('Page', function () { ]); }); itFailsFirefox( - 'should isolate permissions between browser contexs', + 'should isolate permissions between browser contexts', async () => { const { page, server, context, browser } = getTestState(); @@ -400,28 +400,6 @@ describe('Page', function () { }); }); - describeFailsFirefox('Page.emulateNetworkConditions', function () { - it('should change navigator.connection.effectiveType', async () => { - const { page, puppeteer } = getTestState(); - - const slow3G = puppeteer.networkConditions['Slow 3G']; - const fast3G = puppeteer.networkConditions['Fast 3G']; - - expect( - await page.evaluate('window.navigator.connection.effectiveType') - ).toBe('4g'); - await page.emulateNetworkConditions(fast3G); - expect( - await page.evaluate('window.navigator.connection.effectiveType') - ).toBe('3g'); - await page.emulateNetworkConditions(slow3G); - expect( - await page.evaluate('window.navigator.connection.effectiveType') - ).toBe('2g'); - await page.emulateNetworkConditions(null); - }); - }); - describe('ExecutionContext.queryObjects', function () { itFailsFirefox('should work', async () => { const { page } = getTestState(); @@ -751,7 +729,6 @@ describe('Page', function () { await page.goto(server.EMPTY_PAGE); const [response] = await Promise.all([ page.waitForResponse(async (response) => { - console.log(response.url()); return response.url() === server.PREFIX + '/digits/2.png'; }), page.evaluate(() => { @@ -1762,7 +1739,7 @@ describe('Page', function () { }); describe('Page.browserContext', function () { - it('should return the correct browser instance', async () => { + it('should return the correct browser context instance', async () => { const { page, context } = getTestState(); expect(page.browserContext()).toBe(context); diff --git a/test/requestinterception.spec.ts b/test/requestinterception.spec.ts index 35e225bb41d8f..b2ca3f8b4fd35 100644 --- a/test/requestinterception.spec.ts +++ b/test/requestinterception.spec.ts @@ -495,13 +495,14 @@ describe('request interception', function () { expect(urls.has('one-style.html')).toBe(true); expect(urls.has('one-style.css')).toBe(true); }); - it('should not cache if not cache-safe', async () => { + it('should not cache if cache disabled', async () => { const { page, server } = getTestState(); // Load and re-load to make sure it's cached. await page.goto(server.PREFIX + '/cached/one-style.html'); - await page.setRequestInterception(true, false); + await page.setRequestInterception(true); + await page.setCacheEnabled(false); page.on('request', (request) => request.continue()); const cached = []; @@ -510,13 +511,14 @@ describe('request interception', function () { await page.reload(); expect(cached.length).toBe(0); }); - it('should cache if cache-safe', async () => { + it('should cache if cache enabled', async () => { const { page, server } = getTestState(); // Load and re-load to make sure it's cached. await page.goto(server.PREFIX + '/cached/one-style.html'); - await page.setRequestInterception(true, true); + await page.setRequestInterception(true); + await page.setCacheEnabled(true); page.on('request', (request) => request.continue()); const cached = []; @@ -525,6 +527,16 @@ describe('request interception', function () { await page.reload(); expect(cached.length).toBe(1); }); + it('should load fonts if cache enabled', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', (request) => request.continue()); + + await page.goto(server.PREFIX + '/cached/one-style-font.html'); + await page.waitForResponse((r) => r.url().endsWith('/one-style.woff')); + }); }); describeFailsFirefox('Request.continue', function () { diff --git a/test/waittask.spec.ts b/test/waittask.spec.ts index 76e4765f38118..ee76bbb64d53d 100644 --- a/test/waittask.spec.ts +++ b/test/waittask.spec.ts @@ -358,11 +358,10 @@ describe('waittask specs', function () { const endTime = Date.now(); /* In a perfect world endTime - startTime would be exactly 1000 but we * expect some fluctuations and for it to be off by a little bit. So to - * avoid a flaky test we'll make sure it waited for roughly 1 second by - * ensuring 900 < endTime - startTime < 1100 + * avoid a flaky test we'll make sure it waited for roughly 1 second. */ - expect(endTime - startTime).toBeGreaterThan(900); - expect(endTime - startTime).toBeLessThan(1100); + expect(endTime - startTime).toBeGreaterThan(700); + expect(endTime - startTime).toBeLessThan(1300); }); }); @@ -376,11 +375,10 @@ describe('waittask specs', function () { const endTime = Date.now(); /* In a perfect world endTime - startTime would be exactly 1000 but we * expect some fluctuations and for it to be off by a little bit. So to - * avoid a flaky test we'll make sure it waited for roughly 1 second by - * ensuring 900 < endTime - startTime < 1100 + * avoid a flaky test we'll make sure it waited for roughly 1 second */ - expect(endTime - startTime).toBeGreaterThan(900); - expect(endTime - startTime).toBeLessThan(1100); + expect(endTime - startTime).toBeGreaterThan(700); + expect(endTime - startTime).toBeLessThan(1300); }); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index b1229ed97d06b..571f4cc62e4e2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -3,9 +3,8 @@ "esModuleInterop": true, "allowJs": true, "checkJs": true, - "target": "ESNext", + "target": "ES2019", "moduleResolution": "node", - "module": "ESNext", "declaration": true, "declarationMap": true, "resolveJsonModule": true, diff --git a/tsconfig.json b/tsconfig.json index 69717ed6d9b2e..0fe7e06ad83c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,16 @@ /** - * This configuration only exists for the API Extractor tool. See the details in - * CONTRIBUTING.md that describes our TypeScript setup. -*/ + * This configuration only exists for the API Extractor tool and for VSCode to use. It is NOT the tsconfig used for compilation. + * For CJS builds, `tsconfig.cjs.json` is used, and for ESM, it's `tsconfig.esm.json`. + * See the details in CONTRIBUTING.md that describes our TypeScript setup. + */ { "extends": "./tsconfig.base.json", "compilerOptions": { - "noEmit": true + "noEmit": true, + /* This module setting is just for VSCode so it doesn't throw error when we + use dynamic imports. + */ + "module": "esnext" }, "include": ["src"] } diff --git a/utils/check_availability.js b/utils/check_availability.js index e24e2b9dc3605..fbc115c1a53f0 100755 --- a/utils/check_availability.js +++ b/utils/check_availability.js @@ -18,8 +18,8 @@ const assert = require('assert'); const https = require('https'); // run `npm run dev-install` if lib dir is missing -const BrowserFetcher = require('../lib/cjs/puppeteer/node/BrowserFetcher.js') - .BrowserFetcher; +const BrowserFetcher = + require('../lib/cjs/puppeteer/node/BrowserFetcher.js').BrowserFetcher; const SUPPORTER_PLATFORMS = ['linux', 'mac', 'win32', 'win64']; const fetchers = SUPPORTER_PLATFORMS.map( diff --git a/utils/doclint/check_public_api/index.js b/utils/doclint/check_public_api/index.js index e524e52b6a75e..68e60bc96a87f 100644 --- a/utils/doclint/check_public_api/index.js +++ b/utils/doclint/check_public_api/index.js @@ -28,10 +28,12 @@ const EXCLUDE_PROPERTIES = new Set([ 'Page.create', 'JSHandle.toString', 'TimeoutError.name', - /* This isn't an actual property, but a TypeScript generic. + /* These are not actual properties, but a TypeScript generic. * DocLint incorrectly parses it as a property. */ 'ElementHandle.ElementType', + 'ElementHandle.HandleObjectType', + 'JSHandle.HandleObjectType', ]); /** @@ -685,7 +687,8 @@ function compareDocumentations(actual, expected) { 'Method Page.emulateVisionDeficiency() type', { actualName: 'string', - expectedName: 'Object', + expectedName: + '"none"|"achromatopsia"|"blurredVision"|"deuteranopia"|"protanopia"|"tritanopia"', }, ], [ @@ -849,14 +852,6 @@ function compareDocumentations(actual, expected) { expectedName: '...DeleteCookiesRequest', }, ], - [ - 'Method Page.emulateVisionDeficiency() type', - { - actualName: 'string', - expectedName: - '"none"|"achromatopsia"|"blurredVision"|"deuteranopia"|"protanopia"|"tritanopia"', - }, - ], [ 'Method BrowserContext.overridePermissions() permissions', { @@ -885,6 +880,27 @@ function compareDocumentations(actual, expected) { expectedName: 'Object', }, ], + [ + 'Method EventEmitter.emit() eventData', + { + actualName: 'Object', + expectedName: 'unknown', + }, + ], + [ + 'Method Page.queryObjects() prototypeHandle', + { + actualName: 'JSHandle', + expectedName: 'JSHandle', + }, + ], + [ + 'Method ExecutionContext.queryObjects() prototypeHandle', + { + actualName: 'JSHandle', + expectedName: 'JSHandle', + }, + ], ]); const expectedForSource = expectedNamingMismatches.get(source); diff --git a/utils/doclint/preprocessor/index.js b/utils/doclint/preprocessor/index.js index 37d8254b570dd..68967d86f0ceb 100644 --- a/utils/doclint/preprocessor/index.js +++ b/utils/doclint/preprocessor/index.js @@ -20,7 +20,8 @@ const IS_RELEASE = Boolean(process.env.IS_RELEASE); module.exports.ensureReleasedAPILinks = function (sources, version) { // Release version is everything that doesn't include "-". - const apiLinkRegex = /https:\/\/github.com\/puppeteer\/puppeteer\/blob\/v[^/]*\/docs\/api.md/gi; + const apiLinkRegex = + /https:\/\/github.com\/puppeteer\/puppeteer\/blob\/v[^/]*\/docs\/api.md/gi; const lastReleasedAPI = `https://github.com/puppeteer/puppeteer/blob/v${ version.split('-')[0] }/docs/api.md`; diff --git a/utils/fetch_devices.js b/utils/fetch_devices.js index 547b68842e177..f49a09124cd95 100755 --- a/utils/fetch_devices.js +++ b/utils/fetch_devices.js @@ -222,11 +222,9 @@ function loadFromJSONV1(json) { const result = {}; result.type = /** @type {string} */ (parseValue(json, 'type', 'string')); - result.userAgent = /** @type {string} */ (parseValue( - json, - 'user-agent', - 'string' - )); + result.userAgent = /** @type {string} */ ( + parseValue(json, 'user-agent', 'string') + ); const capabilities = parseValue(json, 'capabilities', 'object', []); if (!Array.isArray(capabilities)) @@ -238,11 +236,9 @@ function loadFromJSONV1(json) { result.capabilities.push(capabilities[i]); } - result.deviceScaleFactor = /** @type {number} */ (parseValue( - json['screen'], - 'device-pixel-ratio', - 'number' - )); + result.deviceScaleFactor = /** @type {number} */ ( + parseValue(json['screen'], 'device-pixel-ratio', 'number') + ); if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) throw new Error( 'Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor diff --git a/versions.js b/versions.js index 716c4529f3dc8..502efeb5655e9 100644 --- a/versions.js +++ b/versions.js @@ -17,6 +17,7 @@ const versionsPerRelease = new Map([ // This is a mapping from Chromium version => Puppeteer version. // In Chromium roll patches, use 'NEXT' for the Puppeteer version. + ['92.0.4512.0', 'v10.0.0'], ['91.0.4469.0', 'v9.0.0'], ['90.0.4427.0', 'v8.0.0'], ['90.0.4403.0', 'v7.0.0'],