forked from mirrors/gecko-dev
Bug 1891762 - [puppeteer] Sync vendored puppeteer to v22.6.5 r=webdriver-reviewers,Sasha
Differential Revision: https://phabricator.services.mozilla.com/D207741
This commit is contained in:
parent
397d15d90f
commit
01a62a155f
111 changed files with 3590 additions and 2083 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"packages/puppeteer": "22.4.0",
|
||||
"packages/puppeteer-core": "22.4.0",
|
||||
"packages/puppeteer": "22.6.5",
|
||||
"packages/puppeteer-core": "22.6.5",
|
||||
"packages/testserver": "0.6.0",
|
||||
"packages/ng-schematics": "0.6.0",
|
||||
"packages/browsers": "2.1.0"
|
||||
"packages/browsers": "2.2.2"
|
||||
}
|
||||
|
|
|
|||
46
remote/test/puppeteer/.vscode/launch.template.json
vendored
Normal file
46
remote/test/puppeteer/.vscode/launch.template.json
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "pickString",
|
||||
"id": "suit",
|
||||
"description": "Which test suit to run?",
|
||||
"options": [
|
||||
"chrome-headless",
|
||||
"chrome-headful",
|
||||
"chrome-headless-shell",
|
||||
"firefox-headless",
|
||||
"firefox-headful",
|
||||
"firefox-bidi",
|
||||
"chrome-bidi"
|
||||
],
|
||||
"default": "chrome-headless"
|
||||
}
|
||||
],
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Tests",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"runtimeExecutable": "npm",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeArgs": [
|
||||
"run-script",
|
||||
"test",
|
||||
"--",
|
||||
"--test-suite",
|
||||
"${input:suit}",
|
||||
"--no-coverage",
|
||||
"--no-suggestions"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/**/*.js"],
|
||||
"env": {
|
||||
"DEBUGGER_ATTACHED": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -6,20 +6,20 @@
|
|||
|
||||
/* eslint-disable import/order */
|
||||
|
||||
import {copyFile, readFile, writeFile} from 'fs/promises';
|
||||
import {readFile, writeFile} from 'fs/promises';
|
||||
|
||||
import {docgen, spliceIntoSection} from '@puppeteer/docgen';
|
||||
import {execa} from 'execa';
|
||||
import {task} from 'hereby';
|
||||
import semver from 'semver';
|
||||
|
||||
export const docsNgSchematicsTask = task({
|
||||
name: 'docs:ng-schematics',
|
||||
run: async () => {
|
||||
const readme = await readFile('packages/ng-schematics/README.md', 'utf-8');
|
||||
await writeFile('docs/integrations/ng-schematics.md', readme);
|
||||
},
|
||||
});
|
||||
function addNoTocHeader(markdown) {
|
||||
return `---
|
||||
hide_table_of_contents: true
|
||||
---
|
||||
|
||||
${markdown}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* This logic should match the one in `website/docusaurus.config.js`.
|
||||
|
|
@ -34,10 +34,18 @@ function getApiUrl(version) {
|
|||
}
|
||||
}
|
||||
|
||||
export const docsChromiumSupportTask = task({
|
||||
name: 'docs:chromium-support',
|
||||
export const docsNgSchematicsTask = task({
|
||||
name: 'docs:ng-schematics',
|
||||
run: async () => {
|
||||
const content = await readFile('docs/chromium-support.md', {
|
||||
const readme = await readFile('packages/ng-schematics/README.md', 'utf-8');
|
||||
await writeFile('docs/guides/ng-schematics.md', readme);
|
||||
},
|
||||
});
|
||||
|
||||
export const docsChromiumSupportTask = task({
|
||||
name: 'docs:supported-browsers',
|
||||
run: async () => {
|
||||
const content = await readFile('docs/supported-browsers.md', {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const {versionsPerRelease} = await import('./versions.js');
|
||||
|
|
@ -61,7 +69,7 @@ export const docsChromiumSupportTask = task({
|
|||
}
|
||||
}
|
||||
await writeFile(
|
||||
'docs/chromium-support.md',
|
||||
'docs/supported-browsers.md',
|
||||
spliceIntoSection('version', content, buffer.join('\n'))
|
||||
);
|
||||
},
|
||||
|
|
@ -72,7 +80,8 @@ export const docsTask = task({
|
|||
dependencies: [docsNgSchematicsTask, docsChromiumSupportTask],
|
||||
run: async () => {
|
||||
// Copy main page.
|
||||
await copyFile('README.md', 'docs/index.md');
|
||||
const mainPage = await readFile('README.md', 'utf-8');
|
||||
await writeFile('docs/index.md', addNoTocHeader(mainPage));
|
||||
|
||||
// Generate documentation
|
||||
for (const [name, folder] of [
|
||||
|
|
|
|||
|
|
@ -1,145 +1,21 @@
|
|||
# Puppeteer
|
||||
|
||||
[](https://github.com/puppeteer/puppeteer/actions?query=workflow%3ACI)
|
||||
[](https://github.com/puppeteer/puppeteer/actions/workflows/ci.yml)
|
||||
[](https://npmjs.org/package/puppeteer)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"/>
|
||||
|
||||
#### [Guides](https://pptr.dev/category/guides) | [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting)
|
||||
|
||||
> Puppeteer is a Node.js library which provides a high-level API to control
|
||||
> Chrome/Chromium over the
|
||||
> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/).
|
||||
> Puppeteer runs in
|
||||
> [headless](https://developer.chrome.com/articles/new-headless/)
|
||||
> [headless](https://developer.chrome.com/docs/chromium/new-headless/)
|
||||
> mode by default, but can be configured to run in full ("headful")
|
||||
> Chrome/Chromium.
|
||||
|
||||
#### What can I do?
|
||||
## [Get started](https://pptr.dev/docs) | [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting)
|
||||
|
||||
Most things that you can do manually in the browser can be done using Puppeteer!
|
||||
Here are a few examples to get you started:
|
||||
|
||||
- Generate screenshots and PDFs of pages.
|
||||
- Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e.
|
||||
"SSR" (Server-Side Rendering)).
|
||||
- Automate form submission, UI testing, keyboard input, etc.
|
||||
- Create an automated testing environment using the latest JavaScript and
|
||||
browser features.
|
||||
- Capture a
|
||||
[timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference)
|
||||
of your site to help diagnose performance issues.
|
||||
- [Test Chrome Extensions](https://pptr.dev/guides/chrome-extensions).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
To use Puppeteer in your project, run:
|
||||
|
||||
```bash
|
||||
npm i puppeteer
|
||||
# or using yarn
|
||||
yarn add puppeteer
|
||||
# or using pnpm
|
||||
pnpm i puppeteer
|
||||
```
|
||||
|
||||
When you install Puppeteer, it automatically downloads a recent version of
|
||||
[Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) (~170MB macOS, ~282MB Linux, ~280MB Windows) and a `chrome-headless-shell` binary (starting with Puppeteer v21.6.0) that is [guaranteed to
|
||||
work](https://pptr.dev/faq#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy)
|
||||
with Puppeteer. The browser is downloaded to the `$HOME/.cache/puppeteer` folder
|
||||
by default (starting with Puppeteer v19.0.0). See [configuration](https://pptr.dev/api/puppeteer.configuration) for configuration options and environmental variables to control the download behavor.
|
||||
|
||||
If you deploy a project using Puppeteer to a hosting provider, such as Render or
|
||||
Heroku, you might need to reconfigure the location of the cache to be within
|
||||
your project folder (see an example below) because not all hosting providers
|
||||
include `$HOME/.cache` into the project's deployment.
|
||||
|
||||
For a version of Puppeteer without the browser installation, see
|
||||
[`puppeteer-core`](#puppeteer-core).
|
||||
|
||||
If used with TypeScript, the minimum supported TypeScript version is `4.7.4`.
|
||||
|
||||
#### Configuration
|
||||
|
||||
Puppeteer uses several defaults that can be customized through configuration
|
||||
files.
|
||||
|
||||
For example, to change the default cache directory Puppeteer uses to install
|
||||
browsers, you can add a `.puppeteerrc.cjs` (or `puppeteer.config.cjs`) at the
|
||||
root of your application with the contents
|
||||
|
||||
```js
|
||||
const {join} = require('path');
|
||||
|
||||
/**
|
||||
* @type {import("puppeteer").Configuration}
|
||||
*/
|
||||
module.exports = {
|
||||
// Changes the cache location for Puppeteer.
|
||||
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
|
||||
};
|
||||
```
|
||||
|
||||
After adding the configuration file, you will need to remove and reinstall
|
||||
`puppeteer` for it to take effect.
|
||||
|
||||
See the [configuration guide](https://pptr.dev/guides/configuration) for more
|
||||
information.
|
||||
|
||||
#### `puppeteer-core`
|
||||
|
||||
For every release since v1.7.0 we publish two packages:
|
||||
|
||||
- [`puppeteer`](https://www.npmjs.com/package/puppeteer)
|
||||
- [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core)
|
||||
|
||||
`puppeteer` is a _product_ for browser automation. When installed, it downloads
|
||||
a version of Chrome, which it then drives using `puppeteer-core`. Being an
|
||||
end-user product, `puppeteer` automates several workflows using reasonable
|
||||
defaults [that can be customized](https://pptr.dev/guides/configuration).
|
||||
|
||||
`puppeteer-core` is a _library_ to help drive anything that supports DevTools
|
||||
protocol. Being a library, `puppeteer-core` is fully driven through its
|
||||
programmatic interface implying no defaults are assumed and `puppeteer-core`
|
||||
will not download Chrome when installed.
|
||||
|
||||
You should use `puppeteer-core` if you are
|
||||
[connecting to a remote browser](https://pptr.dev/api/puppeteer.puppeteer.connect)
|
||||
or [managing browsers yourself](https://pptr.dev/browsers-api/).
|
||||
If you are managing browsers yourself, you will need to call
|
||||
[`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) with
|
||||
an explicit
|
||||
[`executablePath`](https://pptr.dev/api/puppeteer.launchoptions)
|
||||
(or [`channel`](https://pptr.dev/api/puppeteer.launchoptions) if it's
|
||||
installed in a standard location).
|
||||
|
||||
When using `puppeteer-core`, remember to change the import:
|
||||
|
||||
```ts
|
||||
import puppeteer from 'puppeteer-core';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Puppeteer follows the latest
|
||||
[maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of
|
||||
Node.
|
||||
|
||||
Puppeteer will be familiar to people using other browser testing frameworks. You
|
||||
[launch](https://pptr.dev/api/puppeteer.puppeteernode.launch)/[connect](https://pptr.dev/api/puppeteer.puppeteernode.connect)
|
||||
a [browser](https://pptr.dev/api/puppeteer.browser),
|
||||
[create](https://pptr.dev/api/puppeteer.browser.newpage) some
|
||||
[pages](https://pptr.dev/api/puppeteer.page), and then manipulate them with
|
||||
[Puppeteer's API](https://pptr.dev/api).
|
||||
|
||||
For more in-depth usage, check our [guides](https://pptr.dev/category/guides)
|
||||
and [examples](https://github.com/puppeteer/puppeteer/tree/main/examples).
|
||||
|
||||
#### Example
|
||||
|
||||
The following example searches [developer.chrome.com](https://developer.chrome.com/) for blog posts with text "automate beyond recorder", click on the first result and print the full title of the blog post.
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import puppeteer from 'puppeteer';
|
||||
|
|
@ -175,87 +51,3 @@ import puppeteer from 'puppeteer';
|
|||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
### Default runtime settings
|
||||
|
||||
**1. Uses Headless mode**
|
||||
|
||||
By default Puppeteer launches Chrome in
|
||||
[the Headless mode](https://developer.chrome.com/articles/new-headless/).
|
||||
|
||||
```ts
|
||||
const browser = await puppeteer.launch();
|
||||
// Equivalent to
|
||||
const browser = await puppeteer.launch({headless: true});
|
||||
```
|
||||
|
||||
Before v22, Puppeteer launched the [old Headless mode](https://developer.chrome.com/articles/new-headless/) by default.
|
||||
The old headless mode is now known as
|
||||
[`chrome-headless-shell`](https://developer.chrome.com/blog/chrome-headless-shell)
|
||||
and ships as a separate binary. `chrome-headless-shell` does not match the
|
||||
behavior of the regular Chrome completely but it is currently more performant
|
||||
for automation tasks where the complete Chrome feature set is not needed. If the performance
|
||||
is more important for your use case, switch to `chrome-headless-shell` as following:
|
||||
|
||||
```ts
|
||||
const browser = await puppeteer.launch({headless: 'shell'});
|
||||
```
|
||||
|
||||
To launch a "headful" version of Chrome, set the
|
||||
[`headless`](https://pptr.dev/api/puppeteer.browserlaunchargumentoptions) to `false`
|
||||
option when launching a browser:
|
||||
|
||||
```ts
|
||||
const browser = await puppeteer.launch({headless: false});
|
||||
```
|
||||
|
||||
**2. Runs a bundled version of Chrome**
|
||||
|
||||
By default, Puppeteer downloads and uses a specific version of Chrome so its
|
||||
API is guaranteed to work out of the box. To use Puppeteer with a different
|
||||
version of Chrome or Chromium, pass in the executable's path when creating a
|
||||
`Browser` instance:
|
||||
|
||||
```ts
|
||||
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
|
||||
```
|
||||
|
||||
You can also use Puppeteer with Firefox. See
|
||||
[status of cross-browser support](https://pptr.dev/faq/#q-what-is-the-status-of-cross-browser-support) 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/+/refs/heads/main/docs/chromium_browser_vs_google_chrome.md)
|
||||
describes some differences for Linux users.
|
||||
|
||||
**3. Creates a fresh user profile**
|
||||
|
||||
Puppeteer creates its own browser user profile which it **cleans up on every
|
||||
run**.
|
||||
|
||||
#### Using Docker
|
||||
|
||||
See our [Docker guide](https://pptr.dev/guides/docker).
|
||||
|
||||
#### Using Chrome Extensions
|
||||
|
||||
See our [Chrome extensions guide](https://pptr.dev/guides/chrome-extensions).
|
||||
|
||||
## Resources
|
||||
|
||||
- [API Documentation](https://pptr.dev/api)
|
||||
- [Guides](https://pptr.dev/category/guides)
|
||||
- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples)
|
||||
- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer)
|
||||
|
||||
## Contributing
|
||||
|
||||
Check out our [contributing guide](https://pptr.dev/contributing) to get an
|
||||
overview of Puppeteer development.
|
||||
|
||||
## FAQ
|
||||
|
||||
Our [FAQ](https://pptr.dev/faq) has migrated to
|
||||
[our site](https://pptr.dev/faq).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
# Running the examples
|
||||
|
||||
Assuming you have a checkout of the Puppeteer repo and have run `npm i` (or `yarn`) to install the dependencies, and `npm run build` (or `yarn run build`) to build the project, the examples can be run from the root folder like so:
|
||||
Assuming you have a checkout of the Puppeteer repo and install the dependencies:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install
|
||||
```
|
||||
|
||||
Build the project:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
The examples can be run from the root folder like so:
|
||||
|
||||
```bash
|
||||
NODE_PATH=../ node examples/search.js
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ origin:
|
|||
description: Headless Chrome Node API
|
||||
license: Apache-2.0
|
||||
name: puppeteer
|
||||
release: puppeteer-v22.4.0
|
||||
url: ../puppeteer
|
||||
release: puppeteer-v22.6.5
|
||||
url: /Users/juliandescottes/Development/git/puppeteer
|
||||
schema: 1
|
||||
|
|
|
|||
1130
remote/test/puppeteer/package-lock.json
generated
1130
remote/test/puppeteer/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -142,13 +142,13 @@
|
|||
"@types/node": "20.8.4",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/sinon": "17.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.0",
|
||||
"@typescript-eslint/parser": "7.1.0",
|
||||
"esbuild": "0.20.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.6.0",
|
||||
"@typescript-eslint/parser": "7.6.0",
|
||||
"esbuild": "0.20.2",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-mocha": "10.3.0",
|
||||
"eslint-plugin-mocha": "10.4.2",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-rulesdir": "0.2.2",
|
||||
"eslint-plugin-tsdoc": "0.2.17",
|
||||
|
|
@ -156,18 +156,18 @@
|
|||
"eslint": "8.57.0",
|
||||
"execa": "8.0.1",
|
||||
"expect": "29.7.0",
|
||||
"gts": "5.2.0",
|
||||
"gts": "5.3.0",
|
||||
"hereby": "1.8.9",
|
||||
"license-checker": "25.0.1",
|
||||
"mocha": "10.3.0",
|
||||
"mocha": "10.4.0",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"prettier": "3.2.5",
|
||||
"semver": "7.6.0",
|
||||
"sinon": "17.0.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"spdx-satisfies": "5.0.1",
|
||||
"tsd": "0.30.7",
|
||||
"tsx": "4.7.1",
|
||||
"tsd": "0.31.0",
|
||||
"tsx": "4.7.2",
|
||||
"typescript": "5.3.3",
|
||||
"wireit": "0.14.4"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,32 @@
|
|||
# Changelog
|
||||
|
||||
## [2.2.2](https://github.com/puppeteer/puppeteer/compare/browsers-v2.2.1...browsers-v2.2.2) (2024-04-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove NetworkServiceInProcess2 set by default ([#12261](https://github.com/puppeteer/puppeteer/issues/12261)) ([ff4f70f](https://github.com/puppeteer/puppeteer/commit/ff4f70f4ae7ca8deb0becbec2e49b35322dba336)), closes [#12257](https://github.com/puppeteer/puppeteer/issues/12257)
|
||||
|
||||
## [2.2.1](https://github.com/puppeteer/puppeteer/compare/browsers-v2.2.0...browsers-v2.2.1) (2024-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not use fallback download URLs if custom baseUrl is provided ([#12206](https://github.com/puppeteer/puppeteer/issues/12206)) ([ab560bc](https://github.com/puppeteer/puppeteer/commit/ab560bcf6fee57cabde94d9d261d28ffc2112948))
|
||||
* only set up a single process event listener in launch ([#12200](https://github.com/puppeteer/puppeteer/issues/12200)) ([7bc5e0f](https://github.com/puppeteer/puppeteer/commit/7bc5e0fb2dc443765e2512e4dc15fb2bcc1cb4be))
|
||||
|
||||
## [2.2.0](https://github.com/puppeteer/puppeteer/compare/browsers-v2.1.0...browsers-v2.2.0) (2024-03-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow downloading Firefox channels other than nightly ([#12051](https://github.com/puppeteer/puppeteer/issues/12051)) ([e4cc2f9](https://github.com/puppeteer/puppeteer/commit/e4cc2f9ee944f2507a03cf8f5af99759c97ee2ec))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't keep connection alive ([#12096](https://github.com/puppeteer/puppeteer/issues/12096)) ([0a142bf](https://github.com/puppeteer/puppeteer/commit/0a142bf0aa8a6666d1ca230d05a1ece0e03ad7d4))
|
||||
|
||||
## [2.1.0](https://github.com/puppeteer/puppeteer/compare/browsers-v2.0.1...browsers-v2.1.0) (2024-02-21)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Use `npx` to run the CLI:
|
|||
npx @puppeteer/browsers --help
|
||||
```
|
||||
|
||||
CLI help will provide all documentation you need to use the CLI.
|
||||
Built-in per-command `help` will provide all documentation you need to use the CLI.
|
||||
|
||||
```bash
|
||||
npx @puppeteer/browsers --help # help for all commands
|
||||
|
|
@ -18,10 +18,28 @@ npx @puppeteer/browsers install --help # help for the install command
|
|||
npx @puppeteer/browsers launch --help # help for the launch command
|
||||
```
|
||||
|
||||
Some example to give an idea of what the CLI looks like (use the `--help` command for more examples):
|
||||
|
||||
```sh
|
||||
# Download the latest available Chrome for Testing binary corresponding to the Stable channel.
|
||||
npx @puppeteer/browsers install chrome@stable
|
||||
|
||||
# Download a specific Chrome for Testing version.
|
||||
npx @puppeteer/browsers install chrome@116.0.5793.0
|
||||
|
||||
# Download the latest Chrome for Testing version for the given milestone.
|
||||
npx @puppeteer/browsers install chrome@117
|
||||
|
||||
# Download the latest available ChromeDriver version corresponding to the Canary channel.
|
||||
npx @puppeteer/browsers install chromedriver@canary
|
||||
|
||||
# Download a specific ChromeDriver version.
|
||||
npx @puppeteer/browsers install chromedriver@116.0.5793.0
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
1. We support installing and running Firefox, Chrome and Chromium. The `latest`, `beta`, `dev`, `canary`, `stable` keywords are only supported for the install command. For the `launch` command you need to specify an exact build ID. The build ID is provided by the `install` command (see `npx @puppeteer/browsers install --help` for the format).
|
||||
2. Launching the system browsers is only possible for Chrome/Chromium.
|
||||
1. Launching the system browsers is only possible for Chrome/Chromium.
|
||||
|
||||
## API
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@puppeteer/browsers",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.2",
|
||||
"description": "Download and launch browsers",
|
||||
"scripts": {
|
||||
"build:docs": "wireit",
|
||||
|
|
|
|||
|
|
@ -222,7 +222,31 @@ export class CLI {
|
|||
);
|
||||
yargs.example(
|
||||
'$0 install firefox',
|
||||
'Install the latest available build of the Firefox browser.'
|
||||
'Install the latest nightly available build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@stable',
|
||||
'Install the latest stable build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@beta',
|
||||
'Install the latest beta build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@devedition',
|
||||
'Install the latest devedition build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@esr',
|
||||
'Install the latest ESR build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@nightly',
|
||||
'Install the latest nightly build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@stable_111.0.1',
|
||||
'Install a specific version of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox --platform mac',
|
||||
|
|
@ -395,7 +419,7 @@ export function makeProgressCallback(
|
|||
return (downloadedBytes: number, totalBytes: number) => {
|
||||
if (!progressBar) {
|
||||
progressBar = new ProgressBar(
|
||||
`Downloading ${browser} r${buildId} - ${toMegabytes(
|
||||
`Downloading ${browser} ${buildId} - ${toMegabytes(
|
||||
totalBytes
|
||||
)} [:bar] :percent :etas `,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -54,28 +54,36 @@ export const versionComparators = {
|
|||
export {Browser, BrowserPlatform, ChromeReleaseChannel};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
export async function resolveBuildId(
|
||||
async function resolveBuildIdForBrowserTag(
|
||||
browser: Browser,
|
||||
platform: BrowserPlatform,
|
||||
tag: string
|
||||
tag: BrowserTag
|
||||
): Promise<string> {
|
||||
switch (browser) {
|
||||
case Browser.FIREFOX:
|
||||
switch (tag as BrowserTag) {
|
||||
switch (tag) {
|
||||
case BrowserTag.LATEST:
|
||||
return await firefox.resolveBuildId('FIREFOX_NIGHTLY');
|
||||
return await firefox.resolveBuildId(firefox.FirefoxChannel.NIGHTLY);
|
||||
case BrowserTag.BETA:
|
||||
return await firefox.resolveBuildId(firefox.FirefoxChannel.BETA);
|
||||
case BrowserTag.NIGHTLY:
|
||||
return await firefox.resolveBuildId(firefox.FirefoxChannel.NIGHTLY);
|
||||
case BrowserTag.DEVEDITION:
|
||||
return await firefox.resolveBuildId(
|
||||
firefox.FirefoxChannel.DEVEDITION
|
||||
);
|
||||
case BrowserTag.STABLE:
|
||||
return await firefox.resolveBuildId(firefox.FirefoxChannel.STABLE);
|
||||
case BrowserTag.ESR:
|
||||
return await firefox.resolveBuildId(firefox.FirefoxChannel.ESR);
|
||||
case BrowserTag.CANARY:
|
||||
case BrowserTag.DEV:
|
||||
case BrowserTag.STABLE:
|
||||
throw new Error(
|
||||
`${tag} is not supported for ${browser}. Use 'latest' instead.`
|
||||
);
|
||||
throw new Error(`${tag.toUpperCase()} is not available for Firefox`);
|
||||
}
|
||||
case Browser.CHROME: {
|
||||
switch (tag as BrowserTag) {
|
||||
switch (tag) {
|
||||
case BrowserTag.LATEST:
|
||||
return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
|
||||
case BrowserTag.BETA:
|
||||
|
|
@ -86,13 +94,11 @@ export async function resolveBuildId(
|
|||
return await chrome.resolveBuildId(ChromeReleaseChannel.DEV);
|
||||
case BrowserTag.STABLE:
|
||||
return await chrome.resolveBuildId(ChromeReleaseChannel.STABLE);
|
||||
default:
|
||||
const result = await chrome.resolveBuildId(tag);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
case BrowserTag.NIGHTLY:
|
||||
case BrowserTag.DEVEDITION:
|
||||
case BrowserTag.ESR:
|
||||
throw new Error(`${tag.toUpperCase()} is not available for Chrome`);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
case Browser.CHROMEDRIVER: {
|
||||
switch (tag) {
|
||||
|
|
@ -105,13 +111,13 @@ export async function resolveBuildId(
|
|||
return await chromedriver.resolveBuildId(ChromeReleaseChannel.DEV);
|
||||
case BrowserTag.STABLE:
|
||||
return await chromedriver.resolveBuildId(ChromeReleaseChannel.STABLE);
|
||||
default:
|
||||
const result = await chromedriver.resolveBuildId(tag);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
case BrowserTag.NIGHTLY:
|
||||
case BrowserTag.DEVEDITION:
|
||||
case BrowserTag.ESR:
|
||||
throw new Error(
|
||||
`${tag.toUpperCase()} is not available for ChromeDriver`
|
||||
);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
case Browser.CHROMEHEADLESSSHELL: {
|
||||
switch (tag) {
|
||||
|
|
@ -132,29 +138,68 @@ export async function resolveBuildId(
|
|||
return await chromeHeadlessShell.resolveBuildId(
|
||||
ChromeReleaseChannel.STABLE
|
||||
);
|
||||
default:
|
||||
const result = await chromeHeadlessShell.resolveBuildId(tag);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
case BrowserTag.NIGHTLY:
|
||||
case BrowserTag.DEVEDITION:
|
||||
case BrowserTag.ESR:
|
||||
throw new Error(`${tag} is not available for chrome-headless-shell`);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
case Browser.CHROMIUM:
|
||||
switch (tag as BrowserTag) {
|
||||
switch (tag) {
|
||||
case BrowserTag.LATEST:
|
||||
return await chromium.resolveBuildId(platform);
|
||||
case BrowserTag.BETA:
|
||||
case BrowserTag.NIGHTLY:
|
||||
case BrowserTag.CANARY:
|
||||
case BrowserTag.DEV:
|
||||
case BrowserTag.DEVEDITION:
|
||||
case BrowserTag.BETA:
|
||||
case BrowserTag.STABLE:
|
||||
case BrowserTag.ESR:
|
||||
throw new Error(
|
||||
`${tag} is not supported for ${browser}. Use 'latest' instead.`
|
||||
`${tag} is not supported for Chromium. Use 'latest' instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
// We assume the tag is the buildId if it didn't match any keywords.
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function resolveBuildId(
|
||||
browser: Browser,
|
||||
platform: BrowserPlatform,
|
||||
tag: string
|
||||
): Promise<string> {
|
||||
const browserTag = tag as BrowserTag;
|
||||
if (Object.values(BrowserTag).includes(browserTag)) {
|
||||
return await resolveBuildIdForBrowserTag(browser, platform, browserTag);
|
||||
}
|
||||
|
||||
switch (browser) {
|
||||
case Browser.FIREFOX:
|
||||
return tag;
|
||||
case Browser.CHROME:
|
||||
const chromeResult = await chrome.resolveBuildId(tag);
|
||||
if (chromeResult) {
|
||||
return chromeResult;
|
||||
}
|
||||
return tag;
|
||||
case Browser.CHROMEDRIVER:
|
||||
const chromeDriverResult = await chromedriver.resolveBuildId(tag);
|
||||
if (chromeDriverResult) {
|
||||
return chromeDriverResult;
|
||||
}
|
||||
return tag;
|
||||
case Browser.CHROMEHEADLESSSHELL:
|
||||
const chromeHeadlessShellResult =
|
||||
await chromeHeadlessShell.resolveBuildId(tag);
|
||||
if (chromeHeadlessShellResult) {
|
||||
return chromeHeadlessShellResult;
|
||||
}
|
||||
return tag;
|
||||
case Browser.CHROMIUM:
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {getJSON} from '../httpUtil.js';
|
|||
|
||||
import {BrowserPlatform, type ProfileOptions} from './types.js';
|
||||
|
||||
function archive(platform: BrowserPlatform, buildId: string): string {
|
||||
function archiveNightly(platform: BrowserPlatform, buildId: string): string {
|
||||
switch (platform) {
|
||||
case BrowserPlatform.LINUX:
|
||||
return `firefox-${buildId}.en-US.${platform}-x86_64.tar.bz2`;
|
||||
|
|
@ -24,48 +24,146 @@ function archive(platform: BrowserPlatform, buildId: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
function archive(platform: BrowserPlatform, buildId: string): string {
|
||||
switch (platform) {
|
||||
case BrowserPlatform.LINUX:
|
||||
return `firefox-${buildId}.tar.bz2`;
|
||||
case BrowserPlatform.MAC_ARM:
|
||||
case BrowserPlatform.MAC:
|
||||
return `Firefox ${buildId}.dmg`;
|
||||
case BrowserPlatform.WIN32:
|
||||
case BrowserPlatform.WIN64:
|
||||
return `Firefox Setup ${buildId}.exe`;
|
||||
}
|
||||
}
|
||||
|
||||
function platformName(platform: BrowserPlatform): string {
|
||||
switch (platform) {
|
||||
case BrowserPlatform.LINUX:
|
||||
return `linux-x86_64`;
|
||||
case BrowserPlatform.MAC_ARM:
|
||||
case BrowserPlatform.MAC:
|
||||
return `mac`;
|
||||
case BrowserPlatform.WIN32:
|
||||
case BrowserPlatform.WIN64:
|
||||
return platform;
|
||||
}
|
||||
}
|
||||
|
||||
function parseBuildId(buildId: string): [FirefoxChannel, string] {
|
||||
for (const value of Object.values(FirefoxChannel)) {
|
||||
if (buildId.startsWith(value + '_')) {
|
||||
buildId = buildId.substring(value.length + 1);
|
||||
return [value, buildId];
|
||||
}
|
||||
}
|
||||
// Older versions do not have channel as the prefix.«
|
||||
return [FirefoxChannel.NIGHTLY, buildId];
|
||||
}
|
||||
|
||||
export function resolveDownloadUrl(
|
||||
platform: BrowserPlatform,
|
||||
buildId: string,
|
||||
baseUrl = 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central'
|
||||
baseUrl?: string
|
||||
): string {
|
||||
return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
|
||||
const [channel, resolvedBuildId] = parseBuildId(buildId);
|
||||
switch (channel) {
|
||||
case FirefoxChannel.NIGHTLY:
|
||||
baseUrl ??=
|
||||
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central';
|
||||
break;
|
||||
case FirefoxChannel.DEVEDITION:
|
||||
baseUrl ??= 'https://archive.mozilla.org/pub/devedition/releases';
|
||||
break;
|
||||
case FirefoxChannel.BETA:
|
||||
case FirefoxChannel.STABLE:
|
||||
case FirefoxChannel.ESR:
|
||||
baseUrl ??= 'https://archive.mozilla.org/pub/firefox/releases';
|
||||
break;
|
||||
}
|
||||
switch (channel) {
|
||||
case FirefoxChannel.NIGHTLY:
|
||||
return `${baseUrl}/${resolveDownloadPath(platform, resolvedBuildId).join('/')}`;
|
||||
case FirefoxChannel.DEVEDITION:
|
||||
case FirefoxChannel.BETA:
|
||||
case FirefoxChannel.STABLE:
|
||||
case FirefoxChannel.ESR:
|
||||
return `${baseUrl}/${resolvedBuildId}/${platformName(platform)}/en-US/${archive(platform, resolvedBuildId)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDownloadPath(
|
||||
platform: BrowserPlatform,
|
||||
buildId: string
|
||||
): string[] {
|
||||
return [archive(platform, buildId)];
|
||||
return [archiveNightly(platform, buildId)];
|
||||
}
|
||||
|
||||
export function relativeExecutablePath(
|
||||
platform: BrowserPlatform,
|
||||
_buildId: string
|
||||
buildId: string
|
||||
): string {
|
||||
switch (platform) {
|
||||
case BrowserPlatform.MAC_ARM:
|
||||
case BrowserPlatform.MAC:
|
||||
return path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox');
|
||||
case BrowserPlatform.LINUX:
|
||||
return path.join('firefox', 'firefox');
|
||||
case BrowserPlatform.WIN32:
|
||||
case BrowserPlatform.WIN64:
|
||||
return path.join('firefox', 'firefox.exe');
|
||||
const [channel] = parseBuildId(buildId);
|
||||
switch (channel) {
|
||||
case FirefoxChannel.NIGHTLY:
|
||||
switch (platform) {
|
||||
case BrowserPlatform.MAC_ARM:
|
||||
case BrowserPlatform.MAC:
|
||||
return path.join(
|
||||
'Firefox Nightly.app',
|
||||
'Contents',
|
||||
'MacOS',
|
||||
'firefox'
|
||||
);
|
||||
case BrowserPlatform.LINUX:
|
||||
return path.join('firefox', 'firefox');
|
||||
case BrowserPlatform.WIN32:
|
||||
case BrowserPlatform.WIN64:
|
||||
return path.join('firefox', 'firefox.exe');
|
||||
}
|
||||
case FirefoxChannel.BETA:
|
||||
case FirefoxChannel.DEVEDITION:
|
||||
case FirefoxChannel.ESR:
|
||||
case FirefoxChannel.STABLE:
|
||||
switch (platform) {
|
||||
case BrowserPlatform.MAC_ARM:
|
||||
case BrowserPlatform.MAC:
|
||||
return path.join('Firefox.app', 'Contents', 'MacOS', 'firefox');
|
||||
case BrowserPlatform.LINUX:
|
||||
return path.join('firefox', 'firefox');
|
||||
case BrowserPlatform.WIN32:
|
||||
case BrowserPlatform.WIN64:
|
||||
return path.join('core', 'firefox.exe');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum FirefoxChannel {
|
||||
STABLE = 'stable',
|
||||
ESR = 'esr',
|
||||
DEVEDITION = 'devedition',
|
||||
BETA = 'beta',
|
||||
NIGHTLY = 'nightly',
|
||||
}
|
||||
|
||||
export async function resolveBuildId(
|
||||
channel: 'FIREFOX_NIGHTLY' = 'FIREFOX_NIGHTLY'
|
||||
channel: FirefoxChannel = FirefoxChannel.NIGHTLY
|
||||
): Promise<string> {
|
||||
const channelToVersionKey = {
|
||||
[FirefoxChannel.ESR]: 'FIREFOX_ESR',
|
||||
[FirefoxChannel.STABLE]: 'LATEST_FIREFOX_VERSION',
|
||||
[FirefoxChannel.DEVEDITION]: 'FIREFOX_DEVEDITION',
|
||||
[FirefoxChannel.BETA]: 'FIREFOX_DEVEDITION',
|
||||
[FirefoxChannel.NIGHTLY]: 'FIREFOX_NIGHTLY',
|
||||
};
|
||||
const versions = (await getJSON(
|
||||
new URL('https://product-details.mozilla.org/1.0/firefox_versions.json')
|
||||
)) as Record<string, string>;
|
||||
const version = versions[channel];
|
||||
const version = versions[channelToVersionKey[channel]];
|
||||
if (!version) {
|
||||
throw new Error(`Channel ${channel} is not found.`);
|
||||
}
|
||||
return version;
|
||||
return channel + '_' + version;
|
||||
}
|
||||
|
||||
export async function createProfile(options: ProfileOptions): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -36,9 +36,12 @@ export enum BrowserPlatform {
|
|||
*/
|
||||
export enum BrowserTag {
|
||||
CANARY = 'canary',
|
||||
NIGHTLY = 'nightly',
|
||||
BETA = 'beta',
|
||||
DEV = 'dev',
|
||||
DEVEDITION = 'devedition',
|
||||
STABLE = 'stable',
|
||||
ESR = 'esr',
|
||||
LATEST = 'latest',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {exec as execChildProcess} from 'child_process';
|
||||
import {exec as execChildProcess, spawnSync} from 'child_process';
|
||||
import {createReadStream} from 'fs';
|
||||
import {mkdir, readdir} from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
|
@ -30,6 +30,18 @@ export async function unpackArchive(
|
|||
} else if (archivePath.endsWith('.dmg')) {
|
||||
await mkdir(folderPath);
|
||||
await installDMG(archivePath, folderPath);
|
||||
} else if (archivePath.endsWith('.exe')) {
|
||||
// Firefox on Windows.
|
||||
const result = spawnSync(archivePath, [`/ExtractDir=${folderPath}`], {
|
||||
env: {
|
||||
__compat_layer: 'RunAsInvoker',
|
||||
},
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to extract ${archivePath} to ${folderPath}: ${result.output}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported archive format: ${archivePath}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ export function httpRequest(
|
|||
res.headers.location
|
||||
) {
|
||||
httpRequest(new URL(res.headers.location), method, response);
|
||||
// consume response data to free up memory
|
||||
// And prevents the connection from being kept alive
|
||||
res.resume();
|
||||
} else {
|
||||
response(res);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,11 @@ export interface InstallOptions {
|
|||
* @defaultValue `true`
|
||||
*/
|
||||
unpack?: boolean;
|
||||
/**
|
||||
* @internal
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
forceFallbackForTesting?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -125,6 +130,10 @@ export async function install(
|
|||
try {
|
||||
return await installUrl(url, options);
|
||||
} catch (err) {
|
||||
// If custom baseUrl is provided, do not fall back to CfT dashboard.
|
||||
if (options.baseUrl && !options.forceFallbackForTesting) {
|
||||
throw err;
|
||||
}
|
||||
debugInstall(`Error downloading from ${url}.`);
|
||||
switch (options.browser) {
|
||||
case Browser.CHROME:
|
||||
|
|
|
|||
|
|
@ -135,6 +135,59 @@ export const CDP_WEBSOCKET_ENDPOINT_REGEX =
|
|||
export const WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX =
|
||||
/^WebDriver BiDi listening on (ws:\/\/.*)$/;
|
||||
|
||||
type EventHandler = (...args: any[]) => void;
|
||||
const processListeners = new Map<string, EventHandler[]>();
|
||||
const dispatchers = {
|
||||
exit: (...args: any[]) => {
|
||||
processListeners.get('exit')?.forEach(handler => {
|
||||
return handler(...args);
|
||||
});
|
||||
},
|
||||
SIGINT: (...args: any[]) => {
|
||||
processListeners.get('SIGINT')?.forEach(handler => {
|
||||
return handler(...args);
|
||||
});
|
||||
},
|
||||
SIGHUP: (...args: any[]) => {
|
||||
processListeners.get('SIGHUP')?.forEach(handler => {
|
||||
return handler(...args);
|
||||
});
|
||||
},
|
||||
SIGTERM: (...args: any[]) => {
|
||||
processListeners.get('SIGTERM')?.forEach(handler => {
|
||||
return handler(...args);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function subscribeToProcessEvent(
|
||||
event: 'exit' | 'SIGINT' | 'SIGHUP' | 'SIGTERM',
|
||||
handler: EventHandler
|
||||
): void {
|
||||
const listeners = processListeners.get(event) || [];
|
||||
if (listeners.length === 0) {
|
||||
process.on(event, dispatchers[event]);
|
||||
}
|
||||
listeners.push(handler);
|
||||
processListeners.set(event, listeners);
|
||||
}
|
||||
|
||||
function unsubscribeFromProcessEvent(
|
||||
event: 'exit' | 'SIGINT' | 'SIGHUP' | 'SIGTERM',
|
||||
handler: EventHandler
|
||||
): void {
|
||||
const listeners = processListeners.get(event) || [];
|
||||
const existingListenerIdx = listeners.indexOf(handler);
|
||||
if (existingListenerIdx === -1) {
|
||||
return;
|
||||
}
|
||||
listeners.splice(existingListenerIdx, 1);
|
||||
processListeners.set(event, listeners);
|
||||
if (listeners.length === 0) {
|
||||
process.off(event, dispatchers[event]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
|
@ -201,15 +254,15 @@ export class Process {
|
|||
this.#browserProcess.stderr?.pipe(process.stderr);
|
||||
this.#browserProcess.stdout?.pipe(process.stdout);
|
||||
}
|
||||
process.on('exit', this.#onDriverProcessExit);
|
||||
subscribeToProcessEvent('exit', this.#onDriverProcessExit);
|
||||
if (opts.handleSIGINT) {
|
||||
process.on('SIGINT', this.#onDriverProcessSignal);
|
||||
subscribeToProcessEvent('SIGINT', this.#onDriverProcessSignal);
|
||||
}
|
||||
if (opts.handleSIGTERM) {
|
||||
process.on('SIGTERM', this.#onDriverProcessSignal);
|
||||
subscribeToProcessEvent('SIGTERM', this.#onDriverProcessSignal);
|
||||
}
|
||||
if (opts.handleSIGHUP) {
|
||||
process.on('SIGHUP', this.#onDriverProcessSignal);
|
||||
subscribeToProcessEvent('SIGHUP', this.#onDriverProcessSignal);
|
||||
}
|
||||
if (opts.onExit) {
|
||||
this.#onExitHook = opts.onExit;
|
||||
|
|
@ -262,10 +315,10 @@ export class Process {
|
|||
}
|
||||
|
||||
#clearListeners(): void {
|
||||
process.off('exit', this.#onDriverProcessExit);
|
||||
process.off('SIGINT', this.#onDriverProcessSignal);
|
||||
process.off('SIGTERM', this.#onDriverProcessSignal);
|
||||
process.off('SIGHUP', this.#onDriverProcessSignal);
|
||||
unsubscribeFromProcessEvent('exit', this.#onDriverProcessExit);
|
||||
unsubscribeFromProcessEvent('SIGINT', this.#onDriverProcessSignal);
|
||||
unsubscribeFromProcessEvent('SIGTERM', this.#onDriverProcessSignal);
|
||||
unsubscribeFromProcessEvent('SIGHUP', this.#onDriverProcessSignal);
|
||||
}
|
||||
|
||||
#onDriverProcessExit = (_code: number) => {
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ describe('Chrome install', () => {
|
|||
});
|
||||
|
||||
it('falls back to the chrome-for-testing dashboard URLs if URL is not available', async function () {
|
||||
this.timeout(60000);
|
||||
const expectedOutputPath = path.join(
|
||||
tmpDir,
|
||||
'chrome',
|
||||
|
|
@ -150,6 +151,7 @@ describe('Chrome install', () => {
|
|||
platform: BrowserPlatform.LINUX,
|
||||
buildId: testChromeBuildId,
|
||||
baseUrl: 'https://127.0.0.1',
|
||||
forceFallbackForTesting: true,
|
||||
});
|
||||
assert.strictEqual(fs.existsSync(expectedOutputPath), true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ describe('Chrome', () => {
|
|||
'--disable-renderer-backgrounding',
|
||||
'--disable-sync',
|
||||
'--enable-automation',
|
||||
'--enable-features=NetworkServiceInProcess2',
|
||||
'--export-tagged-pdf',
|
||||
'--force-color-profile=srgb',
|
||||
'--headless=new',
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ describe('Chromium', () => {
|
|||
'--disable-renderer-backgrounding',
|
||||
'--disable-sync',
|
||||
'--enable-automation',
|
||||
'--enable-features=NetworkServiceInProcess2',
|
||||
'--export-tagged-pdf',
|
||||
'--force-color-profile=srgb',
|
||||
'--headless=new',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from '../../../lib/cjs/browser-data/firefox.js';
|
||||
|
||||
describe('Firefox', () => {
|
||||
it('should resolve download URLs', () => {
|
||||
it('should resolve download URLs for Nightly', () => {
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.LINUX, '111.0a1'),
|
||||
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.linux-x86_64.tar.bz2'
|
||||
|
|
@ -41,6 +41,75 @@ describe('Firefox', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should resolve download URLs for beta', () => {
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.LINUX, 'beta_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/linux-x86_64/en-US/firefox-115.0b8.tar.bz2'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.MAC, 'beta_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/mac/en-US/Firefox 115.0b8.dmg'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.MAC_ARM, 'beta_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/mac/en-US/Firefox 115.0b8.dmg'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.WIN32, 'beta_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/win32/en-US/Firefox Setup 115.0b8.exe'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.WIN64, 'beta_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/win64/en-US/Firefox Setup 115.0b8.exe'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve download URLs for stable', () => {
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.LINUX, 'stable_111.0.1'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/linux-x86_64/en-US/firefox-111.0.1.tar.bz2'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.MAC, 'stable_111.0.1'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/mac/en-US/Firefox 111.0.1.dmg'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.MAC_ARM, 'stable_111.0.1'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/mac/en-US/Firefox 111.0.1.dmg'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.WIN32, 'stable_111.0.1'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/win32/en-US/Firefox Setup 111.0.1.exe'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.WIN64, 'stable_111.0.1'),
|
||||
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/win64/en-US/Firefox Setup 111.0.1.exe'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve download URLs for devedition', () => {
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.LINUX, 'devedition_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/linux-x86_64/en-US/firefox-115.0b8.tar.bz2'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.MAC, 'devedition_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/mac/en-US/Firefox 115.0b8.dmg'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.MAC_ARM, 'devedition_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/mac/en-US/Firefox 115.0b8.dmg'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.WIN32, 'devedition_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/win32/en-US/Firefox Setup 115.0b8.exe'
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveDownloadUrl(BrowserPlatform.WIN64, 'devedition_115.0b8'),
|
||||
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/win64/en-US/Firefox Setup 115.0b8.exe'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve executable paths', () => {
|
||||
assert.strictEqual(
|
||||
relativeExecutablePath(BrowserPlatform.LINUX, '111.0a1'),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@
|
|||
|
||||
export const testChromeBuildId = '121.0.6167.85';
|
||||
export const testChromiumBuildId = '1083080';
|
||||
export const testFirefoxBuildId = '125.0a1';
|
||||
export const testFirefoxBuildId = '126.0a1';
|
||||
export const testChromeDriverBuildId = '121.0.6167.85';
|
||||
export const testChromeHeadlessShellBuildId = '121.0.6167.85';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Ignore File that will be copied to Angular
|
||||
/files/
|
||||
|
||||
# Ignore sandbox enviroment
|
||||
# Ignore sandbox environment
|
||||
./sandbox/
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ node tools/smoke.mjs
|
|||
|
||||
The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit:
|
||||
|
||||
```bash
|
||||
```bash npm2yarn
|
||||
npm run test
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,104 @@ All notable changes to this project will be documented in this file. See [standa
|
|||
* dependencies
|
||||
* @puppeteer/browsers bumped from 1.5.1 to 1.6.0
|
||||
|
||||
## [22.6.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.6.4...puppeteer-core-v22.6.5) (2024-04-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove NetworkServiceInProcess2 set by default ([#12261](https://github.com/puppeteer/puppeteer/issues/12261)) ([ff4f70f](https://github.com/puppeteer/puppeteer/commit/ff4f70f4ae7ca8deb0becbec2e49b35322dba336)), closes [#12257](https://github.com/puppeteer/puppeteer/issues/12257)
|
||||
* use setImmediate to reduce flakiness when processing events ([#12264](https://github.com/puppeteer/puppeteer/issues/12264)) ([73403b3](https://github.com/puppeteer/puppeteer/commit/73403b323ec0dd8a08c164cb2c07751451215788))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* @puppeteer/browsers bumped from 2.2.1 to 2.2.2
|
||||
|
||||
## [22.6.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.6.3...puppeteer-core-v22.6.4) (2024-04-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **a11y:** query only unignored nodes ([#12224](https://github.com/puppeteer/puppeteer/issues/12224)) ([e20cd64](https://github.com/puppeteer/puppeteer/commit/e20cd64fff374c4113777912c193f4a5d7d04297))
|
||||
* retain stale main frame for longer ([#12225](https://github.com/puppeteer/puppeteer/issues/12225)) ([aa5b182](https://github.com/puppeteer/puppeteer/commit/aa5b1824a5c82005fcfc05b002facfbbb9810f8f))
|
||||
* roll to Chrome 123.0.6312.122 (r1262506) ([#12248](https://github.com/puppeteer/puppeteer/issues/12248)) ([50b6659](https://github.com/puppeteer/puppeteer/commit/50b66591e70a7b6907d86594d7dacee6e76afc2d))
|
||||
* **webdriver:** suppress error for error code errors ([5f7254c](https://github.com/puppeteer/puppeteer/commit/5f7254c41c7c1bda82477488f10254d204373d54))
|
||||
|
||||
## [22.6.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.6.2...puppeteer-core-v22.6.3) (2024-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* check if executablePath exists ([#12201](https://github.com/puppeteer/puppeteer/issues/12201)) ([4ec0280](https://github.com/puppeteer/puppeteer/commit/4ec02800801d441238d6160a933f88f98c5f7165))
|
||||
* roll to Chrome 123.0.6312.105 (r1262506) ([#12209](https://github.com/puppeteer/puppeteer/issues/12209)) ([ee31272](https://github.com/puppeteer/puppeteer/commit/ee312721152cce61a9e9cb2b78b71b40c4fa9e64))
|
||||
* wait for fonts before pdf printing ([#12175](https://github.com/puppeteer/puppeteer/issues/12175)) ([59bffce](https://github.com/puppeteer/puppeteer/commit/59bffce9720b4d5e5204b26b335735e0a5ca9cc1))
|
||||
* **webdriver:** request redirect chain ([#12168](https://github.com/puppeteer/puppeteer/issues/12168)) ([d345055](https://github.com/puppeteer/puppeteer/commit/d345055af3c63effbdfb2751274b9d7137b8a308))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* @puppeteer/browsers bumped from 2.2.0 to 2.2.1
|
||||
|
||||
## [22.6.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.6.1...puppeteer-core-v22.6.2) (2024-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* roll to Chrome 123.0.6312.86 (r1262506) ([#12156](https://github.com/puppeteer/puppeteer/issues/12156)) ([29637f2](https://github.com/puppeteer/puppeteer/commit/29637f2b8f2dc1d684dbbb62d1a75857e016be33))
|
||||
|
||||
## [22.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.6.0...puppeteer-core-v22.6.1) (2024-03-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* apply timeout to waiting for a response ([#12142](https://github.com/puppeteer/puppeteer/issues/12142)) ([ac1767d](https://github.com/puppeteer/puppeteer/commit/ac1767da0b4214ced548a62dd737e2863f92c715))
|
||||
* reload should not resolve early on fragment navigations ([#12119](https://github.com/puppeteer/puppeteer/issues/12119)) ([d476031](https://github.com/puppeteer/puppeteer/commit/d4760317c9bd359c9ecdb5f36231449dae16a8d2))
|
||||
* support clip in ElementHandle.screenshot ([#12115](https://github.com/puppeteer/puppeteer/issues/12115)) ([b096ffa](https://github.com/puppeteer/puppeteer/commit/b096ffaa0359078bd5748b53b67e87c9453c7196))
|
||||
|
||||
## [22.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.5.0...puppeteer-core-v22.6.0) (2024-03-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* roll to Chrome 123.0.6312.58 (r1262506) ([#12110](https://github.com/puppeteer/puppeteer/issues/12110)) ([6f5b3bc](https://github.com/puppeteer/puppeteer/commit/6f5b3bc9b88c6d3204dda396f8963591ea6eb883))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **webdriver:** emit RequestServedFromCache for requests ([#12104](https://github.com/puppeteer/puppeteer/issues/12104)) ([6ba6bef](https://github.com/puppeteer/puppeteer/commit/6ba6bef1b99742543942cef2f6c840bd543f5dee))
|
||||
|
||||
## [22.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.4.1...puppeteer-core-v22.5.0) (2024-03-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* deprecate `Frame.prototype.name` ([#12084](https://github.com/puppeteer/puppeteer/issues/12084)) ([0203b45](https://github.com/puppeteer/puppeteer/commit/0203b4533dfec503f9ce7fcd07c3493021a9cecb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix keyboard.sendCharacter ([#12088](https://github.com/puppeteer/puppeteer/issues/12088)) ([2637622](https://github.com/puppeteer/puppeteer/commit/26376224d557ce30c911f670c5e7625dd1a1df72))
|
||||
* roll to Chrome 122.0.6261.128 (r1250580) ([#12078](https://github.com/puppeteer/puppeteer/issues/12078)) ([ef7a9ea](https://github.com/puppeteer/puppeteer/commit/ef7a9eac16dcb466b220bcb0bc06a1eac3492354))
|
||||
* **webdriver:** emit CDP events ([#12058](https://github.com/puppeteer/puppeteer/issues/12058)) ([9afe424](https://github.com/puppeteer/puppeteer/commit/9afe4246bb58c30a13215a254f9326935b24ece3))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* @puppeteer/browsers bumped from 2.1.0 to 2.2.0
|
||||
|
||||
## [22.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.4.0...puppeteer-core-v22.4.1) (2024-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* roll to Chrome 122.0.6261.111 (r1250580) ([#12055](https://github.com/puppeteer/puppeteer/issues/12055)) ([9b31bca](https://github.com/puppeteer/puppeteer/commit/9b31bca01adeb2ce04c97d9fcb3c6b6461469f07))
|
||||
|
||||
## [22.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.3.0...puppeteer-core-v22.4.0) (2024-03-05)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -110,8 +110,10 @@ export const buildTask = task({
|
|||
bundle: true,
|
||||
allowOverwrite: true,
|
||||
format,
|
||||
target: 'node16',
|
||||
minify: true,
|
||||
target: 'node18',
|
||||
// Do not minify for readability and leave minification to
|
||||
// consumers.
|
||||
minify: false,
|
||||
legalComments: 'inline',
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "puppeteer-core",
|
||||
"version": "22.4.0",
|
||||
"version": "22.6.5",
|
||||
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
||||
"keywords": [
|
||||
"puppeteer",
|
||||
|
|
@ -119,11 +119,10 @@
|
|||
"author": "The Chromium Authors",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.1.0",
|
||||
"chromium-bidi": "0.5.12",
|
||||
"cross-fetch": "4.0.0",
|
||||
"@puppeteer/browsers": "2.2.2",
|
||||
"chromium-bidi": "0.5.17",
|
||||
"debug": "4.3.4",
|
||||
"devtools-protocol": "0.0.1249869",
|
||||
"devtools-protocol": "0.0.1262051",
|
||||
"ws": "8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export interface DebugInfo {
|
|||
* - connected to via {@link Puppeteer.connect} or
|
||||
* - launched by {@link PuppeteerNode.launch}.
|
||||
*
|
||||
* {@link Browser} {@link EventEmitter | emits} various events which are
|
||||
* {@link Browser} {@link EventEmitter.emit | emits} various events which are
|
||||
* documented in the {@link BrowserEvent} enum.
|
||||
*
|
||||
* @example Using a {@link Browser} to create a {@link Page}:
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ export abstract class BrowserContext extends EventEmitter<BrowserContextEvents>
|
|||
*
|
||||
* @deprecated In Chrome, the
|
||||
* {@link Browser.defaultBrowserContext | default browser context} can also be
|
||||
* "icognito" if configured via the arguments and in such cases this getter
|
||||
* "incognito" if configured via the arguments and in such cases this getter
|
||||
* returns wrong results (see
|
||||
* https://github.com/puppeteer/puppeteer/issues/8836). Also, the term
|
||||
* "incognito" is not applicable to other browsers. To migrate, check the
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export interface CommandOptions {
|
|||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* const client = await page.target().createCDPSession();
|
||||
* const client = await page.createCDPSession();
|
||||
* await client.send('Animation.enable');
|
||||
* client.on('Animation.animationCreated', () =>
|
||||
* console.log('Animation created!')
|
||||
|
|
|
|||
|
|
@ -623,7 +623,7 @@ export abstract class ElementHandle<
|
|||
|
||||
/**
|
||||
* This method scrolls element into view if needed, and then
|
||||
* uses {@link Page} to hover over the center of the element.
|
||||
* uses {@link Page.mouse} to hover over the center of the element.
|
||||
* If the element is detached from DOM, the method throws an error.
|
||||
*/
|
||||
@throwIfDisposed()
|
||||
|
|
@ -636,7 +636,7 @@ export abstract class ElementHandle<
|
|||
|
||||
/**
|
||||
* This method scrolls element into view if needed, and then
|
||||
* uses {@link Page | Page.mouse} to click in the center of the element.
|
||||
* uses {@link Page.mouse} to click in the center of the element.
|
||||
* If the element is detached from DOM, the method throws an error.
|
||||
*/
|
||||
@throwIfDisposed()
|
||||
|
|
@ -1236,9 +1236,9 @@ export abstract class ElementHandle<
|
|||
this: ElementHandle<Element>,
|
||||
options: Readonly<ElementScreenshotOptions> = {}
|
||||
): Promise<string | Buffer> {
|
||||
const {scrollIntoView = true} = options;
|
||||
const {scrollIntoView = true, clip} = options;
|
||||
|
||||
let clip = await this.#nonEmptyVisibleBoundingBox();
|
||||
let elementClip = await this.#nonEmptyVisibleBoundingBox();
|
||||
|
||||
const page = this.frame.page();
|
||||
|
||||
|
|
@ -1247,7 +1247,7 @@ export abstract class ElementHandle<
|
|||
await this.scrollIntoViewIfNeeded();
|
||||
|
||||
// We measure again just in case.
|
||||
clip = await this.#nonEmptyVisibleBoundingBox();
|
||||
elementClip = await this.#nonEmptyVisibleBoundingBox();
|
||||
}
|
||||
|
||||
const [pageLeft, pageTop] = await this.evaluate(() => {
|
||||
|
|
@ -1259,10 +1259,16 @@ export abstract class ElementHandle<
|
|||
window.visualViewport.pageTop,
|
||||
] as const;
|
||||
});
|
||||
clip.x += pageLeft;
|
||||
clip.y += pageTop;
|
||||
elementClip.x += pageLeft;
|
||||
elementClip.y += pageTop;
|
||||
if (clip) {
|
||||
elementClip.x += clip.x;
|
||||
elementClip.y += clip.y;
|
||||
elementClip.height = clip.height;
|
||||
elementClip.width = clip.width;
|
||||
}
|
||||
|
||||
return await page.screenshot({...options, clip});
|
||||
return await page.screenshot({...options, clip: elementClip});
|
||||
}
|
||||
|
||||
async #nonEmptyVisibleBoundingBox() {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
|
|||
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
|
||||
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
|
||||
import {transposeIterableHandle} from '../common/HandleIterator.js';
|
||||
import {LazyArg} from '../common/LazyArg.js';
|
||||
import type {
|
||||
Awaitable,
|
||||
EvaluateFunc,
|
||||
|
|
@ -63,6 +62,10 @@ export interface WaitForOptions {
|
|||
* @defaultValue `'load'`
|
||||
*/
|
||||
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ignoreSameDocumentNavigation?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -405,7 +408,7 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
|||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @returns The frame element associated with this frame (if any).
|
||||
*/
|
||||
@throwIfDetached
|
||||
async frameElement(): Promise<HandleFor<HTMLIFrameElement> | null> {
|
||||
|
|
@ -447,7 +450,7 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Behaves identically to {@link Page.evaluate} except it's run within the
|
||||
* Behaves identically to {@link Page.evaluate} except it's run within
|
||||
* the context of this frame.
|
||||
*
|
||||
* @see {@link Page.evaluate} for details.
|
||||
|
|
@ -760,6 +763,13 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
|||
* @remarks
|
||||
* This value is calculated once when the frame is created, and will not
|
||||
* update if the attribute is changed later.
|
||||
*
|
||||
* @deprecated Use
|
||||
*
|
||||
* ```ts
|
||||
* const element = await frame.frameElement();
|
||||
* const nameOrId = await element.evaluate(frame => frame.name ?? frame.id);
|
||||
* ```
|
||||
*/
|
||||
name(): string {
|
||||
return this._name || '';
|
||||
|
|
@ -830,42 +840,37 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
|||
|
||||
return await this.mainRealm().transferHandle(
|
||||
await this.isolatedRealm().evaluateHandle(
|
||||
async ({Deferred}, {url, id, type, content}) => {
|
||||
const deferred = Deferred.create<void>();
|
||||
const script = document.createElement('script');
|
||||
script.type = type;
|
||||
script.text = content;
|
||||
if (url) {
|
||||
script.src = url;
|
||||
script.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
return deferred.resolve();
|
||||
},
|
||||
{once: true}
|
||||
);
|
||||
async ({url, id, type, content}) => {
|
||||
return await new Promise<HTMLScriptElement>((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.type = type;
|
||||
script.text = content;
|
||||
script.addEventListener(
|
||||
'error',
|
||||
event => {
|
||||
deferred.reject(
|
||||
new Error(event.message ?? 'Could not load script')
|
||||
);
|
||||
reject(new Error(event.message ?? 'Could not load script'));
|
||||
},
|
||||
{once: true}
|
||||
);
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
if (id) {
|
||||
script.id = id;
|
||||
}
|
||||
document.head.appendChild(script);
|
||||
await deferred.valueOrThrow();
|
||||
return script;
|
||||
if (id) {
|
||||
script.id = id;
|
||||
}
|
||||
if (url) {
|
||||
script.src = url;
|
||||
script.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
resolve(script);
|
||||
},
|
||||
{once: true}
|
||||
);
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
document.head.appendChild(script);
|
||||
resolve(script);
|
||||
}
|
||||
});
|
||||
},
|
||||
LazyArg.create(context => {
|
||||
return context.puppeteerUtil;
|
||||
}),
|
||||
{...options, type, content}
|
||||
)
|
||||
);
|
||||
|
|
@ -915,46 +920,42 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
|||
}
|
||||
|
||||
return await this.mainRealm().transferHandle(
|
||||
await this.isolatedRealm().evaluateHandle(
|
||||
async ({Deferred}, {url, content}) => {
|
||||
const deferred = Deferred.create<void>();
|
||||
let element: HTMLStyleElement | HTMLLinkElement;
|
||||
if (!url) {
|
||||
element = document.createElement('style');
|
||||
element.appendChild(document.createTextNode(content!));
|
||||
} else {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
element = link;
|
||||
await this.isolatedRealm().evaluateHandle(async ({url, content}) => {
|
||||
return await new Promise<HTMLStyleElement | HTMLLinkElement>(
|
||||
(resolve, reject) => {
|
||||
let element: HTMLStyleElement | HTMLLinkElement;
|
||||
if (!url) {
|
||||
element = document.createElement('style');
|
||||
element.appendChild(document.createTextNode(content!));
|
||||
} else {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
element = link;
|
||||
}
|
||||
element.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
resolve(element);
|
||||
},
|
||||
{once: true}
|
||||
);
|
||||
element.addEventListener(
|
||||
'error',
|
||||
event => {
|
||||
reject(
|
||||
new Error(
|
||||
(event as ErrorEvent).message ?? 'Could not load style'
|
||||
)
|
||||
);
|
||||
},
|
||||
{once: true}
|
||||
);
|
||||
document.head.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
element.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
deferred.resolve();
|
||||
},
|
||||
{once: true}
|
||||
);
|
||||
element.addEventListener(
|
||||
'error',
|
||||
event => {
|
||||
deferred.reject(
|
||||
new Error(
|
||||
(event as ErrorEvent).message ?? 'Could not load style'
|
||||
)
|
||||
);
|
||||
},
|
||||
{once: true}
|
||||
);
|
||||
document.head.appendChild(element);
|
||||
await deferred.valueOrThrow();
|
||||
return element;
|
||||
},
|
||||
LazyArg.create(context => {
|
||||
return context.puppeteerUtil;
|
||||
}),
|
||||
options
|
||||
)
|
||||
);
|
||||
}, options)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
*/
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {ProtocolError} from '../common/Errors.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
import type {CDPSession} from './CDPSession.js';
|
||||
import type {Frame} from './Frame.js';
|
||||
import type {HTTPResponse} from './HTTPResponse.js';
|
||||
|
|
@ -117,6 +121,29 @@ export abstract class HTTPRequest {
|
|||
*/
|
||||
_redirectChain: HTTPRequest[] = [];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected interception: {
|
||||
enabled: boolean;
|
||||
handled: boolean;
|
||||
handlers: Array<() => void | PromiseLike<any>>;
|
||||
resolutionState: InterceptResolutionState;
|
||||
requestOverrides: ContinueRequestOverrides;
|
||||
response: Partial<ResponseForRequest> | null;
|
||||
abortReason: Protocol.Network.ErrorReason | null;
|
||||
} = {
|
||||
enabled: false,
|
||||
handled: false,
|
||||
handlers: [],
|
||||
resolutionState: {
|
||||
action: InterceptResolutionAction.None,
|
||||
},
|
||||
requestOverrides: {},
|
||||
response: null,
|
||||
abortReason: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Warning! Using this client can break Puppeteer. Use with caution.
|
||||
*
|
||||
|
|
@ -139,18 +166,27 @@ export abstract class HTTPRequest {
|
|||
* if the interception is allowed to continue (ie, `abort()` and
|
||||
* `respond()` aren't called).
|
||||
*/
|
||||
abstract continueRequestOverrides(): ContinueRequestOverrides;
|
||||
continueRequestOverrides(): ContinueRequestOverrides {
|
||||
assert(this.interception.enabled, 'Request Interception is not enabled!');
|
||||
return this.interception.requestOverrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `ResponseForRequest` that gets used if the
|
||||
* interception is allowed to respond (ie, `abort()` is not called).
|
||||
*/
|
||||
abstract responseForRequest(): Partial<ResponseForRequest> | null;
|
||||
responseForRequest(): Partial<ResponseForRequest> | null {
|
||||
assert(this.interception.enabled, 'Request Interception is not enabled!');
|
||||
return this.interception.response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent reason for aborting the request
|
||||
*/
|
||||
abstract abortErrorReason(): Protocol.Network.ErrorReason | null;
|
||||
abortErrorReason(): Protocol.Network.ErrorReason | null {
|
||||
assert(this.interception.enabled, 'Request Interception is not enabled!');
|
||||
return this.interception.abortReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* An InterceptResolutionState object describing the current resolution
|
||||
|
|
@ -163,13 +199,23 @@ export abstract class HTTPRequest {
|
|||
* InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
|
||||
* `disabled`, `none`, or `already-handled`.
|
||||
*/
|
||||
abstract interceptResolutionState(): InterceptResolutionState;
|
||||
interceptResolutionState(): InterceptResolutionState {
|
||||
if (!this.interception.enabled) {
|
||||
return {action: InterceptResolutionAction.Disabled};
|
||||
}
|
||||
if (this.interception.handled) {
|
||||
return {action: InterceptResolutionAction.AlreadyHandled};
|
||||
}
|
||||
return {...this.interception.resolutionState};
|
||||
}
|
||||
|
||||
/**
|
||||
* Is `true` if the intercept resolution has already been handled,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
abstract isInterceptResolutionHandled(): boolean;
|
||||
isInterceptResolutionHandled(): boolean {
|
||||
return this.interception.handled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an async request handler to the processing queue.
|
||||
|
|
@ -177,15 +223,51 @@ export abstract class HTTPRequest {
|
|||
* but they are guaranteed to resolve before the request interception
|
||||
* is finalized.
|
||||
*/
|
||||
abstract enqueueInterceptAction(
|
||||
enqueueInterceptAction(
|
||||
pendingHandler: () => void | PromiseLike<unknown>
|
||||
): void;
|
||||
): void {
|
||||
this.interception.handlers.push(pendingHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract _abort(
|
||||
errorReason: Protocol.Network.ErrorReason | null
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract _respond(response: Partial<ResponseForRequest>): Promise<void>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract _continue(overrides: ContinueRequestOverrides): Promise<void>;
|
||||
|
||||
/**
|
||||
* Awaits pending interception handlers and then decides how to fulfill
|
||||
* the request interception.
|
||||
*/
|
||||
abstract finalizeInterceptions(): Promise<void>;
|
||||
async finalizeInterceptions(): Promise<void> {
|
||||
await this.interception.handlers.reduce((promiseChain, interceptAction) => {
|
||||
return promiseChain.then(interceptAction);
|
||||
}, Promise.resolve());
|
||||
this.interception.handlers = []; // TODO: verify this is correct top let gc run
|
||||
const {action} = this.interceptResolutionState();
|
||||
switch (action) {
|
||||
case 'abort':
|
||||
return await this._abort(this.interception.abortReason);
|
||||
case 'respond':
|
||||
if (this.interception.response === null) {
|
||||
throw new Error('Response is missing for the interception');
|
||||
}
|
||||
return await this._respond(this.interception.response);
|
||||
case 'continue':
|
||||
return await this._continue(this.interception.requestOverrides);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the request's resource type as it was perceived by the rendering
|
||||
|
|
@ -323,10 +405,42 @@ export abstract class HTTPRequest {
|
|||
*
|
||||
* Exception is immediately thrown if the request interception is not enabled.
|
||||
*/
|
||||
abstract continue(
|
||||
overrides?: ContinueRequestOverrides,
|
||||
async continue(
|
||||
overrides: ContinueRequestOverrides = {},
|
||||
priority?: number
|
||||
): Promise<void>;
|
||||
): Promise<void> {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this.url().startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
assert(this.interception.enabled, 'Request Interception is not enabled!');
|
||||
assert(!this.interception.handled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return await this._continue(overrides);
|
||||
}
|
||||
this.interception.requestOverrides = overrides;
|
||||
if (
|
||||
this.interception.resolutionState.priority === undefined ||
|
||||
priority > this.interception.resolutionState.priority
|
||||
) {
|
||||
this.interception.resolutionState = {
|
||||
action: InterceptResolutionAction.Continue,
|
||||
priority,
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (priority === this.interception.resolutionState.priority) {
|
||||
if (
|
||||
this.interception.resolutionState.action === 'abort' ||
|
||||
this.interception.resolutionState.action === 'respond'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.interception.resolutionState.action =
|
||||
InterceptResolutionAction.Continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fulfills a request with the given response.
|
||||
|
|
@ -360,10 +474,38 @@ export abstract class HTTPRequest {
|
|||
*
|
||||
* Exception is immediately thrown if the request interception is not enabled.
|
||||
*/
|
||||
abstract respond(
|
||||
async respond(
|
||||
response: Partial<ResponseForRequest>,
|
||||
priority?: number
|
||||
): Promise<void>;
|
||||
): Promise<void> {
|
||||
// Mocking responses for dataURL requests is not currently supported.
|
||||
if (this.url().startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
assert(this.interception.enabled, 'Request Interception is not enabled!');
|
||||
assert(!this.interception.handled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return await this._respond(response);
|
||||
}
|
||||
this.interception.response = response;
|
||||
if (
|
||||
this.interception.resolutionState.priority === undefined ||
|
||||
priority > this.interception.resolutionState.priority
|
||||
) {
|
||||
this.interception.resolutionState = {
|
||||
action: InterceptResolutionAction.Respond,
|
||||
priority,
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (priority === this.interception.resolutionState.priority) {
|
||||
if (this.interception.resolutionState.action === 'abort') {
|
||||
return;
|
||||
}
|
||||
this.interception.resolutionState.action =
|
||||
InterceptResolutionAction.Respond;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts a request.
|
||||
|
|
@ -379,7 +521,33 @@ export abstract class HTTPRequest {
|
|||
* {@link Page.setRequestInterception}. If it is not enabled, this method will
|
||||
* throw an exception immediately.
|
||||
*/
|
||||
abstract abort(errorCode?: ErrorCode, priority?: number): Promise<void>;
|
||||
async abort(
|
||||
errorCode: ErrorCode = 'failed',
|
||||
priority?: number
|
||||
): Promise<void> {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this.url().startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
const errorReason = errorReasons[errorCode];
|
||||
assert(errorReason, 'Unknown error code: ' + errorCode);
|
||||
assert(this.interception.enabled, 'Request Interception is not enabled!');
|
||||
assert(!this.interception.handled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return await this._abort(errorReason);
|
||||
}
|
||||
this.interception.abortReason = errorReason;
|
||||
if (
|
||||
this.interception.resolutionState.priority === undefined ||
|
||||
priority >= this.interception.resolutionState.priority
|
||||
) {
|
||||
this.interception.resolutionState = {
|
||||
action: InterceptResolutionAction.Abort,
|
||||
priority,
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -513,3 +681,33 @@ export const STATUS_TEXTS: Record<string, string> = {
|
|||
'510': 'Not Extended',
|
||||
'511': 'Network Authentication Required',
|
||||
} as const;
|
||||
|
||||
const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = {
|
||||
aborted: 'Aborted',
|
||||
accessdenied: 'AccessDenied',
|
||||
addressunreachable: 'AddressUnreachable',
|
||||
blockedbyclient: 'BlockedByClient',
|
||||
blockedbyresponse: 'BlockedByResponse',
|
||||
connectionaborted: 'ConnectionAborted',
|
||||
connectionclosed: 'ConnectionClosed',
|
||||
connectionfailed: 'ConnectionFailed',
|
||||
connectionrefused: 'ConnectionRefused',
|
||||
connectionreset: 'ConnectionReset',
|
||||
internetdisconnected: 'InternetDisconnected',
|
||||
namenotresolved: 'NameNotResolved',
|
||||
timedout: 'TimedOut',
|
||||
failed: 'Failed',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function handleError(error: ProtocolError): void {
|
||||
if (error.originalMessage.includes('Invalid header')) {
|
||||
throw error;
|
||||
}
|
||||
// In certain cases, protocol will return error if the request was
|
||||
// already canceled or the page was closed. We should tolerate these
|
||||
// errors.
|
||||
debugError(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,11 +81,18 @@ export abstract class HTTPResponse {
|
|||
|
||||
/**
|
||||
* Promise which resolves to a buffer with response body.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The buffer might be re-encoded by the browser
|
||||
* based on HTTP-headers or other heuristics. If the browser
|
||||
* failed to detect the correct encoding, the buffer might
|
||||
* be encoded incorrectly. See https://github.com/puppeteer/puppeteer/issues/6478.
|
||||
*/
|
||||
abstract buffer(): Promise<Buffer>;
|
||||
|
||||
/**
|
||||
* Promise which resolves to a text representation of response body.
|
||||
* Promise which resolves to a text (utf8) representation of response body.
|
||||
*/
|
||||
async text(): Promise<string> {
|
||||
const content = await this.buffer();
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import type {HTTPResponse} from '../api/HTTPResponse.js';
|
|||
import type {Accessibility} from '../cdp/Accessibility.js';
|
||||
import type {Coverage} from '../cdp/Coverage.js';
|
||||
import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
|
||||
import type {Credentials, NetworkConditions} from '../cdp/NetworkManager.js';
|
||||
import type {NetworkConditions} from '../cdp/NetworkManager.js';
|
||||
import type {Tracing} from '../cdp/Tracing.js';
|
||||
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
|
||||
import type {
|
||||
|
|
@ -131,6 +131,14 @@ export interface Metrics {
|
|||
JSHeapTotalSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Credentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
|
@ -274,7 +282,7 @@ export interface ScreenshotOptions {
|
|||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* Specifies the region of the page to clip.
|
||||
* Specifies the region of the page/element to clip.
|
||||
*/
|
||||
clip?: ScreenshotClip;
|
||||
/**
|
||||
|
|
@ -644,7 +652,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
|||
*
|
||||
* @deprecated We no longer support intercepting drag payloads. Use the new
|
||||
* drag APIs found on {@link ElementHandle} to drag (or just use the
|
||||
* {@link Page | Page.mouse}).
|
||||
* {@link Page.mouse}).
|
||||
*/
|
||||
abstract isDragInterceptionEnabled(): boolean;
|
||||
|
||||
|
|
@ -875,7 +883,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
|||
*
|
||||
* @deprecated We no longer support intercepting drag payloads. Use the new
|
||||
* drag APIs found on {@link ElementHandle} to drag (or just use the
|
||||
* {@link Page | Page.mouse}).
|
||||
* {@link Page.mouse}).
|
||||
*/
|
||||
abstract setDragInterception(enabled: boolean): Promise<void>;
|
||||
|
||||
|
|
@ -1342,7 +1350,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
|||
*
|
||||
* Functions installed via `page.exposeFunction` survive navigations.
|
||||
*
|
||||
* :::note
|
||||
* :::
|
||||
*
|
||||
* @example
|
||||
* An example of adding an `md5` function into the page:
|
||||
|
|
@ -2116,7 +2124,8 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
|||
*
|
||||
* This is either the viewport set with the previous {@link Page.setViewport}
|
||||
* call or the default viewport set via
|
||||
* {@link BrowserConnectOptions | BrowserConnectOptions.defaultViewport}.
|
||||
* {@link BrowserConnectOptions.defaultViewport |
|
||||
* BrowserConnectOptions.defaultViewport}.
|
||||
*/
|
||||
abstract viewport(): Viewport | null;
|
||||
|
||||
|
|
@ -2458,7 +2467,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
|||
};
|
||||
if (options.type === undefined && options.path !== undefined) {
|
||||
const filePath = options.path;
|
||||
// Note we cannot use Node.js here due to browser compatability.
|
||||
// Note we cannot use Node.js here due to browser compatibility.
|
||||
const extension = filePath
|
||||
.slice(filePath.lastIndexOf('.') + 1)
|
||||
.toLowerCase();
|
||||
|
|
@ -2609,7 +2618,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
|||
|
||||
/**
|
||||
* This method fetches an element with `selector`, scrolls it into view if
|
||||
* needed, and then uses {@link Page | Page.mouse} to click in the center of the
|
||||
* needed, and then uses {@link Page.mouse} to click in the center of the
|
||||
* element. If there's no element matching `selector`, the method throws an
|
||||
* error.
|
||||
*
|
||||
|
|
@ -2660,7 +2669,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
|||
|
||||
/**
|
||||
* This method fetches an element with `selector`, scrolls it into view if
|
||||
* needed, and then uses {@link Page | Page.mouse}
|
||||
* needed, and then uses {@link Page.mouse}
|
||||
* to hover over the center of the element.
|
||||
* If there's no element matching `selector`, the method throws an error.
|
||||
* @param selector - A
|
||||
|
|
@ -2709,7 +2718,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
|||
|
||||
/**
|
||||
* This method fetches an element with `selector`, scrolls it into view if
|
||||
* needed, and then uses {@link Page | Page.touchscreen}
|
||||
* needed, and then uses {@link Page.touchscreen}
|
||||
* to tap in the center of the element.
|
||||
* If there's no element matching `selector`, the method throws an error.
|
||||
* @param selector - A
|
||||
|
|
|
|||
|
|
@ -172,19 +172,23 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
});
|
||||
},
|
||||
retryAndRaceWithSignalAndTimer: <T>(
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
cause?: Error
|
||||
): OperatorFunction<T, T> => {
|
||||
const candidates = [];
|
||||
if (signal) {
|
||||
candidates.push(
|
||||
fromEvent(signal, 'abort').pipe(
|
||||
map(() => {
|
||||
if (signal.reason instanceof Error) {
|
||||
signal.reason.cause = cause;
|
||||
}
|
||||
throw signal.reason;
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
candidates.push(timeout(this._timeout));
|
||||
candidates.push(timeout(this._timeout, cause));
|
||||
return pipe(
|
||||
retry({delay: RETRY_DELAY}),
|
||||
raceWith<T, never[]>(...candidates)
|
||||
|
|
@ -368,6 +372,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
options?: Readonly<LocatorClickOptions>
|
||||
): Observable<void> {
|
||||
const signal = options?.signal;
|
||||
const cause = new Error('Locator.click');
|
||||
return this._wait(options).pipe(
|
||||
this.operators.conditions(
|
||||
[
|
||||
|
|
@ -388,7 +393,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
})
|
||||
);
|
||||
}),
|
||||
this.operators.retryAndRaceWithSignalAndTimer(signal)
|
||||
this.operators.retryAndRaceWithSignalAndTimer(signal, cause)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -398,6 +403,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
options?: Readonly<ActionOptions>
|
||||
): Observable<void> {
|
||||
const signal = options?.signal;
|
||||
const cause = new Error('Locator.fill');
|
||||
return this._wait(options).pipe(
|
||||
this.operators.conditions(
|
||||
[
|
||||
|
|
@ -521,7 +527,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
})
|
||||
);
|
||||
}),
|
||||
this.operators.retryAndRaceWithSignalAndTimer(signal)
|
||||
this.operators.retryAndRaceWithSignalAndTimer(signal, cause)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -530,6 +536,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
options?: Readonly<ActionOptions>
|
||||
): Observable<void> {
|
||||
const signal = options?.signal;
|
||||
const cause = new Error('Locator.hover');
|
||||
return this._wait(options).pipe(
|
||||
this.operators.conditions(
|
||||
[
|
||||
|
|
@ -549,7 +556,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
})
|
||||
);
|
||||
}),
|
||||
this.operators.retryAndRaceWithSignalAndTimer(signal)
|
||||
this.operators.retryAndRaceWithSignalAndTimer(signal, cause)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -558,6 +565,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
options?: Readonly<LocatorScrollOptions>
|
||||
): Observable<void> {
|
||||
const signal = options?.signal;
|
||||
const cause = new Error('Locator.scroll');
|
||||
return this._wait(options).pipe(
|
||||
this.operators.conditions(
|
||||
[
|
||||
|
|
@ -590,7 +598,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
})
|
||||
);
|
||||
}),
|
||||
this.operators.retryAndRaceWithSignalAndTimer(signal)
|
||||
this.operators.retryAndRaceWithSignalAndTimer(signal, cause)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -617,9 +625,10 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
|||
* @public
|
||||
*/
|
||||
async waitHandle(options?: Readonly<ActionOptions>): Promise<HandleFor<T>> {
|
||||
const cause = new Error('Locator.waitHandle');
|
||||
return await firstValueFrom(
|
||||
this._wait(options).pipe(
|
||||
this.operators.retryAndRaceWithSignalAndTimer(options?.signal)
|
||||
this.operators.retryAndRaceWithSignalAndTimer(options?.signal, cause)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,7 @@ const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
|
|||
*/
|
||||
export async function connectBidiOverCdp(
|
||||
cdp: CdpConnection,
|
||||
// TODO: replace with `BidiMapper.MapperOptions`, once it's exported in
|
||||
// https://github.com/puppeteer/puppeteer/pull/11415.
|
||||
options: {acceptInsecureCerts: boolean}
|
||||
options: BidiMapper.MapperOptions
|
||||
): Promise<BidiConnection> {
|
||||
const transportBiDi = new NoOpTransport();
|
||||
const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import {
|
|||
import {BrowserContextEvent} from '../api/BrowserContext.js';
|
||||
import type {Page} from '../api/Page.js';
|
||||
import type {Target} from '../api/Target.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
|
|
@ -50,7 +49,7 @@ export class BidiBrowser extends Browser {
|
|||
readonly protocol = 'webDriverBiDi';
|
||||
|
||||
// TODO: Update generator to include fully module
|
||||
static readonly subscribeModules: string[] = [
|
||||
static readonly subscribeModules: [string, ...string[]] = [
|
||||
'browsingContext',
|
||||
'network',
|
||||
'log',
|
||||
|
|
@ -133,8 +132,8 @@ export class BidiBrowser extends Browser {
|
|||
return !this.#browserName.toLocaleLowerCase().includes('firefox');
|
||||
}
|
||||
|
||||
override userAgent(): never {
|
||||
throw new UnsupportedOperation();
|
||||
override async userAgent(): Promise<string> {
|
||||
return this.#browserCore.session.capabilities.userAgent;
|
||||
}
|
||||
|
||||
#createBrowserContext(userContext: UserContext) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
|
||||
|
||||
import type {CommandOptions} from '../api/CDPSession.js';
|
||||
import {CDPSession} from '../api/CDPSession.js';
|
||||
import type {Connection as CdpConnection} from '../cdp/Connection.js';
|
||||
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
|
||||
|
|
@ -61,7 +62,8 @@ export class BidiCdpSession extends CDPSession {
|
|||
|
||||
override async send<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
params?: ProtocolMapping.Commands[T]['paramsType'][0]
|
||||
params?: ProtocolMapping.Commands[T]['paramsType'][0],
|
||||
options?: CommandOptions
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
if (this.#connection === undefined) {
|
||||
throw new UnsupportedOperation(
|
||||
|
|
@ -74,11 +76,15 @@ export class BidiCdpSession extends CDPSession {
|
|||
);
|
||||
}
|
||||
const session = await this.#sessionId.valueOrThrow();
|
||||
const {result} = await this.#connection.send('cdp.sendCommand', {
|
||||
method: method,
|
||||
params: params,
|
||||
session,
|
||||
});
|
||||
const {result} = await this.#connection.send(
|
||||
'cdp.sendCommand',
|
||||
{
|
||||
method: method,
|
||||
params: params,
|
||||
session,
|
||||
},
|
||||
options?.timeout
|
||||
);
|
||||
return result.result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,11 +97,12 @@ export class BidiConnection
|
|||
|
||||
send<T extends keyof Commands>(
|
||||
method: T,
|
||||
params: Commands[T]['params']
|
||||
params: Commands[T]['params'],
|
||||
timeout?: number
|
||||
): Promise<{result: Commands[T]['returnType']}> {
|
||||
assert(!this.#closed, 'Protocol error: Connection closed.');
|
||||
|
||||
return this.#callbacks.create(method, this.#timeout, id => {
|
||||
return this.#callbacks.create(method, timeout ?? this.#timeout, id => {
|
||||
const stringifiedMessage = JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
|
|||
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
|
||||
import type {Awaitable, NodeFor} from '../common/types.js';
|
||||
import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import {BidiCdpSession} from './CDPSession.js';
|
||||
import type {BrowsingContext} from './core/BrowsingContext.js';
|
||||
|
|
@ -114,13 +115,13 @@ export class BidiFrame extends Frame {
|
|||
this.browsingContext.on('request', ({request}) => {
|
||||
const httpRequest = BidiHTTPRequest.from(request, this);
|
||||
request.once('success', () => {
|
||||
// SAFETY: BidiHTTPRequest will create this before here.
|
||||
this.page().trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
|
||||
});
|
||||
|
||||
request.once('error', () => {
|
||||
this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
|
||||
});
|
||||
void httpRequest.finalizeInterceptions();
|
||||
});
|
||||
|
||||
this.browsingContext.on('navigation', ({navigation}) => {
|
||||
|
|
@ -300,10 +301,18 @@ export class BidiFrame extends Frame {
|
|||
// readiness=interactive.
|
||||
//
|
||||
// Related: https://bugzilla.mozilla.org/show_bug.cgi?id=1846601
|
||||
this.browsingContext.navigate(
|
||||
url,
|
||||
Bidi.BrowsingContext.ReadinessState.Interactive
|
||||
),
|
||||
this.browsingContext
|
||||
.navigate(url, Bidi.BrowsingContext.ReadinessState.Interactive)
|
||||
.catch(error => {
|
||||
if (
|
||||
isErrorLike(error) &&
|
||||
error.message.includes('net::ERR_HTTP_RESPONSE_CODE_FAILURE')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}),
|
||||
]).catch(
|
||||
rewriteNavigationError(
|
||||
url,
|
||||
|
|
@ -351,11 +360,7 @@ export class BidiFrame extends Frame {
|
|||
}),
|
||||
raceWith(
|
||||
fromEmitterEvent(navigation, 'fragment'),
|
||||
fromEmitterEvent(navigation, 'failed').pipe(
|
||||
map(({url}) => {
|
||||
throw new Error(`Navigation failed: ${url}`);
|
||||
})
|
||||
),
|
||||
fromEmitterEvent(navigation, 'failed'),
|
||||
fromEmitterEvent(navigation, 'aborted').pipe(
|
||||
map(({url}) => {
|
||||
throw new Error(`Navigation aborted: ${url}`);
|
||||
|
|
@ -401,11 +406,9 @@ export class BidiFrame extends Frame {
|
|||
if (!request) {
|
||||
return null;
|
||||
}
|
||||
const httpRequest = requests.get(request)!;
|
||||
const lastRedirect = httpRequest.redirectChain().at(-1);
|
||||
return (
|
||||
lastRedirect !== undefined ? lastRedirect : httpRequest
|
||||
).response();
|
||||
const lastRequest = request.lastRedirect ?? request;
|
||||
const httpRequest = requests.get(lastRequest)!;
|
||||
return httpRequest.response();
|
||||
}),
|
||||
raceWith(
|
||||
timeout(ms),
|
||||
|
|
@ -471,6 +474,7 @@ export class BidiFrame extends Frame {
|
|||
targetId: this._id,
|
||||
flatten: true,
|
||||
});
|
||||
await this.browsingContext.subscribe([Bidi.ChromiumBidi.BiDiModule.Cdp]);
|
||||
return new BidiCdpSession(this, sessionId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ import type {
|
|||
ContinueRequestOverrides,
|
||||
ResponseForRequest,
|
||||
} from '../api/HTTPRequest.js';
|
||||
import {HTTPRequest, type ResourceType} from '../api/HTTPRequest.js';
|
||||
import {
|
||||
HTTPRequest,
|
||||
STATUS_TEXTS,
|
||||
type ResourceType,
|
||||
handleError,
|
||||
} from '../api/HTTPRequest.js';
|
||||
import {PageEvent} from '../api/Page.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
|
||||
|
|
@ -26,41 +31,61 @@ export const requests = new WeakMap<Request, BidiHTTPRequest>();
|
|||
export class BidiHTTPRequest extends HTTPRequest {
|
||||
static from(
|
||||
bidiRequest: Request,
|
||||
frame: BidiFrame | undefined
|
||||
frame: BidiFrame,
|
||||
redirect?: BidiHTTPRequest
|
||||
): BidiHTTPRequest {
|
||||
const request = new BidiHTTPRequest(bidiRequest, frame);
|
||||
const request = new BidiHTTPRequest(bidiRequest, frame, redirect);
|
||||
request.#initialize();
|
||||
return request;
|
||||
}
|
||||
|
||||
#redirect: BidiHTTPRequest | undefined;
|
||||
#redirectBy: BidiHTTPRequest | undefined;
|
||||
#response: BidiHTTPResponse | null = null;
|
||||
override readonly id: string;
|
||||
readonly #frame: BidiFrame | undefined;
|
||||
readonly #frame: BidiFrame;
|
||||
readonly #request: Request;
|
||||
|
||||
private constructor(request: Request, frame: BidiFrame | undefined) {
|
||||
private constructor(
|
||||
request: Request,
|
||||
frame: BidiFrame,
|
||||
redirectBy?: BidiHTTPRequest
|
||||
) {
|
||||
super();
|
||||
requests.set(request, this);
|
||||
|
||||
this.interception.enabled = request.isBlocked;
|
||||
|
||||
this.#request = request;
|
||||
this.#frame = frame;
|
||||
this.#redirectBy = redirectBy;
|
||||
this.id = request.id;
|
||||
}
|
||||
|
||||
override get client(): CDPSession {
|
||||
throw new UnsupportedOperation();
|
||||
return this.#frame.client;
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
this.#request.on('redirect', request => {
|
||||
this.#redirect = BidiHTTPRequest.from(request, this.#frame);
|
||||
const httpRequest = BidiHTTPRequest.from(request, this.#frame, this);
|
||||
void httpRequest.finalizeInterceptions();
|
||||
});
|
||||
this.#request.once('success', data => {
|
||||
this.#response = BidiHTTPResponse.from(data, this);
|
||||
});
|
||||
this.#request.on('authenticate', this.#handleAuthentication);
|
||||
|
||||
this.#frame?.page().trustedEmitter.emit(PageEvent.Request, this);
|
||||
this.#frame.page().trustedEmitter.emit(PageEvent.Request, this);
|
||||
|
||||
if (Object.keys(this.#extraHTTPHeaders).length) {
|
||||
this.interception.handlers.push(async () => {
|
||||
await this.continue(
|
||||
{
|
||||
headers: this.headers(),
|
||||
},
|
||||
0
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override url(): string {
|
||||
|
|
@ -68,7 +93,7 @@ export class BidiHTTPRequest extends HTTPRequest {
|
|||
}
|
||||
|
||||
override resourceType(): ResourceType {
|
||||
return this.initiator().type.toLowerCase() as ResourceType;
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override method(): string {
|
||||
|
|
@ -87,12 +112,19 @@ export class BidiHTTPRequest extends HTTPRequest {
|
|||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
get #extraHTTPHeaders(): Record<string, string> {
|
||||
return this.#frame?.page()._extraHTTPHeaders ?? {};
|
||||
}
|
||||
|
||||
override headers(): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
for (const header of this.#request.headers) {
|
||||
headers[header.name.toLowerCase()] = header.value.value;
|
||||
}
|
||||
return headers;
|
||||
return {
|
||||
...headers,
|
||||
...this.#extraHTTPHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
override response(): BidiHTTPResponse | null {
|
||||
|
|
@ -115,65 +147,167 @@ export class BidiHTTPRequest extends HTTPRequest {
|
|||
}
|
||||
|
||||
override redirectChain(): BidiHTTPRequest[] {
|
||||
if (this.#redirect === undefined) {
|
||||
if (this.#redirectBy === undefined) {
|
||||
return [];
|
||||
}
|
||||
const redirects = [this.#redirect];
|
||||
const redirects = [this.#redirectBy];
|
||||
for (const redirect of redirects) {
|
||||
if (redirect.#redirect !== undefined) {
|
||||
redirects.push(redirect.#redirect);
|
||||
if (redirect.#redirectBy !== undefined) {
|
||||
redirects.push(redirect.#redirectBy);
|
||||
}
|
||||
}
|
||||
return redirects;
|
||||
}
|
||||
|
||||
override enqueueInterceptAction(
|
||||
pendingHandler: () => void | PromiseLike<unknown>
|
||||
): void {
|
||||
// Execute the handler when interception is not supported
|
||||
void pendingHandler();
|
||||
override frame(): BidiFrame {
|
||||
return this.#frame;
|
||||
}
|
||||
|
||||
override frame(): BidiFrame | null {
|
||||
return this.#frame ?? null;
|
||||
override async continue(
|
||||
overrides?: ContinueRequestOverrides,
|
||||
priority?: number | undefined
|
||||
): Promise<void> {
|
||||
return await super.continue(
|
||||
{
|
||||
headers: Object.keys(this.#extraHTTPHeaders).length
|
||||
? this.headers()
|
||||
: undefined,
|
||||
...overrides,
|
||||
},
|
||||
priority
|
||||
);
|
||||
}
|
||||
|
||||
override continueRequestOverrides(): never {
|
||||
throw new UnsupportedOperation();
|
||||
override async _continue(
|
||||
overrides: ContinueRequestOverrides = {}
|
||||
): Promise<void> {
|
||||
const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers);
|
||||
this.interception.handled = true;
|
||||
|
||||
return await this.#request
|
||||
.continueRequest({
|
||||
url: overrides.url,
|
||||
method: overrides.method,
|
||||
body: overrides.postData
|
||||
? {
|
||||
type: 'base64',
|
||||
value: btoa(overrides.postData),
|
||||
}
|
||||
: undefined,
|
||||
headers: headers.length > 0 ? headers : undefined,
|
||||
})
|
||||
.catch(error => {
|
||||
this.interception.handled = false;
|
||||
return handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
override continue(_overrides: ContinueRequestOverrides = {}): never {
|
||||
throw new UnsupportedOperation();
|
||||
override async _abort(): Promise<void> {
|
||||
this.interception.handled = true;
|
||||
return await this.#request.failRequest().catch(error => {
|
||||
this.interception.handled = false;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
override responseForRequest(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override abortErrorReason(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override interceptResolutionState(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override isInterceptResolutionHandled(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override finalizeInterceptions(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override abort(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override respond(
|
||||
_response: Partial<ResponseForRequest>,
|
||||
override async _respond(
|
||||
response: Partial<ResponseForRequest>,
|
||||
_priority?: number
|
||||
): never {
|
||||
throw new UnsupportedOperation();
|
||||
): Promise<void> {
|
||||
this.interception.handled = true;
|
||||
const responseBody: string | undefined =
|
||||
response.body && response.body instanceof Uint8Array
|
||||
? response.body.toString('base64')
|
||||
: response.body
|
||||
? btoa(response.body)
|
||||
: undefined;
|
||||
|
||||
const headers: Bidi.Network.Header[] = getBidiHeaders(response.headers);
|
||||
const hasContentLength = headers.some(header => {
|
||||
return header.name === 'content-length';
|
||||
});
|
||||
|
||||
if (response.contentType) {
|
||||
headers.push({
|
||||
name: 'content-type',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: response.contentType,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (responseBody && !hasContentLength) {
|
||||
const encoder = new TextEncoder();
|
||||
headers.push({
|
||||
name: 'content-length',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: String(encoder.encode(responseBody).byteLength),
|
||||
},
|
||||
});
|
||||
}
|
||||
const status = response.status || 200;
|
||||
|
||||
return await this.#request
|
||||
.provideResponse({
|
||||
statusCode: status,
|
||||
headers: headers.length > 0 ? headers : undefined,
|
||||
reasonPhrase: STATUS_TEXTS[status],
|
||||
body: responseBody
|
||||
? {
|
||||
type: 'base64',
|
||||
value: responseBody,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
.catch(error => {
|
||||
this.interception.handled = false;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
#authenticationHandled = false;
|
||||
#handleAuthentication = async () => {
|
||||
if (!this.#frame) {
|
||||
return;
|
||||
}
|
||||
const credentials = this.#frame.page()._credentials;
|
||||
if (credentials && !this.#authenticationHandled) {
|
||||
this.#authenticationHandled = true;
|
||||
void this.#request.continueWithAuth({
|
||||
action: 'provideCredentials',
|
||||
credentials: {
|
||||
type: 'password',
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
void this.#request.continueWithAuth({
|
||||
action: 'cancel',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getBidiHeaders(rawHeaders?: Record<string, unknown>) {
|
||||
const headers: Bidi.Network.Header[] = [];
|
||||
for (const [name, value] of Object.entries(rawHeaders ?? [])) {
|
||||
if (!Object.is(value, undefined)) {
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
|
||||
for (const value of values) {
|
||||
headers.push({
|
||||
name: name.toLowerCase(),
|
||||
value: {
|
||||
type: 'string',
|
||||
value: String(value),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ export class BidiHTTPResponse extends HTTPResponse {
|
|||
}
|
||||
|
||||
#initialize() {
|
||||
if (this.#data.fromCache) {
|
||||
this.#request
|
||||
.frame()
|
||||
?.page()
|
||||
.trustedEmitter.emit(PageEvent.RequestServedFromCache, this.#request);
|
||||
}
|
||||
this.#request.frame()?.page().trustedEmitter.emit(PageEvent.Response, this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ import type {BoundingBox} from '../api/ElementHandle.js';
|
|||
import type {WaitForOptions} from '../api/Frame.js';
|
||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||
import type {
|
||||
MediaFeature,
|
||||
Credentials,
|
||||
GeolocationOptions,
|
||||
MediaFeature,
|
||||
PageEvents,
|
||||
} from '../api/Page.js';
|
||||
import {
|
||||
|
|
@ -27,13 +28,22 @@ import {Accessibility} from '../cdp/Accessibility.js';
|
|||
import {Coverage} from '../cdp/Coverage.js';
|
||||
import {EmulationManager} from '../cdp/EmulationManager.js';
|
||||
import {Tracing} from '../cdp/Tracing.js';
|
||||
import type {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js';
|
||||
import type {DeleteCookiesRequest} from '../common/Cookie.js';
|
||||
import type {
|
||||
Cookie,
|
||||
CookieParam,
|
||||
CookieSameSite,
|
||||
DeleteCookiesRequest,
|
||||
} from '../common/Cookie.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import type {PDFOptions} from '../common/PDFOptions.js';
|
||||
import type {Awaitable} from '../common/types.js';
|
||||
import {evaluationString, parsePDFOptions, timeout} from '../common/util.js';
|
||||
import {
|
||||
evaluationString,
|
||||
isString,
|
||||
parsePDFOptions,
|
||||
timeout,
|
||||
} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {bubble} from '../util/decorators.js';
|
||||
|
|
@ -43,7 +53,6 @@ import type {BidiBrowser} from './Browser.js';
|
|||
import type {BidiBrowserContext} from './BrowserContext.js';
|
||||
import type {BidiCdpSession} from './CDPSession.js';
|
||||
import type {BrowsingContext} from './core/BrowsingContext.js';
|
||||
import {BidiElementHandle} from './ElementHandle.js';
|
||||
import {BidiFrame} from './Frame.js';
|
||||
import type {BidiHTTPResponse} from './HTTPResponse.js';
|
||||
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
|
||||
|
|
@ -161,21 +170,28 @@ export class BidiPage extends Page {
|
|||
}
|
||||
|
||||
async focusedFrame(): Promise<BidiFrame> {
|
||||
using frame = await this.mainFrame()
|
||||
using handle = (await this.mainFrame()
|
||||
.isolatedRealm()
|
||||
.evaluateHandle(() => {
|
||||
let frame: HTMLIFrameElement | undefined;
|
||||
let win: Window | null = window;
|
||||
while (win?.document.activeElement instanceof HTMLIFrameElement) {
|
||||
frame = win.document.activeElement;
|
||||
win = frame.contentWindow;
|
||||
let win = window;
|
||||
while (
|
||||
win.document.activeElement instanceof win.HTMLIFrameElement ||
|
||||
win.document.activeElement instanceof win.HTMLFrameElement
|
||||
) {
|
||||
if (win.document.activeElement.contentWindow === null) {
|
||||
break;
|
||||
}
|
||||
win = win.document.activeElement.contentWindow as typeof win;
|
||||
}
|
||||
return frame;
|
||||
});
|
||||
if (!(frame instanceof BidiElementHandle)) {
|
||||
return this.mainFrame();
|
||||
}
|
||||
return await frame.contentFrame();
|
||||
return win;
|
||||
})) as BidiJSHandle<Window & typeof globalThis>;
|
||||
const value = handle.remoteValue();
|
||||
assert(value.type === 'window');
|
||||
const frame = this.frames().find(frame => {
|
||||
return frame._id === value.value.context;
|
||||
});
|
||||
assert(frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
override frames(): BidiFrame[] {
|
||||
|
|
@ -311,6 +327,17 @@ export class BidiPage extends Page {
|
|||
preferCSSPageSize,
|
||||
} = parsePDFOptions(options, 'cm');
|
||||
const pageRanges = ranges ? ranges.split(', ') : [];
|
||||
|
||||
await firstValueFrom(
|
||||
from(
|
||||
this.mainFrame()
|
||||
.isolatedRealm()
|
||||
.evaluate(() => {
|
||||
return document.fonts.ready;
|
||||
})
|
||||
).pipe(raceWith(timeout(ms)))
|
||||
);
|
||||
|
||||
const data = await firstValueFrom(
|
||||
from(
|
||||
this.#frame.browsingContext.print({
|
||||
|
|
@ -489,8 +516,71 @@ export class BidiPage extends Page {
|
|||
return [...this.#workers];
|
||||
}
|
||||
|
||||
override setRequestInterception(): never {
|
||||
throw new UnsupportedOperation();
|
||||
#userInterception?: string;
|
||||
override async setRequestInterception(enable: boolean): Promise<void> {
|
||||
this.#userInterception = await this.#toggleInterception(
|
||||
[Bidi.Network.InterceptPhase.BeforeRequestSent],
|
||||
this.#userInterception,
|
||||
enable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_extraHTTPHeaders: Record<string, string> = {};
|
||||
#extraHeadersInterception?: string;
|
||||
override async setExtraHTTPHeaders(
|
||||
headers: Record<string, string>
|
||||
): Promise<void> {
|
||||
const extraHTTPHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
assert(
|
||||
isString(value),
|
||||
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`
|
||||
);
|
||||
extraHTTPHeaders[key.toLowerCase()] = value;
|
||||
}
|
||||
this._extraHTTPHeaders = extraHTTPHeaders;
|
||||
|
||||
this.#extraHeadersInterception = await this.#toggleInterception(
|
||||
[Bidi.Network.InterceptPhase.BeforeRequestSent],
|
||||
this.#extraHeadersInterception,
|
||||
Boolean(Object.keys(this._extraHTTPHeaders).length)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_credentials: Credentials | null = null;
|
||||
#authInterception?: string;
|
||||
override async authenticate(credentials: Credentials | null): Promise<void> {
|
||||
this.#authInterception = await this.#toggleInterception(
|
||||
[Bidi.Network.InterceptPhase.AuthRequired],
|
||||
this.#authInterception,
|
||||
Boolean(credentials)
|
||||
);
|
||||
|
||||
this._credentials = credentials;
|
||||
}
|
||||
|
||||
async #toggleInterception(
|
||||
phases: [Bidi.Network.InterceptPhase, ...Bidi.Network.InterceptPhase[]],
|
||||
interception: string | undefined,
|
||||
expected: boolean
|
||||
): Promise<string | undefined> {
|
||||
if (expected && !interception) {
|
||||
return await this.#frame.browsingContext.addIntercept({
|
||||
phases,
|
||||
});
|
||||
} else if (!expected && interception) {
|
||||
await this.#frame.browsingContext.userContext.browser.removeIntercept(
|
||||
interception
|
||||
);
|
||||
return;
|
||||
}
|
||||
return interception;
|
||||
}
|
||||
|
||||
override setDragInterception(): never {
|
||||
|
|
@ -603,14 +693,6 @@ export class BidiPage extends Page {
|
|||
await this.#frame.removeExposedFunction(name);
|
||||
}
|
||||
|
||||
override authenticate(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override setExtraHTTPHeaders(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override metrics(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,20 +51,17 @@ export class Browser extends EventEmitter<{
|
|||
return browser;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
#closed = false;
|
||||
#reason: string | undefined;
|
||||
readonly #disposables = new DisposableStack();
|
||||
readonly #userContexts = new Map<string, UserContext>();
|
||||
readonly session: Session;
|
||||
readonly #sharedWorkers = new Map<string, SharedWorkerRealm>();
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(session: Session) {
|
||||
super();
|
||||
// keep-sorted start
|
||||
|
||||
this.session = session;
|
||||
// keep-sorted end
|
||||
}
|
||||
|
||||
async #initialize() {
|
||||
|
|
@ -141,7 +138,6 @@ export class Browser extends EventEmitter<{
|
|||
return userContext;
|
||||
}
|
||||
|
||||
// keep-sorted start block=yes
|
||||
get closed(): boolean {
|
||||
return this.#closed;
|
||||
}
|
||||
|
|
@ -158,7 +154,6 @@ export class Browser extends EventEmitter<{
|
|||
get userContexts(): Iterable<UserContext> {
|
||||
return this.#userContexts.values();
|
||||
}
|
||||
// keep-sorted end
|
||||
|
||||
@inertIfDisposed
|
||||
dispose(reason?: string, closed = false): void {
|
||||
|
|
@ -199,6 +194,16 @@ export class Browser extends EventEmitter<{
|
|||
return script;
|
||||
}
|
||||
|
||||
@throwIfDisposed<Browser>(browser => {
|
||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||
return browser.#reason!;
|
||||
})
|
||||
async removeIntercept(intercept: Bidi.Network.Intercept): Promise<void> {
|
||||
await this.session.send('network.removeIntercept', {
|
||||
intercept,
|
||||
});
|
||||
}
|
||||
|
||||
@throwIfDisposed<Browser>(browser => {
|
||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||
return browser.#reason!;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ import {Request} from './Request.js';
|
|||
import type {UserContext} from './UserContext.js';
|
||||
import {UserPrompt} from './UserPrompt.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type AddInterceptOptions = Omit<
|
||||
Bidi.Network.AddInterceptParameters,
|
||||
'contexts'
|
||||
>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
|
@ -121,7 +129,6 @@ export class BrowsingContext extends EventEmitter<{
|
|||
return browsingContext;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
#navigation: Navigation | undefined;
|
||||
#reason?: string;
|
||||
#url: string;
|
||||
|
|
@ -133,7 +140,6 @@ export class BrowsingContext extends EventEmitter<{
|
|||
readonly id: string;
|
||||
readonly parent: BrowsingContext | undefined;
|
||||
readonly userContext: UserContext;
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(
|
||||
context: UserContext,
|
||||
|
|
@ -142,12 +148,11 @@ export class BrowsingContext extends EventEmitter<{
|
|||
url: string
|
||||
) {
|
||||
super();
|
||||
// keep-sorted start
|
||||
|
||||
this.#url = url;
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
this.userContext = context;
|
||||
// keep-sorted end
|
||||
|
||||
this.defaultRealm = this.#createWindowRealm();
|
||||
}
|
||||
|
|
@ -275,7 +280,6 @@ export class BrowsingContext extends EventEmitter<{
|
|||
});
|
||||
}
|
||||
|
||||
// keep-sorted start block=yes
|
||||
get #session() {
|
||||
return this.userContext.browser.session;
|
||||
}
|
||||
|
|
@ -306,7 +310,6 @@ export class BrowsingContext extends EventEmitter<{
|
|||
get url(): string {
|
||||
return this.#url;
|
||||
}
|
||||
// keep-sorted end
|
||||
|
||||
#createWindowRealm(sandbox?: string) {
|
||||
const realm = WindowRealm.from(this, sandbox);
|
||||
|
|
@ -478,11 +481,26 @@ export class BrowsingContext extends EventEmitter<{
|
|||
functionDeclaration,
|
||||
{
|
||||
...options,
|
||||
contexts: [this, ...(options.contexts ?? [])],
|
||||
contexts: [this],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@throwIfDisposed<BrowsingContext>(context => {
|
||||
// SAFETY: Disposal implies this exists.
|
||||
return context.#reason!;
|
||||
})
|
||||
async addIntercept(options: AddInterceptOptions): Promise<string> {
|
||||
const {
|
||||
result: {intercept},
|
||||
} = await this.userContext.browser.session.send('network.addIntercept', {
|
||||
...options,
|
||||
contexts: [this.id],
|
||||
});
|
||||
|
||||
return intercept;
|
||||
}
|
||||
|
||||
@throwIfDisposed<BrowsingContext>(context => {
|
||||
// SAFETY: Disposal implies this exists.
|
||||
return context.#reason!;
|
||||
|
|
@ -539,6 +557,22 @@ export class BrowsingContext extends EventEmitter<{
|
|||
});
|
||||
}
|
||||
|
||||
@throwIfDisposed<BrowsingContext>(context => {
|
||||
// SAFETY: Disposal implies this exists.
|
||||
return context.#reason!;
|
||||
})
|
||||
async subscribe(events: [string, ...string[]]): Promise<void> {
|
||||
await this.#session.subscribe(events, [this.id]);
|
||||
}
|
||||
|
||||
@throwIfDisposed<BrowsingContext>(context => {
|
||||
// SAFETY: Disposal implies this exists.
|
||||
return context.#reason!;
|
||||
})
|
||||
async addInterception(events: [string, ...string[]]): Promise<void> {
|
||||
await this.#session.subscribe(events, [this.id]);
|
||||
}
|
||||
|
||||
[disposeSymbol](): void {
|
||||
this.#reason ??=
|
||||
'Browsing context already closed, probably because the user context closed.';
|
||||
|
|
|
|||
|
|
@ -149,6 +149,31 @@ export interface Commands {
|
|||
params: Bidi.Storage.SetCookieParameters;
|
||||
returnType: Bidi.Storage.SetCookieParameters;
|
||||
};
|
||||
|
||||
'network.addIntercept': {
|
||||
params: Bidi.Network.AddInterceptParameters;
|
||||
returnType: Bidi.Network.AddInterceptResult;
|
||||
};
|
||||
'network.removeIntercept': {
|
||||
params: Bidi.Network.RemoveInterceptParameters;
|
||||
returnType: Bidi.EmptyResult;
|
||||
};
|
||||
'network.continueRequest': {
|
||||
params: Bidi.Network.ContinueRequestParameters;
|
||||
returnType: Bidi.EmptyResult;
|
||||
};
|
||||
'network.continueWithAuth': {
|
||||
params: Bidi.Network.ContinueWithAuthParameters;
|
||||
returnType: Bidi.EmptyResult;
|
||||
};
|
||||
'network.failRequest': {
|
||||
params: Bidi.Network.FailRequestParameters;
|
||||
returnType: Bidi.EmptyResult;
|
||||
};
|
||||
'network.provideResponse': {
|
||||
params: Bidi.Network.ProvideResponseParameters;
|
||||
returnType: Bidi.EmptyResult;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||
import {inertIfDisposed} from '../../util/decorators.js';
|
||||
import {Deferred} from '../../util/Deferred.js';
|
||||
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||
|
||||
import type {BrowsingContext} from './BrowsingContext.js';
|
||||
|
|
@ -39,19 +38,16 @@ export class Navigation extends EventEmitter<{
|
|||
return navigation;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
#request: Request | undefined;
|
||||
#navigation: Navigation | undefined;
|
||||
readonly #browsingContext: BrowsingContext;
|
||||
readonly #disposables = new DisposableStack();
|
||||
readonly #id = new Deferred<string | null>();
|
||||
// keep-sorted end
|
||||
#id?: string | null;
|
||||
|
||||
private constructor(context: BrowsingContext) {
|
||||
super();
|
||||
// keep-sorted start
|
||||
|
||||
this.#browsingContext = context;
|
||||
// keep-sorted end
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
|
|
@ -69,7 +65,6 @@ export class Navigation extends EventEmitter<{
|
|||
browsingContextEmitter.on('request', ({request}) => {
|
||||
if (
|
||||
request.navigation === undefined ||
|
||||
this.#request !== undefined ||
|
||||
// If a request with a navigation ID comes in, then the navigation ID is
|
||||
// for this navigation.
|
||||
!this.#matches(request.navigation)
|
||||
|
|
@ -79,6 +74,13 @@ export class Navigation extends EventEmitter<{
|
|||
|
||||
this.#request = request;
|
||||
this.emit('request', request);
|
||||
const requestEmitter = this.#disposables.use(
|
||||
new EventEmitter(this.#request)
|
||||
);
|
||||
|
||||
requestEmitter.on('redirect', request => {
|
||||
this.#request = request;
|
||||
});
|
||||
});
|
||||
|
||||
const sessionEmitter = this.#disposables.use(
|
||||
|
|
@ -139,14 +141,13 @@ export class Navigation extends EventEmitter<{
|
|||
if (this.#navigation !== undefined && !this.#navigation.disposed) {
|
||||
return false;
|
||||
}
|
||||
if (!this.#id.resolved()) {
|
||||
this.#id.resolve(navigation);
|
||||
if (this.#id === undefined) {
|
||||
this.#id = navigation;
|
||||
return true;
|
||||
}
|
||||
return this.#id.value() === navigation;
|
||||
return this.#id === navigation;
|
||||
}
|
||||
|
||||
// keep-sorted start block=yes
|
||||
get #session() {
|
||||
return this.#browsingContext.userContext.browser.session;
|
||||
}
|
||||
|
|
@ -159,7 +160,6 @@ export class Navigation extends EventEmitter<{
|
|||
get navigation(): Navigation | undefined {
|
||||
return this.#navigation;
|
||||
}
|
||||
// keep-sorted end
|
||||
|
||||
@inertIfDisposed
|
||||
private dispose(): void {
|
||||
|
|
|
|||
|
|
@ -44,22 +44,19 @@ export abstract class Realm extends EventEmitter<{
|
|||
/** Emitted when a shared worker is created in the realm. */
|
||||
sharedworker: SharedWorkerRealm;
|
||||
}> {
|
||||
// keep-sorted start
|
||||
#reason?: string;
|
||||
protected readonly disposables = new DisposableStack();
|
||||
readonly id: string;
|
||||
readonly origin: string;
|
||||
// keep-sorted end
|
||||
protected executionContextId?: number;
|
||||
|
||||
protected constructor(id: string, origin: string) {
|
||||
super();
|
||||
// keep-sorted start
|
||||
|
||||
this.id = id;
|
||||
this.origin = origin;
|
||||
// keep-sorted end
|
||||
}
|
||||
|
||||
// keep-sorted start block=yes
|
||||
get disposed(): boolean {
|
||||
return this.#reason !== undefined;
|
||||
}
|
||||
|
|
@ -67,7 +64,6 @@ export abstract class Realm extends EventEmitter<{
|
|||
get target(): Bidi.Script.Target {
|
||||
return {realm: this.id};
|
||||
}
|
||||
// keep-sorted end
|
||||
|
||||
@inertIfDisposed
|
||||
protected dispose(reason?: string): void {
|
||||
|
|
@ -127,11 +123,15 @@ export abstract class Realm extends EventEmitter<{
|
|||
return realm.#reason!;
|
||||
})
|
||||
async resolveExecutionContextId(): Promise<number> {
|
||||
const {result} = await (this.session.connection as BidiConnection).send(
|
||||
'cdp.resolveRealm',
|
||||
{realm: this.id}
|
||||
);
|
||||
return result.executionContextId;
|
||||
if (!this.executionContextId) {
|
||||
const {result} = await (this.session.connection as BidiConnection).send(
|
||||
'cdp.resolveRealm',
|
||||
{realm: this.id}
|
||||
);
|
||||
this.executionContextId = result.executionContextId;
|
||||
}
|
||||
|
||||
return this.executionContextId;
|
||||
}
|
||||
|
||||
[disposeSymbol](): void {
|
||||
|
|
@ -154,19 +154,16 @@ export class WindowRealm extends Realm {
|
|||
return realm;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
readonly browsingContext: BrowsingContext;
|
||||
readonly sandbox?: string;
|
||||
// keep-sorted end
|
||||
|
||||
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||
|
||||
private constructor(context: BrowsingContext, sandbox?: string) {
|
||||
super('', '');
|
||||
// keep-sorted start
|
||||
|
||||
this.browsingContext = context;
|
||||
this.sandbox = sandbox;
|
||||
// keep-sorted end
|
||||
}
|
||||
|
||||
#initialize(): void {
|
||||
|
|
@ -188,6 +185,7 @@ export class WindowRealm extends Realm {
|
|||
}
|
||||
(this as any).id = info.realm;
|
||||
(this as any).origin = info.origin;
|
||||
this.executionContextId = undefined;
|
||||
this.emit('updated', this);
|
||||
});
|
||||
sessionEmitter.on('script.realmCreated', info => {
|
||||
|
|
@ -242,10 +240,8 @@ export class DedicatedWorkerRealm extends Realm {
|
|||
return realm;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||
readonly owners: Set<DedicatedWorkerOwnerRealm>;
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(
|
||||
owner: DedicatedWorkerOwnerRealm,
|
||||
|
|
@ -300,10 +296,8 @@ export class SharedWorkerRealm extends Realm {
|
|||
return realm;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||
readonly browser: Browser;
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(browser: Browser, id: string, origin: string) {
|
||||
super(id, origin);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export class Request extends EventEmitter<{
|
|||
/** Emitted when the request is redirected. */
|
||||
redirect: Request;
|
||||
/** Emitted when the request succeeds. */
|
||||
authenticate: void;
|
||||
/** Emitted when the request succeeds. */
|
||||
success: Bidi.Network.ResponseData;
|
||||
/** Emitted when the request fails. */
|
||||
error: string;
|
||||
|
|
@ -32,24 +34,21 @@ export class Request extends EventEmitter<{
|
|||
return request;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
#error?: string;
|
||||
#redirect?: Request;
|
||||
#response?: Bidi.Network.ResponseData;
|
||||
readonly #browsingContext: BrowsingContext;
|
||||
readonly #disposables = new DisposableStack();
|
||||
readonly #event: Bidi.Network.BeforeRequestSentParameters;
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(
|
||||
browsingContext: BrowsingContext,
|
||||
event: Bidi.Network.BeforeRequestSentParameters
|
||||
) {
|
||||
super();
|
||||
// keep-sorted start
|
||||
|
||||
this.#browsingContext = browsingContext;
|
||||
this.#event = event;
|
||||
// keep-sorted end
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
|
|
@ -77,6 +76,17 @@ export class Request extends EventEmitter<{
|
|||
this.emit('redirect', this.#redirect);
|
||||
this.dispose();
|
||||
});
|
||||
sessionEmitter.on('network.authRequired', event => {
|
||||
if (
|
||||
event.context !== this.#browsingContext.id ||
|
||||
event.request.request !== this.id ||
|
||||
// Don't try to authenticate for events that are not blocked
|
||||
!event.isBlocked
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.emit('authenticate', undefined);
|
||||
});
|
||||
sessionEmitter.on('network.fetchError', event => {
|
||||
if (
|
||||
event.context !== this.#browsingContext.id ||
|
||||
|
|
@ -107,7 +117,6 @@ export class Request extends EventEmitter<{
|
|||
});
|
||||
}
|
||||
|
||||
// keep-sorted start block=yes
|
||||
get #session() {
|
||||
return this.#browsingContext.userContext.browser.session;
|
||||
}
|
||||
|
|
@ -135,13 +144,82 @@ export class Request extends EventEmitter<{
|
|||
get redirect(): Request | undefined {
|
||||
return this.#redirect;
|
||||
}
|
||||
get lastRedirect(): Request | undefined {
|
||||
let redirect = this.#redirect;
|
||||
while (redirect) {
|
||||
if (redirect && !redirect.#redirect) {
|
||||
return redirect;
|
||||
}
|
||||
redirect = redirect.#redirect;
|
||||
}
|
||||
return redirect;
|
||||
}
|
||||
get response(): Bidi.Network.ResponseData | undefined {
|
||||
return this.#response;
|
||||
}
|
||||
get url(): string {
|
||||
return this.#event.request.url;
|
||||
}
|
||||
// keep-sorted end
|
||||
get isBlocked(): boolean {
|
||||
return this.#event.isBlocked;
|
||||
}
|
||||
|
||||
async continueRequest({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
cookies,
|
||||
body,
|
||||
}: Omit<Bidi.Network.ContinueRequestParameters, 'request'>): Promise<void> {
|
||||
await this.#session.send('network.continueRequest', {
|
||||
request: this.id,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
cookies,
|
||||
});
|
||||
}
|
||||
|
||||
async failRequest(): Promise<void> {
|
||||
await this.#session.send('network.failRequest', {
|
||||
request: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
async provideResponse({
|
||||
statusCode,
|
||||
reasonPhrase,
|
||||
headers,
|
||||
body,
|
||||
}: Omit<Bidi.Network.ProvideResponseParameters, 'request'>): Promise<void> {
|
||||
await this.#session.send('network.provideResponse', {
|
||||
request: this.id,
|
||||
statusCode,
|
||||
reasonPhrase,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
async continueWithAuth(
|
||||
parameters:
|
||||
| Bidi.Network.ContinueWithAuthCredentials
|
||||
| Bidi.Network.ContinueWithAuthNoCredentials
|
||||
): Promise<void> {
|
||||
if (parameters.action === 'provideCredentials') {
|
||||
await this.#session.send('network.continueWithAuth', {
|
||||
request: this.id,
|
||||
action: parameters.action,
|
||||
credentials: parameters.credentials,
|
||||
});
|
||||
} else {
|
||||
await this.#session.send('network.continueWithAuth', {
|
||||
request: this.id,
|
||||
action: parameters.action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@inertIfDisposed
|
||||
private dispose(): void {
|
||||
|
|
|
|||
|
|
@ -71,8 +71,9 @@ export class Session
|
|||
platformName: '',
|
||||
setWindowRect: false,
|
||||
webSocketUrl: '',
|
||||
userAgent: '',
|
||||
},
|
||||
};
|
||||
} satisfies Bidi.Session.NewResult;
|
||||
}
|
||||
|
||||
const session = new Session(connection, result);
|
||||
|
|
@ -80,21 +81,18 @@ export class Session
|
|||
return session;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
#reason: string | undefined;
|
||||
readonly #disposables = new DisposableStack();
|
||||
readonly #info: Bidi.Session.NewResult;
|
||||
readonly browser!: Browser;
|
||||
@bubble()
|
||||
accessor connection: Connection;
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
|
||||
super();
|
||||
// keep-sorted start
|
||||
|
||||
this.#info = info;
|
||||
this.connection = connection;
|
||||
// keep-sorted end
|
||||
}
|
||||
|
||||
async #initialize(): Promise<void> {
|
||||
|
|
@ -120,7 +118,6 @@ export class Session
|
|||
});
|
||||
}
|
||||
|
||||
// keep-sorted start block=yes
|
||||
get capabilities(): Bidi.Session.NewResult['capabilities'] {
|
||||
return this.#info.capabilities;
|
||||
}
|
||||
|
|
@ -133,7 +130,6 @@ export class Session
|
|||
get id(): string {
|
||||
return this.#info.sessionId;
|
||||
}
|
||||
// keep-sorted end
|
||||
|
||||
@inertIfDisposed
|
||||
private dispose(reason?: string): void {
|
||||
|
|
@ -163,9 +159,27 @@ export class Session
|
|||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||
return session.#reason!;
|
||||
})
|
||||
async subscribe(events: string[]): Promise<void> {
|
||||
async subscribe(
|
||||
events: [string, ...string[]],
|
||||
contexts?: [string, ...string[]]
|
||||
): Promise<void> {
|
||||
await this.send('session.subscribe', {
|
||||
events,
|
||||
contexts,
|
||||
});
|
||||
}
|
||||
|
||||
@throwIfDisposed<Session>(session => {
|
||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||
return session.#reason!;
|
||||
})
|
||||
async addIntercepts(
|
||||
events: [string, ...string[]],
|
||||
contexts?: [string, ...string[]]
|
||||
): Promise<void> {
|
||||
await this.send('session.subscribe', {
|
||||
events,
|
||||
contexts,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,21 +52,18 @@ export class UserContext extends EventEmitter<{
|
|||
return context;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
#reason?: string;
|
||||
// Note these are only top-level contexts.
|
||||
readonly #browsingContexts = new Map<string, BrowsingContext>();
|
||||
readonly #disposables = new DisposableStack();
|
||||
readonly #id: string;
|
||||
readonly browser: Browser;
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(browser: Browser, id: string) {
|
||||
super();
|
||||
// keep-sorted start
|
||||
|
||||
this.#id = id;
|
||||
this.browser = browser;
|
||||
// keep-sorted end
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
|
|
@ -110,7 +107,6 @@ export class UserContext extends EventEmitter<{
|
|||
});
|
||||
}
|
||||
|
||||
// keep-sorted start block=yes
|
||||
get #session() {
|
||||
return this.browser.session;
|
||||
}
|
||||
|
|
@ -126,7 +122,6 @@ export class UserContext extends EventEmitter<{
|
|||
get id(): string {
|
||||
return this.#id;
|
||||
}
|
||||
// keep-sorted end
|
||||
|
||||
@inertIfDisposed
|
||||
private dispose(reason?: string): void {
|
||||
|
|
@ -227,8 +222,7 @@ export class UserContext extends EventEmitter<{
|
|||
origin,
|
||||
descriptor,
|
||||
state,
|
||||
// @ts-expect-error not standard implementation.
|
||||
'goog:userContext': this.#id,
|
||||
userContext: this.#id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,23 +49,20 @@ export class UserPrompt extends EventEmitter<{
|
|||
return userPrompt;
|
||||
}
|
||||
|
||||
// keep-sorted start
|
||||
#reason?: string;
|
||||
#result?: UserPromptResult;
|
||||
readonly #disposables = new DisposableStack();
|
||||
readonly browsingContext: BrowsingContext;
|
||||
readonly info: Bidi.BrowsingContext.UserPromptOpenedParameters;
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(
|
||||
context: BrowsingContext,
|
||||
info: Bidi.BrowsingContext.UserPromptOpenedParameters
|
||||
) {
|
||||
super();
|
||||
// keep-sorted start
|
||||
|
||||
this.browsingContext = context;
|
||||
this.info = info;
|
||||
// keep-sorted end
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
|
|
@ -89,7 +86,6 @@ export class UserPrompt extends EventEmitter<{
|
|||
});
|
||||
}
|
||||
|
||||
// keep-sorted start block=yes
|
||||
get #session() {
|
||||
return this.browsingContext.userContext.browser.session;
|
||||
}
|
||||
|
|
@ -105,7 +101,6 @@ export class UserPrompt extends EventEmitter<{
|
|||
get result(): UserPromptResult | undefined {
|
||||
return this.#result;
|
||||
}
|
||||
// keep-sorted end
|
||||
|
||||
@inertIfDisposed
|
||||
private dispose(reason?: string): void {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,16 @@ const queryAXTree = async (
|
|||
role,
|
||||
});
|
||||
return nodes.filter((node: Protocol.Accessibility.AXNode) => {
|
||||
return !node.role || !NON_ELEMENT_NODE_ROLES.has(node.role.value);
|
||||
if (node.ignored) {
|
||||
return false;
|
||||
}
|
||||
if (!node.role) {
|
||||
return false;
|
||||
}
|
||||
if (NON_ELEMENT_NODE_ROLES.has(node.role.value)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,20 +12,18 @@ import type {DebugInfo} from '../api/Browser.js';
|
|||
import {
|
||||
Browser as BrowserBase,
|
||||
BrowserEvent,
|
||||
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
|
||||
type BrowserCloseCallback,
|
||||
type BrowserContextOptions,
|
||||
type IsPageTargetCallback,
|
||||
type Permission,
|
||||
type TargetFilterCallback,
|
||||
} from '../api/Browser.js';
|
||||
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
|
||||
import {BrowserContextEvent} from '../api/BrowserContext.js';
|
||||
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
|
||||
import type {Page} from '../api/Page.js';
|
||||
import type {Target} from '../api/Target.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
import {CdpBrowserContext} from './BrowserContext.js';
|
||||
import {ChromeTargetManager} from './ChromeTargetManager.js';
|
||||
import type {Connection} from './Connection.js';
|
||||
import {FirefoxTargetManager} from './FirefoxTargetManager.js';
|
||||
|
|
@ -424,90 +422,3 @@ export class CdpBrowser extends BrowserBase {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpBrowserContext extends BrowserContext {
|
||||
#connection: Connection;
|
||||
#browser: CdpBrowser;
|
||||
#id?: string;
|
||||
|
||||
constructor(connection: Connection, browser: CdpBrowser, contextId?: string) {
|
||||
super();
|
||||
this.#connection = connection;
|
||||
this.#browser = browser;
|
||||
this.#id = contextId;
|
||||
}
|
||||
|
||||
override get id(): string | undefined {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
override targets(): CdpTarget[] {
|
||||
return this.#browser.targets().filter(target => {
|
||||
return target.browserContext() === this;
|
||||
});
|
||||
}
|
||||
|
||||
override async pages(): Promise<Page[]> {
|
||||
const pages = await Promise.all(
|
||||
this.targets()
|
||||
.filter(target => {
|
||||
return (
|
||||
target.type() === 'page' ||
|
||||
(target.type() === 'other' &&
|
||||
this.#browser._getIsPageTargetCallback()?.(target))
|
||||
);
|
||||
})
|
||||
.map(target => {
|
||||
return target.page();
|
||||
})
|
||||
);
|
||||
return pages.filter((page): page is Page => {
|
||||
return !!page;
|
||||
});
|
||||
}
|
||||
|
||||
override isIncognito(): boolean {
|
||||
return !!this.#id;
|
||||
}
|
||||
|
||||
override async overridePermissions(
|
||||
origin: string,
|
||||
permissions: Permission[]
|
||||
): Promise<void> {
|
||||
const protocolPermissions = permissions.map(permission => {
|
||||
const protocolPermission =
|
||||
WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
|
||||
if (!protocolPermission) {
|
||||
throw new Error('Unknown permission: ' + permission);
|
||||
}
|
||||
return protocolPermission;
|
||||
});
|
||||
await this.#connection.send('Browser.grantPermissions', {
|
||||
origin,
|
||||
browserContextId: this.#id || undefined,
|
||||
permissions: protocolPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
override async clearPermissionOverrides(): Promise<void> {
|
||||
await this.#connection.send('Browser.resetPermissions', {
|
||||
browserContextId: this.#id || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
override newPage(): Promise<Page> {
|
||||
return this.#browser._createPageInContext(this.#id);
|
||||
}
|
||||
|
||||
override browser(): CdpBrowser {
|
||||
return this.#browser;
|
||||
}
|
||||
|
||||
override async close(): Promise<void> {
|
||||
assert(this.#id, 'Non-incognito profiles cannot be closed!');
|
||||
await this.#browser._disposeContext(this.#id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
|
||||
type Permission,
|
||||
} from '../api/Browser.js';
|
||||
import {BrowserContext} from '../api/BrowserContext.js';
|
||||
import type {Page} from '../api/Page.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
import type {CdpBrowser} from './Browser.js';
|
||||
import type {Connection} from './Connection.js';
|
||||
import type {CdpTarget} from './Target.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpBrowserContext extends BrowserContext {
|
||||
#connection: Connection;
|
||||
#browser: CdpBrowser;
|
||||
#id?: string;
|
||||
|
||||
constructor(connection: Connection, browser: CdpBrowser, contextId?: string) {
|
||||
super();
|
||||
this.#connection = connection;
|
||||
this.#browser = browser;
|
||||
this.#id = contextId;
|
||||
}
|
||||
|
||||
override get id(): string | undefined {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
override targets(): CdpTarget[] {
|
||||
return this.#browser.targets().filter(target => {
|
||||
return target.browserContext() === this;
|
||||
});
|
||||
}
|
||||
|
||||
override async pages(): Promise<Page[]> {
|
||||
const pages = await Promise.all(
|
||||
this.targets()
|
||||
.filter(target => {
|
||||
return (
|
||||
target.type() === 'page' ||
|
||||
(target.type() === 'other' &&
|
||||
this.#browser._getIsPageTargetCallback()?.(target))
|
||||
);
|
||||
})
|
||||
.map(target => {
|
||||
return target.page();
|
||||
})
|
||||
);
|
||||
return pages.filter((page): page is Page => {
|
||||
return !!page;
|
||||
});
|
||||
}
|
||||
|
||||
override isIncognito(): boolean {
|
||||
return !!this.#id;
|
||||
}
|
||||
|
||||
override async overridePermissions(
|
||||
origin: string,
|
||||
permissions: Permission[]
|
||||
): Promise<void> {
|
||||
const protocolPermissions = permissions.map(permission => {
|
||||
const protocolPermission =
|
||||
WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
|
||||
if (!protocolPermission) {
|
||||
throw new Error('Unknown permission: ' + permission);
|
||||
}
|
||||
return protocolPermission;
|
||||
});
|
||||
await this.#connection.send('Browser.grantPermissions', {
|
||||
origin,
|
||||
browserContextId: this.#id || undefined,
|
||||
permissions: protocolPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
override async clearPermissionOverrides(): Promise<void> {
|
||||
await this.#connection.send('Browser.resetPermissions', {
|
||||
browserContextId: this.#id || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
override newPage(): Promise<Page> {
|
||||
return this.#browser._createPageInContext(this.#id);
|
||||
}
|
||||
|
||||
override browser(): CdpBrowser {
|
||||
return this.#browser;
|
||||
}
|
||||
|
||||
override async close(): Promise<void> {
|
||||
assert(this.#id, 'Non-incognito profiles cannot be closed!');
|
||||
await this.#browser._disposeContext(this.#id);
|
||||
}
|
||||
}
|
||||
|
|
@ -206,6 +206,7 @@ export class CdpFrame extends Frame {
|
|||
options: {
|
||||
timeout?: number;
|
||||
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
|
||||
ignoreSameDocumentNavigation?: boolean;
|
||||
} = {}
|
||||
): Promise<HTTPResponse | null> {
|
||||
const {
|
||||
|
|
@ -220,14 +221,22 @@ export class CdpFrame extends Frame {
|
|||
);
|
||||
const error = await Deferred.race([
|
||||
watcher.terminationPromise(),
|
||||
watcher.sameDocumentNavigationPromise(),
|
||||
...(options.ignoreSameDocumentNavigation
|
||||
? []
|
||||
: [watcher.sameDocumentNavigationPromise()]),
|
||||
watcher.newDocumentNavigationPromise(),
|
||||
]);
|
||||
try {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return await watcher.navigationResponse();
|
||||
const result = await Deferred.race<
|
||||
Error | HTTPResponse | null | undefined
|
||||
>([watcher.terminationPromise(), watcher.navigationResponse()]);
|
||||
if (result instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
return result || null;
|
||||
} finally {
|
||||
watcher.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export class FrameTree<FrameType extends Frame> {
|
|||
// frameID -> childFrameIDs
|
||||
#childIds = new Map<string, Set<string>>();
|
||||
#mainFrame?: FrameType;
|
||||
#isMainFrameStale = false;
|
||||
#waitRequests = new Map<string, Set<Deferred<FrameType>>>();
|
||||
|
||||
getMainFrame(): FrameType | undefined {
|
||||
|
|
@ -59,8 +60,9 @@ export class FrameTree<FrameType extends Frame> {
|
|||
this.#childIds.set(frame._parentId, new Set());
|
||||
}
|
||||
this.#childIds.get(frame._parentId)!.add(frame._id);
|
||||
} else if (!this.#mainFrame) {
|
||||
} else if (!this.#mainFrame || this.#isMainFrameStale) {
|
||||
this.#mainFrame = frame;
|
||||
this.#isMainFrameStale = false;
|
||||
}
|
||||
this.#waitRequests.get(frame._id)?.forEach(request => {
|
||||
return request.resolve(frame);
|
||||
|
|
@ -73,7 +75,7 @@ export class FrameTree<FrameType extends Frame> {
|
|||
if (frame._parentId) {
|
||||
this.#childIds.get(frame._parentId)?.delete(frame._id);
|
||||
} else {
|
||||
this.#mainFrame = undefined;
|
||||
this.#isMainFrameStale = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,18 +9,14 @@ import type {CDPSession} from '../api/CDPSession.js';
|
|||
import type {Frame} from '../api/Frame.js';
|
||||
import {
|
||||
type ContinueRequestOverrides,
|
||||
type ErrorCode,
|
||||
headersArray,
|
||||
HTTPRequest,
|
||||
InterceptResolutionAction,
|
||||
type InterceptResolutionState,
|
||||
type ResourceType,
|
||||
type ResponseForRequest,
|
||||
STATUS_TEXTS,
|
||||
handleError,
|
||||
} from '../api/HTTPRequest.js';
|
||||
import type {ProtocolError} from '../common/Errors.js';
|
||||
import {debugError, isString} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
import type {CdpHTTPResponse} from './HTTPResponse.js';
|
||||
|
||||
|
|
@ -34,8 +30,7 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
|
||||
#client: CDPSession;
|
||||
#isNavigationRequest: boolean;
|
||||
#allowInterception: boolean;
|
||||
#interceptionHandled = false;
|
||||
|
||||
#url: string;
|
||||
#resourceType: ResourceType;
|
||||
|
||||
|
|
@ -44,13 +39,6 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
#postData?: string;
|
||||
#headers: Record<string, string> = {};
|
||||
#frame: Frame | null;
|
||||
#continueRequestOverrides: ContinueRequestOverrides;
|
||||
#responseForRequest: Partial<ResponseForRequest> | null = null;
|
||||
#abortErrorReason: Protocol.Network.ErrorReason | null = null;
|
||||
#interceptResolutionState: InterceptResolutionState = {
|
||||
action: InterceptResolutionAction.None,
|
||||
};
|
||||
#interceptHandlers: Array<() => void | PromiseLike<any>>;
|
||||
#initiator?: Protocol.Network.Initiator;
|
||||
|
||||
override get client(): CDPSession {
|
||||
|
|
@ -96,7 +84,6 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
this.#isNavigationRequest =
|
||||
data.requestId === data.loaderId && data.type === 'Document';
|
||||
this._interceptionId = interceptionId;
|
||||
this.#allowInterception = allowInterception;
|
||||
this.#url = data.request.url;
|
||||
this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
|
||||
this.#method = data.request.method;
|
||||
|
|
@ -104,10 +91,10 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
this.#hasPostData = data.request.hasPostData ?? false;
|
||||
this.#frame = frame;
|
||||
this._redirectChain = redirectChain;
|
||||
this.#continueRequestOverrides = {};
|
||||
this.#interceptHandlers = [];
|
||||
this.#initiator = data.initiator;
|
||||
|
||||
this.interception.enabled = allowInterception;
|
||||
|
||||
for (const [key, value] of Object.entries(data.request.headers)) {
|
||||
this.#headers[key.toLowerCase()] = value;
|
||||
}
|
||||
|
|
@ -117,59 +104,6 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
return this.#url;
|
||||
}
|
||||
|
||||
override continueRequestOverrides(): ContinueRequestOverrides {
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
return this.#continueRequestOverrides;
|
||||
}
|
||||
|
||||
override responseForRequest(): Partial<ResponseForRequest> | null {
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
return this.#responseForRequest;
|
||||
}
|
||||
|
||||
override abortErrorReason(): Protocol.Network.ErrorReason | null {
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
return this.#abortErrorReason;
|
||||
}
|
||||
|
||||
override interceptResolutionState(): InterceptResolutionState {
|
||||
if (!this.#allowInterception) {
|
||||
return {action: InterceptResolutionAction.Disabled};
|
||||
}
|
||||
if (this.#interceptionHandled) {
|
||||
return {action: InterceptResolutionAction.AlreadyHandled};
|
||||
}
|
||||
return {...this.#interceptResolutionState};
|
||||
}
|
||||
|
||||
override isInterceptResolutionHandled(): boolean {
|
||||
return this.#interceptionHandled;
|
||||
}
|
||||
|
||||
enqueueInterceptAction(
|
||||
pendingHandler: () => void | PromiseLike<unknown>
|
||||
): void {
|
||||
this.#interceptHandlers.push(pendingHandler);
|
||||
}
|
||||
|
||||
override async finalizeInterceptions(): Promise<void> {
|
||||
await this.#interceptHandlers.reduce((promiseChain, interceptAction) => {
|
||||
return promiseChain.then(interceptAction);
|
||||
}, Promise.resolve());
|
||||
const {action} = this.interceptResolutionState();
|
||||
switch (action) {
|
||||
case 'abort':
|
||||
return await this.#abort(this.#abortErrorReason);
|
||||
case 'respond':
|
||||
if (this.#responseForRequest === null) {
|
||||
throw new Error('Response is missing for the interception');
|
||||
}
|
||||
return await this.#respond(this.#responseForRequest);
|
||||
case 'continue':
|
||||
return await this.#continue(this.#continueRequestOverrides);
|
||||
}
|
||||
}
|
||||
|
||||
override resourceType(): ResourceType {
|
||||
return this.#resourceType;
|
||||
}
|
||||
|
|
@ -231,46 +165,12 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
};
|
||||
}
|
||||
|
||||
override async continue(
|
||||
overrides: ContinueRequestOverrides = {},
|
||||
priority?: number
|
||||
): Promise<void> {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this.#url.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this.#interceptionHandled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return await this.#continue(overrides);
|
||||
}
|
||||
this.#continueRequestOverrides = overrides;
|
||||
if (
|
||||
this.#interceptResolutionState.priority === undefined ||
|
||||
priority > this.#interceptResolutionState.priority
|
||||
) {
|
||||
this.#interceptResolutionState = {
|
||||
action: InterceptResolutionAction.Continue,
|
||||
priority,
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (priority === this.#interceptResolutionState.priority) {
|
||||
if (
|
||||
this.#interceptResolutionState.action === 'abort' ||
|
||||
this.#interceptResolutionState.action === 'respond'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.#interceptResolutionState.action =
|
||||
InterceptResolutionAction.Continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async #continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
|
||||
const {url, method, postData, headers} = overrides;
|
||||
this.#interceptionHandled = true;
|
||||
this.interception.handled = true;
|
||||
|
||||
const postDataBinaryBase64 = postData
|
||||
? Buffer.from(postData).toString('base64')
|
||||
|
|
@ -290,45 +190,13 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
headers: headers ? headersArray(headers) : undefined,
|
||||
})
|
||||
.catch(error => {
|
||||
this.#interceptionHandled = false;
|
||||
this.interception.handled = false;
|
||||
return handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
override async respond(
|
||||
response: Partial<ResponseForRequest>,
|
||||
priority?: number
|
||||
): Promise<void> {
|
||||
// Mocking responses for dataURL requests is not currently supported.
|
||||
if (this.#url.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this.#interceptionHandled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return await this.#respond(response);
|
||||
}
|
||||
this.#responseForRequest = response;
|
||||
if (
|
||||
this.#interceptResolutionState.priority === undefined ||
|
||||
priority > this.#interceptResolutionState.priority
|
||||
) {
|
||||
this.#interceptResolutionState = {
|
||||
action: InterceptResolutionAction.Respond,
|
||||
priority,
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (priority === this.#interceptResolutionState.priority) {
|
||||
if (this.#interceptResolutionState.action === 'abort') {
|
||||
return;
|
||||
}
|
||||
this.#interceptResolutionState.action = InterceptResolutionAction.Respond;
|
||||
}
|
||||
}
|
||||
|
||||
async #respond(response: Partial<ResponseForRequest>): Promise<void> {
|
||||
this.#interceptionHandled = true;
|
||||
async _respond(response: Partial<ResponseForRequest>): Promise<void> {
|
||||
this.interception.handled = true;
|
||||
|
||||
const responseBody: Buffer | null =
|
||||
response.body && isString(response.body)
|
||||
|
|
@ -371,43 +239,15 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
body: responseBody ? responseBody.toString('base64') : undefined,
|
||||
})
|
||||
.catch(error => {
|
||||
this.#interceptionHandled = false;
|
||||
this.interception.handled = false;
|
||||
return handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
override async abort(
|
||||
errorCode: ErrorCode = 'failed',
|
||||
priority?: number
|
||||
): Promise<void> {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this.#url.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
const errorReason = errorReasons[errorCode];
|
||||
assert(errorReason, 'Unknown error code: ' + errorCode);
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this.#interceptionHandled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return await this.#abort(errorReason);
|
||||
}
|
||||
this.#abortErrorReason = errorReason;
|
||||
if (
|
||||
this.#interceptResolutionState.priority === undefined ||
|
||||
priority >= this.#interceptResolutionState.priority
|
||||
) {
|
||||
this.#interceptResolutionState = {
|
||||
action: InterceptResolutionAction.Abort,
|
||||
priority,
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async #abort(
|
||||
async _abort(
|
||||
errorReason: Protocol.Network.ErrorReason | null
|
||||
): Promise<void> {
|
||||
this.#interceptionHandled = true;
|
||||
this.interception.handled = true;
|
||||
if (this._interceptionId === undefined) {
|
||||
throw new Error(
|
||||
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
|
||||
|
|
@ -421,30 +261,3 @@ export class CdpHTTPRequest extends HTTPRequest {
|
|||
.catch(handleError);
|
||||
}
|
||||
}
|
||||
|
||||
const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = {
|
||||
aborted: 'Aborted',
|
||||
accessdenied: 'AccessDenied',
|
||||
addressunreachable: 'AddressUnreachable',
|
||||
blockedbyclient: 'BlockedByClient',
|
||||
blockedbyresponse: 'BlockedByResponse',
|
||||
connectionaborted: 'ConnectionAborted',
|
||||
connectionclosed: 'ConnectionClosed',
|
||||
connectionfailed: 'ConnectionFailed',
|
||||
connectionrefused: 'ConnectionRefused',
|
||||
connectionreset: 'ConnectionReset',
|
||||
internetdisconnected: 'InternetDisconnected',
|
||||
namenotresolved: 'NameNotResolved',
|
||||
timedout: 'TimedOut',
|
||||
failed: 'Failed',
|
||||
} as const;
|
||||
|
||||
async function handleError(error: ProtocolError) {
|
||||
if (['Invalid header'].includes(error.originalMessage)) {
|
||||
throw error;
|
||||
}
|
||||
// In certain cases, protocol will return error if the request was
|
||||
// already canceled or the page was closed. We should tolerate these
|
||||
// errors.
|
||||
debugError(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {Protocol} from 'devtools-protocol';
|
|||
|
||||
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
|
||||
import type {Frame} from '../api/Frame.js';
|
||||
import type {Credentials} from '../api/Page.js';
|
||||
import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
|
||||
import {
|
||||
NetworkManagerEvent,
|
||||
|
|
@ -24,14 +25,6 @@ import {
|
|||
type FetchRequestId,
|
||||
} from './NetworkEventManager.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Credentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
|
@ -147,18 +140,16 @@ export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
|
|||
);
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(
|
||||
extraHTTPHeaders: Record<string, string>
|
||||
): Promise<void> {
|
||||
this.#extraHTTPHeaders = {};
|
||||
for (const key of Object.keys(extraHTTPHeaders)) {
|
||||
const value = extraHTTPHeaders[key];
|
||||
async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void> {
|
||||
const extraHTTPHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
assert(
|
||||
isString(value),
|
||||
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`
|
||||
);
|
||||
this.#extraHTTPHeaders[key.toLowerCase()] = value;
|
||||
extraHTTPHeaders[key.toLowerCase()] = value;
|
||||
}
|
||||
this.#extraHTTPHeaders = extraHTTPHeaders;
|
||||
|
||||
await this.#applyToAllClients(this.#applyExtraHTTPHeaders.bind(this));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type {Frame, WaitForOptions} from '../api/Frame.js';
|
|||
import type {HTTPRequest} from '../api/HTTPRequest.js';
|
||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||
import type {JSHandle} from '../api/JSHandle.js';
|
||||
import type {Credentials} from '../api/Page.js';
|
||||
import {
|
||||
Page,
|
||||
PageEvent,
|
||||
|
|
@ -71,7 +72,7 @@ import {FrameManagerEvent} from './FrameManagerEvents.js';
|
|||
import {CdpKeyboard, CdpMouse, CdpTouchscreen} from './Input.js';
|
||||
import {MAIN_WORLD} from './IsolatedWorlds.js';
|
||||
import {releaseObject} from './JSHandle.js';
|
||||
import type {Credentials, NetworkConditions} from './NetworkManager.js';
|
||||
import type {NetworkConditions} from './NetworkManager.js';
|
||||
import type {CdpTarget} from './Target.js';
|
||||
import type {TargetManager} from './TargetManager.js';
|
||||
import {TargetManagerEvent} from './TargetManager.js';
|
||||
|
|
@ -916,7 +917,10 @@ export class CdpPage extends Page {
|
|||
options?: WaitForOptions
|
||||
): Promise<HTTPResponse | null> {
|
||||
const [result] = await Promise.all([
|
||||
this.waitForNavigation(options),
|
||||
this.waitForNavigation({
|
||||
...options,
|
||||
ignoreSameDocumentNavigation: true,
|
||||
}),
|
||||
this.#primaryTargetClient.send('Page.reload'),
|
||||
]);
|
||||
|
||||
|
|
@ -1130,6 +1134,16 @@ export class CdpPage extends Page {
|
|||
await this.#emulationManager.setTransparentBackgroundColor();
|
||||
}
|
||||
|
||||
await firstValueFrom(
|
||||
from(
|
||||
this.mainFrame()
|
||||
.isolatedRealm()
|
||||
.evaluate(() => {
|
||||
return document.fonts.ready;
|
||||
})
|
||||
).pipe(raceWith(timeout(ms)))
|
||||
);
|
||||
|
||||
const printCommandPromise = this.#primaryTargetClient.send(
|
||||
'Page.printToPDF',
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,17 +31,14 @@ export class CallbackRegistry {
|
|||
} catch (error) {
|
||||
// We still throw sync errors synchronously and clean up the scheduled
|
||||
// callback.
|
||||
callback.promise
|
||||
.valueOrThrow()
|
||||
.catch(debugError)
|
||||
.finally(() => {
|
||||
this.#callbacks.delete(callback.id);
|
||||
});
|
||||
callback.promise.catch(debugError).finally(() => {
|
||||
this.#callbacks.delete(callback.id);
|
||||
});
|
||||
callback.reject(error as Error);
|
||||
throw error;
|
||||
}
|
||||
// Must only have sync code up until here.
|
||||
return callback.promise.valueOrThrow().finally(() => {
|
||||
return callback.promise.finally(() => {
|
||||
this.#callbacks.delete(callback.id);
|
||||
});
|
||||
}
|
||||
|
|
@ -148,8 +145,8 @@ export class Callback {
|
|||
return this.#id;
|
||||
}
|
||||
|
||||
get promise(): Deferred<unknown> {
|
||||
return this.#deferred;
|
||||
get promise(): Promise<unknown> {
|
||||
return this.#deferred.valueOrThrow();
|
||||
}
|
||||
|
||||
get error(): ProtocolError {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export interface Configuration {
|
|||
/**
|
||||
* Tells Puppeteer to not chrome-headless-shell download during installation.
|
||||
*
|
||||
* Can be overridden by `PUPPETEER_SKIP_CHROME_HEADLESSS_HELL_DOWNLOAD`.
|
||||
* Can be overridden by `PUPPETEER_SKIP_CHROME_HEADLESS_SHELL_DOWNLOAD`.
|
||||
*/
|
||||
skipChromeHeadlessShellDownload?: boolean;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ export class PuppeteerError extends Error {
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
constructor(message?: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export interface PDFOptions {
|
|||
headerTemplate?: string;
|
||||
/**
|
||||
* HTML template for the print footer. Has the same constraints and support
|
||||
* for special classes as {@link PDFOptions | PDFOptions.headerTemplate}.
|
||||
* for special classes as {@link PDFOptions.headerTemplate}.
|
||||
*/
|
||||
footerTemplate?: string;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -312,12 +312,12 @@ export function validateDialogType(
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function timeout(ms: number): Observable<never> {
|
||||
export function timeout(ms: number, cause?: Error): Observable<never> {
|
||||
return ms === 0
|
||||
? NEVER
|
||||
: timer(ms).pipe(
|
||||
map(() => {
|
||||
throw new TimeoutError(`Timed out after waiting ${ms}ms`);
|
||||
throw new TimeoutError(`Timed out after waiting ${ms}ms`, {cause});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,9 @@ export class ChromeLauncher extends ProductLauncher {
|
|||
removeMatchingFlags(options.args, '--disable-features');
|
||||
}
|
||||
|
||||
const turnOnExperimentalFeaturesForTesting =
|
||||
process.env['PUPPETEER_TEST_EXPERIMENTAL_CHROME_FEATURES'] === 'true';
|
||||
|
||||
// Merge default disabled features with user-provided ones, if any.
|
||||
const disabledFeatures = [
|
||||
'Translate',
|
||||
|
|
@ -174,9 +177,13 @@ export class ChromeLauncher extends ProductLauncher {
|
|||
'MediaRouter',
|
||||
'OptimizationHints',
|
||||
// https://crbug.com/1492053
|
||||
'ProcessPerSiteUpToMainFrameThreshold',
|
||||
turnOnExperimentalFeaturesForTesting
|
||||
? ''
|
||||
: 'ProcessPerSiteUpToMainFrameThreshold',
|
||||
...userDisabledFeatures,
|
||||
];
|
||||
].filter(feature => {
|
||||
return feature !== '';
|
||||
});
|
||||
|
||||
const userEnabledFeatures = getFeatures('--enable-features', options.args);
|
||||
if (options.args && userEnabledFeatures.length > 0) {
|
||||
|
|
@ -185,9 +192,11 @@ export class ChromeLauncher extends ProductLauncher {
|
|||
|
||||
// Merge default enabled features with user-provided ones, if any.
|
||||
const enabledFeatures = [
|
||||
'NetworkServiceInProcess2',
|
||||
// Add features to enable by default here.
|
||||
...userEnabledFeatures,
|
||||
];
|
||||
].filter(feature => {
|
||||
return feature !== '';
|
||||
});
|
||||
|
||||
const chromeArguments = [
|
||||
'--allow-pre-commit-input',
|
||||
|
|
@ -201,7 +210,9 @@ export class ChromeLauncher extends ProductLauncher {
|
|||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
'--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
|
||||
turnOnExperimentalFeaturesForTesting
|
||||
? ''
|
||||
: '--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
|
||||
'--disable-hang-monitor',
|
||||
'--disable-infobars',
|
||||
'--disable-ipc-flooding-protection',
|
||||
|
|
@ -220,7 +231,9 @@ export class ChromeLauncher extends ProductLauncher {
|
|||
'--use-mock-keychain',
|
||||
`--disable-features=${disabledFeatures.join(',')}`,
|
||||
`--enable-features=${enabledFeatures.join(',')}`,
|
||||
];
|
||||
].filter(arg => {
|
||||
return arg !== '';
|
||||
});
|
||||
const {
|
||||
devtools = false,
|
||||
headless = !devtools,
|
||||
|
|
|
|||
|
|
@ -41,14 +41,18 @@ export class NodeWebSocketTransport implements ConnectionTransport {
|
|||
constructor(ws: NodeWebSocket) {
|
||||
this.#ws = ws;
|
||||
this.#ws.addEventListener('message', event => {
|
||||
if (this.onmessage) {
|
||||
this.onmessage.call(null, event.data);
|
||||
}
|
||||
setImmediate(() => {
|
||||
if (this.onmessage) {
|
||||
this.onmessage.call(null, event.data);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.#ws.addEventListener('close', () => {
|
||||
if (this.onclose) {
|
||||
this.onclose.call(null);
|
||||
}
|
||||
setImmediate(() => {
|
||||
if (this.onclose) {
|
||||
this.onclose.call(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Silently ignore all errors - we don't know what to do with them.
|
||||
this.#ws.addEventListener('error', () => {});
|
||||
|
|
|
|||
|
|
@ -98,6 +98,12 @@ export abstract class ProductLauncher {
|
|||
|
||||
const launchArgs = await this.computeLaunchArguments(options);
|
||||
|
||||
if (!existsSync(launchArgs.executablePath)) {
|
||||
throw new Error(
|
||||
`Browser was not found at the configured executablePath (${launchArgs.executablePath})`
|
||||
);
|
||||
}
|
||||
|
||||
const usePipe = launchArgs.args.includes('--remote-debugging-pipe');
|
||||
|
||||
const onProcessExit = async () => {
|
||||
|
|
|
|||
|
|
@ -136,11 +136,11 @@ export class PuppeteerNode extends Puppeteer {
|
|||
* specified.
|
||||
*
|
||||
* When using with `puppeteer-core`,
|
||||
* {@link LaunchOptions | options.executablePath} or
|
||||
* {@link LaunchOptions | options.channel} must be provided.
|
||||
* {@link LaunchOptions.executablePath | options.executablePath} or
|
||||
* {@link LaunchOptions.channel | options.channel} must be provided.
|
||||
*
|
||||
* @example
|
||||
* You can use {@link LaunchOptions | options.ignoreDefaultArgs}
|
||||
* You can use {@link LaunchOptions.ignoreDefaultArgs | options.ignoreDefaultArgs}
|
||||
* to filter out `--mute-audio` from default arguments:
|
||||
*
|
||||
* ```ts
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* @internal
|
||||
*/
|
||||
export const PUPPETEER_REVISIONS = Object.freeze({
|
||||
chrome: '122.0.6261.94',
|
||||
'chrome-headless-shell': '122.0.6261.94',
|
||||
chrome: '123.0.6312.122',
|
||||
'chrome-headless-shell': '123.0.6312.122',
|
||||
firefox: 'latest',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const interpolateFunction = <T extends (...args: never[]) => unknown>(
|
|||
for (const [name, jsValue] of Object.entries(replacements)) {
|
||||
value = value.replace(
|
||||
new RegExp(`PLACEHOLDER\\(\\s*(?:'${name}'|"${name}")\\s*\\)`, 'g'),
|
||||
// Wrapping this ensures tersers that accidently inline PLACEHOLDER calls
|
||||
// Wrapping this ensures tersers that accidentally inline PLACEHOLDER calls
|
||||
// are still valid. Without, we may get calls like ()=>{...}() which is
|
||||
// not valid.
|
||||
`(${jsValue})`
|
||||
|
|
|
|||
|
|
@ -29,6 +29,121 @@ All notable changes to this project will be documented in this file. See [standa
|
|||
* puppeteer-core bumped from 21.0.2 to 21.0.3
|
||||
* @puppeteer/browsers bumped from 1.5.1 to 1.6.0
|
||||
|
||||
## [22.6.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.6.4...puppeteer-v22.6.5) (2024-04-15)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.6.4 to 22.6.5
|
||||
* @puppeteer/browsers bumped from 2.2.1 to 2.2.2
|
||||
|
||||
## [22.6.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.6.3...puppeteer-v22.6.4) (2024-04-11)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.6.3 to 22.6.4
|
||||
|
||||
## [22.6.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.6.2...puppeteer-v22.6.3) (2024-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* deprecate configuration via package.json ([#12176](https://github.com/puppeteer/puppeteer/issues/12176)) ([c96c762](https://github.com/puppeteer/puppeteer/commit/c96c7623bc2258ba7419812333ec42cdf83bf432))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.6.2 to 22.6.3
|
||||
* @puppeteer/browsers bumped from 2.2.0 to 2.2.1
|
||||
|
||||
## [22.6.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.6.1...puppeteer-v22.6.2) (2024-03-28)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.6.1 to 22.6.2
|
||||
|
||||
## [22.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.6.0...puppeteer-v22.6.1) (2024-03-25)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.6.0 to 22.6.1
|
||||
|
||||
## [22.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.5.0...puppeteer-v22.6.0) (2024-03-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* roll to Chrome 123.0.6312.58 (r1262506) ([#12110](https://github.com/puppeteer/puppeteer/issues/12110)) ([6f5b3bc](https://github.com/puppeteer/puppeteer/commit/6f5b3bc9b88c6d3204dda396f8963591ea6eb883))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.5.0 to 22.6.0
|
||||
|
||||
## [22.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.4.1...puppeteer-v22.5.0) (2024-03-15)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.4.1 to 22.5.0
|
||||
* @puppeteer/browsers bumped from 2.1.0 to 2.2.0
|
||||
|
||||
## [22.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.4.0...puppeteer-v22.4.1) (2024-03-08)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.4.0 to 22.4.1
|
||||
|
||||
## [22.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.3.0...puppeteer-v22.4.0) (2024-03-05)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "puppeteer",
|
||||
"version": "22.4.0",
|
||||
"version": "22.6.5",
|
||||
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
||||
"keywords": [
|
||||
"puppeteer",
|
||||
|
|
@ -124,8 +124,9 @@
|
|||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"cosmiconfig": "9.0.0",
|
||||
"puppeteer-core": "22.4.0",
|
||||
"@puppeteer/browsers": "2.1.0"
|
||||
"puppeteer-core": "22.6.5",
|
||||
"@puppeteer/browsers": "2.2.2",
|
||||
"devtools-protocol": "0.0.1262051"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.17.15"
|
||||
|
|
|
|||
|
|
@ -127,6 +127,17 @@ export const getConfiguration = (): Configuration => {
|
|||
downloadHost;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(process.env).some(key => {
|
||||
return key.startsWith('npm_package_config_puppeteer_');
|
||||
}) &&
|
||||
configuration.logLevel === 'warn'
|
||||
) {
|
||||
console.warn(
|
||||
`Configuring Puppeteer via npm/package.json is deprecated. Use https://pptr.dev/guides/configuration instead.`
|
||||
);
|
||||
}
|
||||
|
||||
configuration.cacheDirectory =
|
||||
process.env['PUPPETEER_CACHE_DIR'] ??
|
||||
process.env['npm_config_puppeteer_cache_dir'] ??
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ module.exports = {
|
|||
selector:
|
||||
'CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflake"], CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflakeOnly"]',
|
||||
},
|
||||
{
|
||||
message:
|
||||
'No `expect` in EventHandler. They will never throw errors',
|
||||
selector:
|
||||
'CallExpression[callee.property.name="on"] BlockStatement > :not(TryStatement) > ExpressionStatement > CallExpression[callee.object.callee.name="expect"]',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -45,7 +45,7 @@
|
|||
{
|
||||
"id": "chrome-bidi",
|
||||
"platforms": ["linux"],
|
||||
"parameters": ["chrome", "chrome-headless-shell", "webDriverBiDi"],
|
||||
"parameters": ["chrome", "headless", "webDriverBiDi"],
|
||||
"expectedLineCoverage": 56
|
||||
}
|
||||
],
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 104 B |
Binary file not shown.
|
After Width: | Height: | Size: 104 B |
|
|
@ -13,7 +13,11 @@ import puppeteer from 'puppeteer-core';
|
|||
executablePath: 'node',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('Failed to launch the browser process')) {
|
||||
if (
|
||||
error.message.includes(
|
||||
'Browser was not found at the configured executablePath (node)'
|
||||
)
|
||||
) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
"assets"
|
||||
],
|
||||
"dependencies": {
|
||||
"glob": "10.3.10",
|
||||
"mocha": "10.3.0"
|
||||
"glob": "10.3.12",
|
||||
"mocha": "10.4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import {spawnSync} from 'child_process';
|
||||
import {existsSync} from 'fs';
|
||||
import {readdir} from 'fs/promises';
|
||||
import {platform} from 'os';
|
||||
import {join} from 'path';
|
||||
|
|
@ -49,3 +51,36 @@ import {readAsset} from './util.js';
|
|||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe('Firefox download', () => {
|
||||
configureSandbox({
|
||||
dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
|
||||
env: cwd => {
|
||||
return {
|
||||
PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
|
||||
PUPPETEER_SKIP_DOWNLOAD: 'true',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
it('can download Firefox stable', async function () {
|
||||
assert.ok(!existsSync(join(this.sandbox, '.cache', 'puppeteer')));
|
||||
const result = spawnSync(
|
||||
'npx',
|
||||
['puppeteer', 'browsers', 'install', 'firefox@stable'],
|
||||
{
|
||||
// npx is not found without the shell flag on Windows.
|
||||
shell: process.platform === 'win32',
|
||||
cwd: this.sandbox,
|
||||
env: {
|
||||
...process.env,
|
||||
PUPPETEER_CACHE_DIR: join(this.sandbox, '.cache', 'puppeteer'),
|
||||
},
|
||||
}
|
||||
);
|
||||
assert.strictEqual(result.status, 0);
|
||||
const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
|
||||
assert.equal(files.length, 1);
|
||||
assert.equal(files[0], 'firefox');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -697,20 +697,13 @@ describe('AriaQueryHandler', () => {
|
|||
ElementHandle<HTMLButtonElement>
|
||||
>;
|
||||
const ids = await getIds(found);
|
||||
expect(ids).toEqual([
|
||||
'node5',
|
||||
'node6',
|
||||
'node7',
|
||||
'node8',
|
||||
'node10',
|
||||
'node21',
|
||||
]);
|
||||
expect(ids).toEqual(['node5', 'node6', 'node8', 'node10', 'node21']);
|
||||
});
|
||||
it('should find by role "heading"', async () => {
|
||||
const {page} = await setupPage();
|
||||
const found = await page.$$('aria/[role="heading"]');
|
||||
const ids = await getIds(found);
|
||||
expect(ids).toEqual(['shown', 'hidden', 'node11', 'node13']);
|
||||
expect(ids).toEqual(['shown', 'node11', 'node13']);
|
||||
});
|
||||
it('should find both ignored and unignored', async () => {
|
||||
const {page} = await setupPage();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import expect from 'expect';
|
||||
|
||||
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
||||
import {getTestState, launch, setupTestBrowserHooks} from './mocha-utils.js';
|
||||
|
||||
describe('Browser specs', function () {
|
||||
setupTestBrowserHooks();
|
||||
|
|
@ -64,6 +64,23 @@ describe('Browser specs', function () {
|
|||
expect(remoteBrowser.process()).toBe(null);
|
||||
await remoteBrowser.disconnect();
|
||||
});
|
||||
it('should keep connected after the last page is closed', async () => {
|
||||
const {browser, close} = await launch({}, {createContext: false});
|
||||
try {
|
||||
const pages = await browser.pages();
|
||||
await Promise.all(
|
||||
pages.map(page => {
|
||||
return page.close();
|
||||
})
|
||||
);
|
||||
// Verify the browser is still connected.
|
||||
expect(browser.connected).toBe(true);
|
||||
// Verify the browser can open a new page.
|
||||
await browser.newPage();
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Browser.isConnected', () => {
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ describe('Target.createCDPSession', function () {
|
|||
}
|
||||
)
|
||||
).rejects.toThrowError(
|
||||
`Runtime.evaluate timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.`
|
||||
/Increase the 'protocolTimeout' setting in launch\/connect calls for a higher timeout if needed./gi
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ describe('DevTools', function () {
|
|||
return 2 * 3;
|
||||
})
|
||||
).toBe(6);
|
||||
expect(await browser.pages()).toContainEqual(page);
|
||||
expect(await browser.pages()).toContain(page);
|
||||
});
|
||||
it('target.page() should return a DevTools page if asPage is used', async function () {
|
||||
it('target.page() should return Page when calling asPage on DevTools target', async function () {
|
||||
const {puppeteer} = await getTestState({skipLaunch: true});
|
||||
const originalBrowser = await launchBrowser(launchOptions);
|
||||
|
||||
|
|
@ -87,7 +87,8 @@ describe('DevTools', function () {
|
|||
return 2 * 3;
|
||||
})
|
||||
).toBe(6);
|
||||
expect(await browser.pages()).toContainEqual(page);
|
||||
// The page won't be part of browser.pages() if a custom isPageTarget is not provided
|
||||
expect(await browser.pages()).not.toContain(page);
|
||||
});
|
||||
it('should open devtools when "devtools: true" option is given', async () => {
|
||||
const browser = await launchBrowser(
|
||||
|
|
|
|||
|
|
@ -385,8 +385,15 @@ describe('ElementHandle specs', function () {
|
|||
await page.setContent(
|
||||
`<iframe name='frame' style='position: absolute; left: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
|
||||
);
|
||||
const frame = await page.waitForFrame(frame => {
|
||||
return frame.name() === 'frame';
|
||||
const frame = await page.waitForFrame(async frame => {
|
||||
using element = await frame.frameElement();
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const name = await element.evaluate(frame => {
|
||||
return frame.name;
|
||||
});
|
||||
return name === 'frame';
|
||||
});
|
||||
|
||||
using handle = await frame.locator('button').waitHandle();
|
||||
|
|
@ -395,8 +402,15 @@ describe('ElementHandle specs', function () {
|
|||
await page.setContent(
|
||||
`<iframe name='frame2' style='position: absolute; top: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
|
||||
);
|
||||
const frame2 = await page.waitForFrame(frame => {
|
||||
return frame.name() === 'frame2';
|
||||
const frame2 = await page.waitForFrame(async frame => {
|
||||
using element = await frame.frameElement();
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const name = await element.evaluate(frame => {
|
||||
return frame.name;
|
||||
});
|
||||
return name === 'frame2';
|
||||
});
|
||||
|
||||
using handle2 = await frame2.locator('button').waitHandle();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import expect from 'expect';
|
||||
import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
|
||||
import type {Frame} from 'puppeteer-core/internal/api/Frame.js';
|
||||
import {assert} from 'puppeteer-core/internal/util/assert.js';
|
||||
|
||||
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
||||
import {
|
||||
|
|
@ -78,7 +79,7 @@ describe('Frame specs', function () {
|
|||
const {page, server} = await getTestState();
|
||||
|
||||
await page.goto(server.PREFIX + '/frames/nested-frames.html');
|
||||
expect(dumpFrames(page.mainFrame())).toEqual([
|
||||
expect(await dumpFrames(page.mainFrame())).toEqual([
|
||||
'http://localhost:<PORT>/frames/nested-frames.html',
|
||||
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
||||
' http://localhost:<PORT>/frames/frame.html (uno)',
|
||||
|
|
@ -232,23 +233,6 @@ describe('Frame specs', function () {
|
|||
expect(page.frames()).toHaveLength(2);
|
||||
expect(page.frames()[1]!.url()).toBe(server.EMPTY_PAGE);
|
||||
});
|
||||
it('should report frame.name()', async () => {
|
||||
const {page, server} = await getTestState();
|
||||
|
||||
await attachFrame(page, 'theFrameId', server.EMPTY_PAGE);
|
||||
await page.evaluate((url: string) => {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.name = 'theFrameName';
|
||||
frame.src = url;
|
||||
document.body.appendChild(frame);
|
||||
return new Promise(x => {
|
||||
return (frame.onload = x);
|
||||
});
|
||||
}, server.EMPTY_PAGE);
|
||||
expect(page.frames()[0]!.name()).toBe('');
|
||||
expect(page.frames()[1]!.name()).toBe('theFrameId');
|
||||
expect(page.frames()[2]!.name()).toBe('theFrameName');
|
||||
});
|
||||
it('should report frame.parent()', async () => {
|
||||
const {page, server} = await getTestState();
|
||||
|
||||
|
|
@ -306,4 +290,35 @@ describe('Frame specs', function () {
|
|||
expect(page.mainFrame().client).toBeInstanceOf(CDPSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Frame.prototype.frameElement', function () {
|
||||
it('should work', async () => {
|
||||
const {page, server} = await getTestState();
|
||||
|
||||
await attachFrame(page, 'theFrameId', server.EMPTY_PAGE);
|
||||
await page.evaluate((url: string) => {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.name = 'theFrameName';
|
||||
frame.src = url;
|
||||
document.body.appendChild(frame);
|
||||
return new Promise(x => {
|
||||
return (frame.onload = x);
|
||||
});
|
||||
}, server.EMPTY_PAGE);
|
||||
using frame0 = await page.frames()[0]?.frameElement();
|
||||
assert(!frame0);
|
||||
using frame1 = await page.frames()[1]?.frameElement();
|
||||
assert(frame1);
|
||||
using frame2 = await page.frames()[2]?.frameElement();
|
||||
assert(frame2);
|
||||
const name1 = await frame1.evaluate(frame => {
|
||||
return frame.id;
|
||||
});
|
||||
expect(name1).toBe('theFrameId');
|
||||
const name2 = await frame2.evaluate(frame => {
|
||||
return frame.name;
|
||||
});
|
||||
expect(name2).toBe('theFrameName');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -326,6 +326,16 @@ describe('JSHandle', function () {
|
|||
'JSHandle@proxy'
|
||||
);
|
||||
});
|
||||
it('should work with window subtypes', async () => {
|
||||
const {page} = await getTestState();
|
||||
|
||||
expect((await page.evaluateHandle('window')).toString()).toBe(
|
||||
'JSHandle@window'
|
||||
);
|
||||
expect((await page.evaluateHandle('globalThis')).toString()).toBe(
|
||||
'JSHandle@window'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSHandle[Symbol.dispose]', () => {
|
||||
|
|
|
|||
|
|
@ -186,8 +186,15 @@ describe('Keyboard', function () {
|
|||
await page.setContent(`
|
||||
<iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"</iframe>
|
||||
`);
|
||||
const frame = await page.waitForFrame(frame => {
|
||||
return frame.name() === 'test';
|
||||
const frame = await page.waitForFrame(async frame => {
|
||||
using element = await frame.frameElement();
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const name = await element.evaluate(frame => {
|
||||
return frame.name;
|
||||
});
|
||||
return name === 'test';
|
||||
});
|
||||
await frame.focus('textarea');
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,30 @@ describe('Launcher specs', function () {
|
|||
const {close} = await launch({});
|
||||
await close();
|
||||
});
|
||||
|
||||
it('can launch multiple instances without node warnings', async () => {
|
||||
const instances = [];
|
||||
let warning = null;
|
||||
const warningHandler: NodeJS.WarningListener = w => {
|
||||
return (warning = w);
|
||||
};
|
||||
process.on('warning', warningHandler);
|
||||
process.setMaxListeners(1);
|
||||
try {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
instances.push(launch({}));
|
||||
}
|
||||
await Promise.all(
|
||||
(await Promise.all(instances)).map(instance => {
|
||||
return instance.close();
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
process.setMaxListeners(10);
|
||||
}
|
||||
process.off('warning', warningHandler);
|
||||
expect(warning).toBe(null);
|
||||
});
|
||||
it('should have default url when launching browser', async function () {
|
||||
const {browser, close} = await launch({}, {createContext: false});
|
||||
try {
|
||||
|
|
@ -166,7 +190,9 @@ describe('Launcher specs', function () {
|
|||
}).catch(error => {
|
||||
return (waitError = error);
|
||||
});
|
||||
expect(waitError.message).toContain('Failed to launch');
|
||||
expect(waitError.message).toBe(
|
||||
'Browser was not found at the configured executablePath (random-invalid-path)'
|
||||
);
|
||||
});
|
||||
it('userDataDir option', async () => {
|
||||
const userDataDir = await mkdtemp(TMP_FOLDER);
|
||||
|
|
@ -591,6 +617,20 @@ describe('Launcher specs', function () {
|
|||
});
|
||||
expect(error.message).toContain('either pipe or debugging port');
|
||||
});
|
||||
|
||||
it('throws an error if executable path is not valid with pipe=true', async () => {
|
||||
const options = {
|
||||
executablePath: '/tmp/does-not-exist',
|
||||
pipe: true,
|
||||
};
|
||||
let error!: Error;
|
||||
await launch(options).catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
expect(error.message).toContain(
|
||||
'Browser was not found at the configured executablePath (/tmp/does-not-exist)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Puppeteer.launch', function () {
|
||||
|
|
@ -793,7 +833,7 @@ describe('Launcher specs', function () {
|
|||
const restoredPage = pages.find(page => {
|
||||
return page.url() === server.PREFIX + '/frames/nested-frames.html';
|
||||
})!;
|
||||
expect(dumpFrames(restoredPage.mainFrame())).toEqual([
|
||||
expect(await dumpFrames(restoredPage.mainFrame())).toEqual([
|
||||
'http://localhost:<PORT>/frames/nested-frames.html',
|
||||
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
||||
' http://localhost:<PORT>/frames/frame.html (uno)',
|
||||
|
|
|
|||
|
|
@ -112,6 +112,27 @@ describe('navigation', function () {
|
|||
const response = await page.goto(server.PREFIX + '/grid.html');
|
||||
expect(response!.status()).toBe(200);
|
||||
});
|
||||
it('should work when reload causes history API in beforeunload', async () => {
|
||||
const {page, server} = await getTestState();
|
||||
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.evaluate(() => {
|
||||
window.addEventListener(
|
||||
'beforeunload',
|
||||
() => {
|
||||
return history.replaceState(null, 'initial', window.location.href);
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
await page.reload();
|
||||
// Evaluate still works.
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return 1;
|
||||
})
|
||||
).toBe(1);
|
||||
});
|
||||
it('should navigate to empty page with networkidle0', async () => {
|
||||
const {page, server} = await getTestState();
|
||||
|
||||
|
|
|
|||
|
|
@ -722,7 +722,7 @@ describe('network', function () {
|
|||
} catch (error) {
|
||||
// In headful, an error is thrown instead of 401.
|
||||
if (
|
||||
!(error as Error).message.startsWith(
|
||||
!(error as Error).message?.includes(
|
||||
'net::ERR_INVALID_AUTH_CREDENTIALS'
|
||||
)
|
||||
) {
|
||||
|
|
@ -772,7 +772,7 @@ describe('network', function () {
|
|||
} catch (error) {
|
||||
// In headful, an error is thrown instead of 401.
|
||||
if (
|
||||
!(error as Error).message.startsWith(
|
||||
!(error as Error).message?.includes(
|
||||
'net::ERR_INVALID_AUTH_CREDENTIALS'
|
||||
)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -506,11 +506,14 @@ describe('Page', function () {
|
|||
console.log(1, 2, 3, globalThis);
|
||||
});
|
||||
const log = await logPromise;
|
||||
expect(log.text()).toBe('1 2 3 JSHandle@object');
|
||||
|
||||
expect(log.text()).atLeastOneToContain([
|
||||
'1 2 3 JSHandle@object',
|
||||
'1 2 3 JSHandle@window',
|
||||
]);
|
||||
expect(log.args()).toHaveLength(4);
|
||||
expect(await (await log.args()[3]!.getProperty('test')).jsonValue()).toBe(
|
||||
1
|
||||
);
|
||||
using property = await log.args()[3]!.getProperty('test');
|
||||
expect(await property.jsonValue()).toBe(1);
|
||||
});
|
||||
it('should trigger correct Log', async () => {
|
||||
const {page, server, isChrome} = await getTestState();
|
||||
|
|
@ -1210,13 +1213,15 @@ describe('Page', function () {
|
|||
expect(result).toBe(36);
|
||||
await page.removeExposedFunction('compute');
|
||||
|
||||
let error: Error | null = null;
|
||||
await page
|
||||
const error = await page
|
||||
.evaluate(async function () {
|
||||
return (globalThis as any).compute(9, 4);
|
||||
})
|
||||
.catch(_error => {
|
||||
return (error = _error);
|
||||
.then(() => {
|
||||
return null;
|
||||
})
|
||||
.catch(error => {
|
||||
return error;
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ describe('cooperative request interception', function () {
|
|||
|
||||
describe('Page.setRequestInterception', function () {
|
||||
const expectedActions: ActionResult[] = ['abort', 'continue', 'respond'];
|
||||
while (expectedActions.length > 0) {
|
||||
const expectedAction = expectedActions.pop();
|
||||
for (const expectedAction of expectedActions) {
|
||||
it(`should cooperatively ${expectedAction} by priority`, async () => {
|
||||
const {page, server} = await getTestState();
|
||||
|
||||
|
|
@ -94,24 +93,36 @@ describe('cooperative request interception', function () {
|
|||
const {page, server} = await getTestState();
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
let requestError;
|
||||
page.on('request', request => {
|
||||
if (isFavicon(request)) {
|
||||
void request.continue({}, 0);
|
||||
return;
|
||||
}
|
||||
expect(request.url()).toContain('empty.html');
|
||||
expect(request.headers()['user-agent']).toBeTruthy();
|
||||
expect(request.method()).toBe('GET');
|
||||
expect(request.postData()).toBe(undefined);
|
||||
expect(request.isNavigationRequest()).toBe(true);
|
||||
expect(request.resourceType()).toBe('document');
|
||||
expect(request.frame() === page.mainFrame()).toBe(true);
|
||||
expect(request.frame()!.url()).toBe('about:blank');
|
||||
void request.continue({}, 0);
|
||||
try {
|
||||
expect(request).toBeTruthy();
|
||||
expect(request.url()).toContain('empty.html');
|
||||
expect(request.headers()['user-agent']).toBeTruthy();
|
||||
expect(request.method()).toBe('GET');
|
||||
expect(request.postData()).toBe(undefined);
|
||||
expect(request.isNavigationRequest()).toBe(true);
|
||||
expect(request.resourceType()).toBe('document');
|
||||
expect(request.frame()!.url()).toBe('about:blank');
|
||||
expect(request.frame() === page.mainFrame()).toBe(true);
|
||||
} catch (error) {
|
||||
requestError = error;
|
||||
} finally {
|
||||
void request.continue({}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||
expect(response!.ok()).toBe(true);
|
||||
expect(response!.remoteAddress().port).toBe(server.PORT);
|
||||
if (requestError) {
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.remoteAddress().port).toBe(server.PORT);
|
||||
});
|
||||
// @see https://github.com/puppeteer/puppeteer/pull/3105
|
||||
it('should work when POST is redirected with 302', async () => {
|
||||
|
|
@ -141,16 +152,24 @@ describe('cooperative request interception', function () {
|
|||
|
||||
server.setRedirect('/rrredirect', '/empty.html');
|
||||
await page.setRequestInterception(true);
|
||||
let requestError;
|
||||
page.on('request', request => {
|
||||
const headers = Object.assign({}, request.headers(), {
|
||||
foo: 'bar',
|
||||
});
|
||||
void request.continue({headers}, 0);
|
||||
|
||||
expect(request.continueRequestOverrides()).toEqual({headers});
|
||||
try {
|
||||
expect(request.continueRequestOverrides()).toEqual({headers});
|
||||
} catch (error) {
|
||||
requestError = error;
|
||||
}
|
||||
});
|
||||
// Make sure that the goto does not time out.
|
||||
await page.goto(server.PREFIX + '/rrredirect');
|
||||
|
||||
if (requestError) {
|
||||
throw requestError;
|
||||
}
|
||||
});
|
||||
// @see https://github.com/puppeteer/puppeteer/issues/4743
|
||||
it('should be able to remove headers', async () => {
|
||||
|
|
@ -220,11 +239,20 @@ describe('cooperative request interception', function () {
|
|||
foo: 'bar',
|
||||
});
|
||||
await page.setRequestInterception(true);
|
||||
let requestError;
|
||||
page.on('request', request => {
|
||||
expect(request.headers()['foo']).toBe('bar');
|
||||
void request.continue({}, 0);
|
||||
try {
|
||||
expect(request.headers()['foo']).toBe('bar');
|
||||
} catch (error) {
|
||||
requestError = error;
|
||||
} finally {
|
||||
void request.continue({}, 0);
|
||||
}
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
if (requestError) {
|
||||
throw requestError;
|
||||
}
|
||||
expect(response!.ok()).toBe(true);
|
||||
});
|
||||
// @see https://github.com/puppeteer/puppeteer/issues/4337
|
||||
|
|
@ -250,11 +278,20 @@ describe('cooperative request interception', function () {
|
|||
|
||||
await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE});
|
||||
await page.setRequestInterception(true);
|
||||
let requestError;
|
||||
page.on('request', request => {
|
||||
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
||||
void request.continue({}, 0);
|
||||
try {
|
||||
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
||||
} catch (error) {
|
||||
requestError = error;
|
||||
} finally {
|
||||
void request.continue({}, 0);
|
||||
}
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
if (requestError) {
|
||||
throw requestError;
|
||||
}
|
||||
expect(response!.ok()).toBe(true);
|
||||
});
|
||||
it('should be abortable', async () => {
|
||||
|
|
@ -340,7 +377,7 @@ describe('cooperative request interception', function () {
|
|||
if (isChrome) {
|
||||
expect(error.message).toContain('net::ERR_FAILED');
|
||||
} else {
|
||||
expect(error.message).toContain('NS_ERROR_FAILURE');
|
||||
expect(error.message).toContain('NS_ERROR_ABORT');
|
||||
}
|
||||
});
|
||||
it('should work with redirects', async () => {
|
||||
|
|
@ -947,14 +984,26 @@ describe('cooperative request interception', function () {
|
|||
page.on('request', request => {
|
||||
void request.continue();
|
||||
});
|
||||
let requestError;
|
||||
page.on('request', request => {
|
||||
expect(request.isInterceptResolutionHandled()).toBeTruthy();
|
||||
try {
|
||||
expect(request.isInterceptResolutionHandled()).toBeTruthy();
|
||||
} catch (error) {
|
||||
requestError = error;
|
||||
}
|
||||
});
|
||||
page.on('request', request => {
|
||||
const {action} = request.interceptResolutionState();
|
||||
expect(action).toBe(InterceptResolutionAction.AlreadyHandled);
|
||||
try {
|
||||
expect(action).toBe(InterceptResolutionAction.AlreadyHandled);
|
||||
} catch (error) {
|
||||
requestError = error;
|
||||
}
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
if (requestError) {
|
||||
throw requestError;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,23 +22,34 @@ describe('request interception', function () {
|
|||
const {page, server} = await getTestState();
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
let requestError;
|
||||
page.on('request', request => {
|
||||
if (isFavicon(request)) {
|
||||
void request.continue();
|
||||
return;
|
||||
}
|
||||
expect(request.url()).toContain('empty.html');
|
||||
expect(request.headers()['user-agent']).toBeTruthy();
|
||||
expect(request.headers()['accept']).toBeTruthy();
|
||||
expect(request.method()).toBe('GET');
|
||||
expect(request.postData()).toBe(undefined);
|
||||
expect(request.isNavigationRequest()).toBe(true);
|
||||
expect(request.resourceType()).toBe('document');
|
||||
expect(request.frame() === page.mainFrame()).toBe(true);
|
||||
expect(request.frame()!.url()).toBe('about:blank');
|
||||
void request.continue();
|
||||
try {
|
||||
expect(request).toBeTruthy();
|
||||
expect(request.url()).toContain('empty.html');
|
||||
expect(request.headers()['user-agent']).toBeTruthy();
|
||||
expect(request.method()).toBe('GET');
|
||||
expect(request.postData()).toBe(undefined);
|
||||
expect(request.isNavigationRequest()).toBe(true);
|
||||
expect(request.resourceType()).toBe('document');
|
||||
expect(request.frame()!.url()).toBe('about:blank');
|
||||
expect(request.frame() === page.mainFrame()).toBe(true);
|
||||
} catch (error) {
|
||||
requestError = error;
|
||||
} finally {
|
||||
void request.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||
if (requestError) {
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.remoteAddress().port).toBe(server.PORT);
|
||||
});
|
||||
|
|
@ -76,7 +87,11 @@ describe('request interception', function () {
|
|||
});
|
||||
void request.continue({headers});
|
||||
});
|
||||
await page.goto(server.PREFIX + '/rrredirect');
|
||||
const [request] = await Promise.all([
|
||||
server.waitForRequest('/empty.html'),
|
||||
page.goto(server.PREFIX + '/rrredirect'),
|
||||
]);
|
||||
expect(request.headers['foo']).toBe('bar');
|
||||
});
|
||||
// @see https://github.com/puppeteer/puppeteer/issues/4743
|
||||
it('should be able to remove headers', async () => {
|
||||
|
|
@ -162,11 +177,21 @@ describe('request interception', function () {
|
|||
foo: 'bar',
|
||||
});
|
||||
await page.setRequestInterception(true);
|
||||
let requestError;
|
||||
page.on('request', request => {
|
||||
expect(request.headers()['foo']).toBe('bar');
|
||||
void request.continue();
|
||||
try {
|
||||
expect(request.headers()['foo']).toBe('bar');
|
||||
} catch (error) {
|
||||
requestError = error;
|
||||
} finally {
|
||||
void request.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||
if (requestError) {
|
||||
throw requestError;
|
||||
}
|
||||
expect(response.ok()).toBe(true);
|
||||
});
|
||||
// @see https://github.com/puppeteer/puppeteer/issues/4337
|
||||
|
|
@ -192,11 +217,13 @@ describe('request interception', function () {
|
|||
|
||||
await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE});
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
||||
let request!: HTTPRequest;
|
||||
page.on('request', req => {
|
||||
request = req;
|
||||
void request.continue();
|
||||
});
|
||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
||||
expect(response.ok()).toBe(true);
|
||||
});
|
||||
it('should be abortable', async () => {
|
||||
|
|
@ -267,7 +294,7 @@ describe('request interception', function () {
|
|||
if (isChrome) {
|
||||
expect(error.message).toContain('net::ERR_FAILED');
|
||||
} else {
|
||||
expect(error.message).toContain('NS_ERROR_FAILURE');
|
||||
expect(error.message).toContain('NS_ERROR_ABORT');
|
||||
}
|
||||
});
|
||||
it('should work with redirects', async () => {
|
||||
|
|
@ -493,7 +520,7 @@ describe('request interception', function () {
|
|||
))!;
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
it('should work wit h encoded server - 2', async () => {
|
||||
it('should work with encoded server - 2', async () => {
|
||||
const {page, server} = await getTestState();
|
||||
|
||||
// The requestWillBeSent will report URL as-is, whereas interception will
|
||||
|
|
|
|||
|
|
@ -393,6 +393,33 @@ describe('Screenshots', function () {
|
|||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it('should use element clip', async () => {
|
||||
const {page} = await getTestState();
|
||||
|
||||
await page.setViewport({width: 500, height: 500});
|
||||
await page.setContent(`
|
||||
something above
|
||||
<style>div {
|
||||
border: 2px solid blue;
|
||||
background: green;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
<div></div>
|
||||
`);
|
||||
using elementHandle = (await page.$('div'))!;
|
||||
const screenshot = await elementHandle.screenshot({
|
||||
clip: {
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
});
|
||||
expect(screenshot).toBeGolden('screenshot-element-clip.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cdp', () => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue