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": "22.6.5",
|
||||||
"packages/puppeteer-core": "22.4.0",
|
"packages/puppeteer-core": "22.6.5",
|
||||||
"packages/testserver": "0.6.0",
|
"packages/testserver": "0.6.0",
|
||||||
"packages/ng-schematics": "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 */
|
/* eslint-disable import/order */
|
||||||
|
|
||||||
import {copyFile, readFile, writeFile} from 'fs/promises';
|
import {readFile, writeFile} from 'fs/promises';
|
||||||
|
|
||||||
import {docgen, spliceIntoSection} from '@puppeteer/docgen';
|
import {docgen, spliceIntoSection} from '@puppeteer/docgen';
|
||||||
import {execa} from 'execa';
|
import {execa} from 'execa';
|
||||||
import {task} from 'hereby';
|
import {task} from 'hereby';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
export const docsNgSchematicsTask = task({
|
function addNoTocHeader(markdown) {
|
||||||
name: 'docs:ng-schematics',
|
return `---
|
||||||
run: async () => {
|
hide_table_of_contents: true
|
||||||
const readme = await readFile('packages/ng-schematics/README.md', 'utf-8');
|
---
|
||||||
await writeFile('docs/integrations/ng-schematics.md', readme);
|
|
||||||
},
|
${markdown}`;
|
||||||
});
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This logic should match the one in `website/docusaurus.config.js`.
|
* This logic should match the one in `website/docusaurus.config.js`.
|
||||||
|
|
@ -34,10 +34,18 @@ function getApiUrl(version) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const docsChromiumSupportTask = task({
|
export const docsNgSchematicsTask = task({
|
||||||
name: 'docs:chromium-support',
|
name: 'docs:ng-schematics',
|
||||||
run: async () => {
|
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',
|
encoding: 'utf8',
|
||||||
});
|
});
|
||||||
const {versionsPerRelease} = await import('./versions.js');
|
const {versionsPerRelease} = await import('./versions.js');
|
||||||
|
|
@ -61,7 +69,7 @@ export const docsChromiumSupportTask = task({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await writeFile(
|
await writeFile(
|
||||||
'docs/chromium-support.md',
|
'docs/supported-browsers.md',
|
||||||
spliceIntoSection('version', content, buffer.join('\n'))
|
spliceIntoSection('version', content, buffer.join('\n'))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -72,7 +80,8 @@ export const docsTask = task({
|
||||||
dependencies: [docsNgSchematicsTask, docsChromiumSupportTask],
|
dependencies: [docsNgSchematicsTask, docsChromiumSupportTask],
|
||||||
run: async () => {
|
run: async () => {
|
||||||
// Copy main page.
|
// 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
|
// Generate documentation
|
||||||
for (const [name, folder] of [
|
for (const [name, folder] of [
|
||||||
|
|
|
||||||
|
|
@ -1,145 +1,21 @@
|
||||||
# Puppeteer
|
# Puppeteer
|
||||||
|
|
||||||
[](https://github.com/puppeteer/puppeteer/actions?query=workflow%3ACI)
|
[](https://github.com/puppeteer/puppeteer/actions/workflows/ci.yml)
|
||||||
[](https://npmjs.org/package/puppeteer)
|
[](https://npmjs.org/package/puppeteer)
|
||||||
|
|
||||||
<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"/>
|
<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
|
> Puppeteer is a Node.js library which provides a high-level API to control
|
||||||
> Chrome/Chromium over the
|
> Chrome/Chromium over the
|
||||||
> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/).
|
> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/).
|
||||||
> Puppeteer runs in
|
> 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")
|
> mode by default, but can be configured to run in full ("headful")
|
||||||
> Chrome/Chromium.
|
> 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!
|
## Example
|
||||||
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.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
|
|
@ -175,87 +51,3 @@ import puppeteer from 'puppeteer';
|
||||||
await browser.close();
|
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
|
# 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
|
```bash
|
||||||
NODE_PATH=../ node examples/search.js
|
NODE_PATH=../ node examples/search.js
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@ origin:
|
||||||
description: Headless Chrome Node API
|
description: Headless Chrome Node API
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
name: puppeteer
|
name: puppeteer
|
||||||
release: puppeteer-v22.4.0
|
release: puppeteer-v22.6.5
|
||||||
url: ../puppeteer
|
url: /Users/juliandescottes/Development/git/puppeteer
|
||||||
schema: 1
|
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/node": "20.8.4",
|
||||||
"@types/semver": "7.5.8",
|
"@types/semver": "7.5.8",
|
||||||
"@types/sinon": "17.0.3",
|
"@types/sinon": "17.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "7.1.0",
|
"@typescript-eslint/eslint-plugin": "7.6.0",
|
||||||
"@typescript-eslint/parser": "7.1.0",
|
"@typescript-eslint/parser": "7.6.0",
|
||||||
"esbuild": "0.20.1",
|
"esbuild": "0.20.2",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-import-resolver-typescript": "3.6.1",
|
"eslint-import-resolver-typescript": "3.6.1",
|
||||||
"eslint-plugin-import": "2.29.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-prettier": "5.1.3",
|
||||||
"eslint-plugin-rulesdir": "0.2.2",
|
"eslint-plugin-rulesdir": "0.2.2",
|
||||||
"eslint-plugin-tsdoc": "0.2.17",
|
"eslint-plugin-tsdoc": "0.2.17",
|
||||||
|
|
@ -156,18 +156,18 @@
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
"expect": "29.7.0",
|
"expect": "29.7.0",
|
||||||
"gts": "5.2.0",
|
"gts": "5.3.0",
|
||||||
"hereby": "1.8.9",
|
"hereby": "1.8.9",
|
||||||
"license-checker": "25.0.1",
|
"license-checker": "25.0.1",
|
||||||
"mocha": "10.3.0",
|
"mocha": "10.4.0",
|
||||||
"npm-run-all2": "6.1.2",
|
"npm-run-all2": "6.1.2",
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
"semver": "7.6.0",
|
"semver": "7.6.0",
|
||||||
"sinon": "17.0.1",
|
"sinon": "17.0.1",
|
||||||
"source-map-support": "0.5.21",
|
"source-map-support": "0.5.21",
|
||||||
"spdx-satisfies": "5.0.1",
|
"spdx-satisfies": "5.0.1",
|
||||||
"tsd": "0.30.7",
|
"tsd": "0.31.0",
|
||||||
"tsx": "4.7.1",
|
"tsx": "4.7.2",
|
||||||
"typescript": "5.3.3",
|
"typescript": "5.3.3",
|
||||||
"wireit": "0.14.4"
|
"wireit": "0.14.4"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,32 @@
|
||||||
# Changelog
|
# 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)
|
## [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
|
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
|
```bash
|
||||||
npx @puppeteer/browsers --help # help for all commands
|
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
|
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
|
## 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).
|
1. Launching the system browsers is only possible for Chrome/Chromium.
|
||||||
2. Launching the system browsers is only possible for Chrome/Chromium.
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@puppeteer/browsers",
|
"name": "@puppeteer/browsers",
|
||||||
"version": "2.1.0",
|
"version": "2.2.2",
|
||||||
"description": "Download and launch browsers",
|
"description": "Download and launch browsers",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:docs": "wireit",
|
"build:docs": "wireit",
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,31 @@ export class CLI {
|
||||||
);
|
);
|
||||||
yargs.example(
|
yargs.example(
|
||||||
'$0 install firefox',
|
'$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(
|
yargs.example(
|
||||||
'$0 install firefox --platform mac',
|
'$0 install firefox --platform mac',
|
||||||
|
|
@ -395,7 +419,7 @@ export function makeProgressCallback(
|
||||||
return (downloadedBytes: number, totalBytes: number) => {
|
return (downloadedBytes: number, totalBytes: number) => {
|
||||||
if (!progressBar) {
|
if (!progressBar) {
|
||||||
progressBar = new ProgressBar(
|
progressBar = new ProgressBar(
|
||||||
`Downloading ${browser} r${buildId} - ${toMegabytes(
|
`Downloading ${browser} ${buildId} - ${toMegabytes(
|
||||||
totalBytes
|
totalBytes
|
||||||
)} [:bar] :percent :etas `,
|
)} [:bar] :percent :etas `,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -54,28 +54,36 @@ export const versionComparators = {
|
||||||
export {Browser, BrowserPlatform, ChromeReleaseChannel};
|
export {Browser, BrowserPlatform, ChromeReleaseChannel};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @internal
|
||||||
*/
|
*/
|
||||||
export async function resolveBuildId(
|
async function resolveBuildIdForBrowserTag(
|
||||||
browser: Browser,
|
browser: Browser,
|
||||||
platform: BrowserPlatform,
|
platform: BrowserPlatform,
|
||||||
tag: string
|
tag: BrowserTag
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
switch (browser) {
|
switch (browser) {
|
||||||
case Browser.FIREFOX:
|
case Browser.FIREFOX:
|
||||||
switch (tag as BrowserTag) {
|
switch (tag) {
|
||||||
case BrowserTag.LATEST:
|
case BrowserTag.LATEST:
|
||||||
return await firefox.resolveBuildId('FIREFOX_NIGHTLY');
|
return await firefox.resolveBuildId(firefox.FirefoxChannel.NIGHTLY);
|
||||||
case BrowserTag.BETA:
|
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.CANARY:
|
||||||
case BrowserTag.DEV:
|
case BrowserTag.DEV:
|
||||||
case BrowserTag.STABLE:
|
throw new Error(`${tag.toUpperCase()} is not available for Firefox`);
|
||||||
throw new Error(
|
|
||||||
`${tag} is not supported for ${browser}. Use 'latest' instead.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
case Browser.CHROME: {
|
case Browser.CHROME: {
|
||||||
switch (tag as BrowserTag) {
|
switch (tag) {
|
||||||
case BrowserTag.LATEST:
|
case BrowserTag.LATEST:
|
||||||
return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
|
return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
|
||||||
case BrowserTag.BETA:
|
case BrowserTag.BETA:
|
||||||
|
|
@ -86,14 +94,12 @@ export async function resolveBuildId(
|
||||||
return await chrome.resolveBuildId(ChromeReleaseChannel.DEV);
|
return await chrome.resolveBuildId(ChromeReleaseChannel.DEV);
|
||||||
case BrowserTag.STABLE:
|
case BrowserTag.STABLE:
|
||||||
return await chrome.resolveBuildId(ChromeReleaseChannel.STABLE);
|
return await chrome.resolveBuildId(ChromeReleaseChannel.STABLE);
|
||||||
default:
|
case BrowserTag.NIGHTLY:
|
||||||
const result = await chrome.resolveBuildId(tag);
|
case BrowserTag.DEVEDITION:
|
||||||
if (result) {
|
case BrowserTag.ESR:
|
||||||
return result;
|
throw new Error(`${tag.toUpperCase()} is not available for Chrome`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
case Browser.CHROMEDRIVER: {
|
case Browser.CHROMEDRIVER: {
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case BrowserTag.LATEST:
|
case BrowserTag.LATEST:
|
||||||
|
|
@ -105,14 +111,14 @@ export async function resolveBuildId(
|
||||||
return await chromedriver.resolveBuildId(ChromeReleaseChannel.DEV);
|
return await chromedriver.resolveBuildId(ChromeReleaseChannel.DEV);
|
||||||
case BrowserTag.STABLE:
|
case BrowserTag.STABLE:
|
||||||
return await chromedriver.resolveBuildId(ChromeReleaseChannel.STABLE);
|
return await chromedriver.resolveBuildId(ChromeReleaseChannel.STABLE);
|
||||||
default:
|
case BrowserTag.NIGHTLY:
|
||||||
const result = await chromedriver.resolveBuildId(tag);
|
case BrowserTag.DEVEDITION:
|
||||||
if (result) {
|
case BrowserTag.ESR:
|
||||||
return result;
|
throw new Error(
|
||||||
|
`${tag.toUpperCase()} is not available for ChromeDriver`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
case Browser.CHROMEHEADLESSSHELL: {
|
case Browser.CHROMEHEADLESSSHELL: {
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case BrowserTag.LATEST:
|
case BrowserTag.LATEST:
|
||||||
|
|
@ -132,29 +138,68 @@ export async function resolveBuildId(
|
||||||
return await chromeHeadlessShell.resolveBuildId(
|
return await chromeHeadlessShell.resolveBuildId(
|
||||||
ChromeReleaseChannel.STABLE
|
ChromeReleaseChannel.STABLE
|
||||||
);
|
);
|
||||||
default:
|
case BrowserTag.NIGHTLY:
|
||||||
const result = await chromeHeadlessShell.resolveBuildId(tag);
|
case BrowserTag.DEVEDITION:
|
||||||
if (result) {
|
case BrowserTag.ESR:
|
||||||
return result;
|
throw new Error(`${tag} is not available for chrome-headless-shell`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
case Browser.CHROMIUM:
|
case Browser.CHROMIUM:
|
||||||
switch (tag as BrowserTag) {
|
switch (tag) {
|
||||||
case BrowserTag.LATEST:
|
case BrowserTag.LATEST:
|
||||||
return await chromium.resolveBuildId(platform);
|
return await chromium.resolveBuildId(platform);
|
||||||
case BrowserTag.BETA:
|
case BrowserTag.NIGHTLY:
|
||||||
case BrowserTag.CANARY:
|
case BrowserTag.CANARY:
|
||||||
case BrowserTag.DEV:
|
case BrowserTag.DEV:
|
||||||
|
case BrowserTag.DEVEDITION:
|
||||||
|
case BrowserTag.BETA:
|
||||||
case BrowserTag.STABLE:
|
case BrowserTag.STABLE:
|
||||||
|
case BrowserTag.ESR:
|
||||||
throw new Error(
|
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.
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
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';
|
import {BrowserPlatform, type ProfileOptions} from './types.js';
|
||||||
|
|
||||||
function archive(platform: BrowserPlatform, buildId: string): string {
|
function archiveNightly(platform: BrowserPlatform, buildId: string): string {
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case BrowserPlatform.LINUX:
|
case BrowserPlatform.LINUX:
|
||||||
return `firefox-${buildId}.en-US.${platform}-x86_64.tar.bz2`;
|
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(
|
export function resolveDownloadUrl(
|
||||||
platform: BrowserPlatform,
|
platform: BrowserPlatform,
|
||||||
buildId: string,
|
buildId: string,
|
||||||
baseUrl = 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central'
|
baseUrl?: string
|
||||||
): 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(
|
export function resolveDownloadPath(
|
||||||
platform: BrowserPlatform,
|
platform: BrowserPlatform,
|
||||||
buildId: string
|
buildId: string
|
||||||
): string[] {
|
): string[] {
|
||||||
return [archive(platform, buildId)];
|
return [archiveNightly(platform, buildId)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function relativeExecutablePath(
|
export function relativeExecutablePath(
|
||||||
platform: BrowserPlatform,
|
platform: BrowserPlatform,
|
||||||
_buildId: string
|
buildId: string
|
||||||
): string {
|
): string {
|
||||||
|
const [channel] = parseBuildId(buildId);
|
||||||
|
switch (channel) {
|
||||||
|
case FirefoxChannel.NIGHTLY:
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case BrowserPlatform.MAC_ARM:
|
case BrowserPlatform.MAC_ARM:
|
||||||
case BrowserPlatform.MAC:
|
case BrowserPlatform.MAC:
|
||||||
return path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox');
|
return path.join(
|
||||||
|
'Firefox Nightly.app',
|
||||||
|
'Contents',
|
||||||
|
'MacOS',
|
||||||
|
'firefox'
|
||||||
|
);
|
||||||
case BrowserPlatform.LINUX:
|
case BrowserPlatform.LINUX:
|
||||||
return path.join('firefox', 'firefox');
|
return path.join('firefox', 'firefox');
|
||||||
case BrowserPlatform.WIN32:
|
case BrowserPlatform.WIN32:
|
||||||
case BrowserPlatform.WIN64:
|
case BrowserPlatform.WIN64:
|
||||||
return path.join('firefox', 'firefox.exe');
|
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(
|
export async function resolveBuildId(
|
||||||
channel: 'FIREFOX_NIGHTLY' = 'FIREFOX_NIGHTLY'
|
channel: FirefoxChannel = FirefoxChannel.NIGHTLY
|
||||||
): Promise<string> {
|
): 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(
|
const versions = (await getJSON(
|
||||||
new URL('https://product-details.mozilla.org/1.0/firefox_versions.json')
|
new URL('https://product-details.mozilla.org/1.0/firefox_versions.json')
|
||||||
)) as Record<string, string>;
|
)) as Record<string, string>;
|
||||||
const version = versions[channel];
|
const version = versions[channelToVersionKey[channel]];
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw new Error(`Channel ${channel} is not found.`);
|
throw new Error(`Channel ${channel} is not found.`);
|
||||||
}
|
}
|
||||||
return version;
|
return channel + '_' + version;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createProfile(options: ProfileOptions): Promise<void> {
|
export async function createProfile(options: ProfileOptions): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,12 @@ export enum BrowserPlatform {
|
||||||
*/
|
*/
|
||||||
export enum BrowserTag {
|
export enum BrowserTag {
|
||||||
CANARY = 'canary',
|
CANARY = 'canary',
|
||||||
|
NIGHTLY = 'nightly',
|
||||||
BETA = 'beta',
|
BETA = 'beta',
|
||||||
DEV = 'dev',
|
DEV = 'dev',
|
||||||
|
DEVEDITION = 'devedition',
|
||||||
STABLE = 'stable',
|
STABLE = 'stable',
|
||||||
|
ESR = 'esr',
|
||||||
LATEST = 'latest',
|
LATEST = 'latest',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 {createReadStream} from 'fs';
|
||||||
import {mkdir, readdir} from 'fs/promises';
|
import {mkdir, readdir} from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
@ -30,6 +30,18 @@ export async function unpackArchive(
|
||||||
} else if (archivePath.endsWith('.dmg')) {
|
} else if (archivePath.endsWith('.dmg')) {
|
||||||
await mkdir(folderPath);
|
await mkdir(folderPath);
|
||||||
await installDMG(archivePath, 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 {
|
} else {
|
||||||
throw new Error(`Unsupported archive format: ${archivePath}`);
|
throw new Error(`Unsupported archive format: ${archivePath}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@ export function httpRequest(
|
||||||
res.headers.location
|
res.headers.location
|
||||||
) {
|
) {
|
||||||
httpRequest(new URL(res.headers.location), method, response);
|
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 {
|
} else {
|
||||||
response(res);
|
response(res);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,11 @@ export interface InstallOptions {
|
||||||
* @defaultValue `true`
|
* @defaultValue `true`
|
||||||
*/
|
*/
|
||||||
unpack?: boolean;
|
unpack?: boolean;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @defaultValue `false`
|
||||||
|
*/
|
||||||
|
forceFallbackForTesting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -125,6 +130,10 @@ export async function install(
|
||||||
try {
|
try {
|
||||||
return await installUrl(url, options);
|
return await installUrl(url, options);
|
||||||
} catch (err) {
|
} 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}.`);
|
debugInstall(`Error downloading from ${url}.`);
|
||||||
switch (options.browser) {
|
switch (options.browser) {
|
||||||
case Browser.CHROME:
|
case Browser.CHROME:
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,59 @@ export const CDP_WEBSOCKET_ENDPOINT_REGEX =
|
||||||
export const WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX =
|
export const WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX =
|
||||||
/^WebDriver BiDi listening on (ws:\/\/.*)$/;
|
/^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
|
* @public
|
||||||
*/
|
*/
|
||||||
|
|
@ -201,15 +254,15 @@ export class Process {
|
||||||
this.#browserProcess.stderr?.pipe(process.stderr);
|
this.#browserProcess.stderr?.pipe(process.stderr);
|
||||||
this.#browserProcess.stdout?.pipe(process.stdout);
|
this.#browserProcess.stdout?.pipe(process.stdout);
|
||||||
}
|
}
|
||||||
process.on('exit', this.#onDriverProcessExit);
|
subscribeToProcessEvent('exit', this.#onDriverProcessExit);
|
||||||
if (opts.handleSIGINT) {
|
if (opts.handleSIGINT) {
|
||||||
process.on('SIGINT', this.#onDriverProcessSignal);
|
subscribeToProcessEvent('SIGINT', this.#onDriverProcessSignal);
|
||||||
}
|
}
|
||||||
if (opts.handleSIGTERM) {
|
if (opts.handleSIGTERM) {
|
||||||
process.on('SIGTERM', this.#onDriverProcessSignal);
|
subscribeToProcessEvent('SIGTERM', this.#onDriverProcessSignal);
|
||||||
}
|
}
|
||||||
if (opts.handleSIGHUP) {
|
if (opts.handleSIGHUP) {
|
||||||
process.on('SIGHUP', this.#onDriverProcessSignal);
|
subscribeToProcessEvent('SIGHUP', this.#onDriverProcessSignal);
|
||||||
}
|
}
|
||||||
if (opts.onExit) {
|
if (opts.onExit) {
|
||||||
this.#onExitHook = opts.onExit;
|
this.#onExitHook = opts.onExit;
|
||||||
|
|
@ -262,10 +315,10 @@ export class Process {
|
||||||
}
|
}
|
||||||
|
|
||||||
#clearListeners(): void {
|
#clearListeners(): void {
|
||||||
process.off('exit', this.#onDriverProcessExit);
|
unsubscribeFromProcessEvent('exit', this.#onDriverProcessExit);
|
||||||
process.off('SIGINT', this.#onDriverProcessSignal);
|
unsubscribeFromProcessEvent('SIGINT', this.#onDriverProcessSignal);
|
||||||
process.off('SIGTERM', this.#onDriverProcessSignal);
|
unsubscribeFromProcessEvent('SIGTERM', this.#onDriverProcessSignal);
|
||||||
process.off('SIGHUP', this.#onDriverProcessSignal);
|
unsubscribeFromProcessEvent('SIGHUP', this.#onDriverProcessSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
#onDriverProcessExit = (_code: number) => {
|
#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 () {
|
it('falls back to the chrome-for-testing dashboard URLs if URL is not available', async function () {
|
||||||
|
this.timeout(60000);
|
||||||
const expectedOutputPath = path.join(
|
const expectedOutputPath = path.join(
|
||||||
tmpDir,
|
tmpDir,
|
||||||
'chrome',
|
'chrome',
|
||||||
|
|
@ -150,6 +151,7 @@ describe('Chrome install', () => {
|
||||||
platform: BrowserPlatform.LINUX,
|
platform: BrowserPlatform.LINUX,
|
||||||
buildId: testChromeBuildId,
|
buildId: testChromeBuildId,
|
||||||
baseUrl: 'https://127.0.0.1',
|
baseUrl: 'https://127.0.0.1',
|
||||||
|
forceFallbackForTesting: true,
|
||||||
});
|
});
|
||||||
assert.strictEqual(fs.existsSync(expectedOutputPath), true);
|
assert.strictEqual(fs.existsSync(expectedOutputPath), true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ describe('Chrome', () => {
|
||||||
'--disable-renderer-backgrounding',
|
'--disable-renderer-backgrounding',
|
||||||
'--disable-sync',
|
'--disable-sync',
|
||||||
'--enable-automation',
|
'--enable-automation',
|
||||||
'--enable-features=NetworkServiceInProcess2',
|
|
||||||
'--export-tagged-pdf',
|
'--export-tagged-pdf',
|
||||||
'--force-color-profile=srgb',
|
'--force-color-profile=srgb',
|
||||||
'--headless=new',
|
'--headless=new',
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ describe('Chromium', () => {
|
||||||
'--disable-renderer-backgrounding',
|
'--disable-renderer-backgrounding',
|
||||||
'--disable-sync',
|
'--disable-sync',
|
||||||
'--enable-automation',
|
'--enable-automation',
|
||||||
'--enable-features=NetworkServiceInProcess2',
|
|
||||||
'--export-tagged-pdf',
|
'--export-tagged-pdf',
|
||||||
'--force-color-profile=srgb',
|
'--force-color-profile=srgb',
|
||||||
'--headless=new',
|
'--headless=new',
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from '../../../lib/cjs/browser-data/firefox.js';
|
} from '../../../lib/cjs/browser-data/firefox.js';
|
||||||
|
|
||||||
describe('Firefox', () => {
|
describe('Firefox', () => {
|
||||||
it('should resolve download URLs', () => {
|
it('should resolve download URLs for Nightly', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
resolveDownloadUrl(BrowserPlatform.LINUX, '111.0a1'),
|
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'
|
'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', () => {
|
it('should resolve executable paths', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
relativeExecutablePath(BrowserPlatform.LINUX, '111.0a1'),
|
relativeExecutablePath(BrowserPlatform.LINUX, '111.0a1'),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@
|
||||||
|
|
||||||
export const testChromeBuildId = '121.0.6167.85';
|
export const testChromeBuildId = '121.0.6167.85';
|
||||||
export const testChromiumBuildId = '1083080';
|
export const testChromiumBuildId = '1083080';
|
||||||
export const testFirefoxBuildId = '125.0a1';
|
export const testFirefoxBuildId = '126.0a1';
|
||||||
export const testChromeDriverBuildId = '121.0.6167.85';
|
export const testChromeDriverBuildId = '121.0.6167.85';
|
||||||
export const testChromeHeadlessShellBuildId = '121.0.6167.85';
|
export const testChromeHeadlessShellBuildId = '121.0.6167.85';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Ignore File that will be copied to Angular
|
# Ignore File that will be copied to Angular
|
||||||
/files/
|
/files/
|
||||||
|
|
||||||
# Ignore sandbox enviroment
|
# Ignore sandbox environment
|
||||||
./sandbox/
|
./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:
|
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
|
npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,104 @@ All notable changes to this project will be documented in this file. See [standa
|
||||||
* dependencies
|
* dependencies
|
||||||
* @puppeteer/browsers bumped from 1.5.1 to 1.6.0
|
* @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)
|
## [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,
|
bundle: true,
|
||||||
allowOverwrite: true,
|
allowOverwrite: true,
|
||||||
format,
|
format,
|
||||||
target: 'node16',
|
target: 'node18',
|
||||||
minify: true,
|
// Do not minify for readability and leave minification to
|
||||||
|
// consumers.
|
||||||
|
minify: false,
|
||||||
legalComments: 'inline',
|
legalComments: 'inline',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "puppeteer-core",
|
"name": "puppeteer-core",
|
||||||
"version": "22.4.0",
|
"version": "22.6.5",
|
||||||
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"puppeteer",
|
"puppeteer",
|
||||||
|
|
@ -119,11 +119,10 @@
|
||||||
"author": "The Chromium Authors",
|
"author": "The Chromium Authors",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@puppeteer/browsers": "2.1.0",
|
"@puppeteer/browsers": "2.2.2",
|
||||||
"chromium-bidi": "0.5.12",
|
"chromium-bidi": "0.5.17",
|
||||||
"cross-fetch": "4.0.0",
|
|
||||||
"debug": "4.3.4",
|
"debug": "4.3.4",
|
||||||
"devtools-protocol": "0.0.1249869",
|
"devtools-protocol": "0.0.1262051",
|
||||||
"ws": "8.16.0"
|
"ws": "8.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ export interface DebugInfo {
|
||||||
* - connected to via {@link Puppeteer.connect} or
|
* - connected to via {@link Puppeteer.connect} or
|
||||||
* - launched by {@link PuppeteerNode.launch}.
|
* - 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.
|
* documented in the {@link BrowserEvent} enum.
|
||||||
*
|
*
|
||||||
* @example Using a {@link Browser} to create a {@link Page}:
|
* @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
|
* @deprecated In Chrome, the
|
||||||
* {@link Browser.defaultBrowserContext | default browser context} can also be
|
* {@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
|
* returns wrong results (see
|
||||||
* https://github.com/puppeteer/puppeteer/issues/8836). Also, the term
|
* https://github.com/puppeteer/puppeteer/issues/8836). Also, the term
|
||||||
* "incognito" is not applicable to other browsers. To migrate, check the
|
* "incognito" is not applicable to other browsers. To migrate, check the
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ export interface CommandOptions {
|
||||||
* @example
|
* @example
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* const client = await page.target().createCDPSession();
|
* const client = await page.createCDPSession();
|
||||||
* await client.send('Animation.enable');
|
* await client.send('Animation.enable');
|
||||||
* client.on('Animation.animationCreated', () =>
|
* client.on('Animation.animationCreated', () =>
|
||||||
* console.log('Animation created!')
|
* console.log('Animation created!')
|
||||||
|
|
|
||||||
|
|
@ -623,7 +623,7 @@ export abstract class ElementHandle<
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method scrolls element into view if needed, and then
|
* 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.
|
* If the element is detached from DOM, the method throws an error.
|
||||||
*/
|
*/
|
||||||
@throwIfDisposed()
|
@throwIfDisposed()
|
||||||
|
|
@ -636,7 +636,7 @@ export abstract class ElementHandle<
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method scrolls element into view if needed, and then
|
* 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.
|
* If the element is detached from DOM, the method throws an error.
|
||||||
*/
|
*/
|
||||||
@throwIfDisposed()
|
@throwIfDisposed()
|
||||||
|
|
@ -1236,9 +1236,9 @@ export abstract class ElementHandle<
|
||||||
this: ElementHandle<Element>,
|
this: ElementHandle<Element>,
|
||||||
options: Readonly<ElementScreenshotOptions> = {}
|
options: Readonly<ElementScreenshotOptions> = {}
|
||||||
): Promise<string | Buffer> {
|
): 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();
|
const page = this.frame.page();
|
||||||
|
|
||||||
|
|
@ -1247,7 +1247,7 @@ export abstract class ElementHandle<
|
||||||
await this.scrollIntoViewIfNeeded();
|
await this.scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
// We measure again just in case.
|
// We measure again just in case.
|
||||||
clip = await this.#nonEmptyVisibleBoundingBox();
|
elementClip = await this.#nonEmptyVisibleBoundingBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
const [pageLeft, pageTop] = await this.evaluate(() => {
|
const [pageLeft, pageTop] = await this.evaluate(() => {
|
||||||
|
|
@ -1259,10 +1259,16 @@ export abstract class ElementHandle<
|
||||||
window.visualViewport.pageTop,
|
window.visualViewport.pageTop,
|
||||||
] as const;
|
] as const;
|
||||||
});
|
});
|
||||||
clip.x += pageLeft;
|
elementClip.x += pageLeft;
|
||||||
clip.y += pageTop;
|
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() {
|
async #nonEmptyVisibleBoundingBox() {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
|
||||||
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
|
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
|
||||||
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
|
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
|
||||||
import {transposeIterableHandle} from '../common/HandleIterator.js';
|
import {transposeIterableHandle} from '../common/HandleIterator.js';
|
||||||
import {LazyArg} from '../common/LazyArg.js';
|
|
||||||
import type {
|
import type {
|
||||||
Awaitable,
|
Awaitable,
|
||||||
EvaluateFunc,
|
EvaluateFunc,
|
||||||
|
|
@ -63,6 +62,10 @@ export interface WaitForOptions {
|
||||||
* @defaultValue `'load'`
|
* @defaultValue `'load'`
|
||||||
*/
|
*/
|
||||||
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
|
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
|
@throwIfDetached
|
||||||
async frameElement(): Promise<HandleFor<HTMLIFrameElement> | null> {
|
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.
|
* the context of this frame.
|
||||||
*
|
*
|
||||||
* @see {@link Page.evaluate} for details.
|
* @see {@link Page.evaluate} for details.
|
||||||
|
|
@ -760,6 +763,13 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||||
* @remarks
|
* @remarks
|
||||||
* This value is calculated once when the frame is created, and will not
|
* This value is calculated once when the frame is created, and will not
|
||||||
* update if the attribute is changed later.
|
* 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 {
|
name(): string {
|
||||||
return this._name || '';
|
return this._name || '';
|
||||||
|
|
@ -830,42 +840,37 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||||
|
|
||||||
return await this.mainRealm().transferHandle(
|
return await this.mainRealm().transferHandle(
|
||||||
await this.isolatedRealm().evaluateHandle(
|
await this.isolatedRealm().evaluateHandle(
|
||||||
async ({Deferred}, {url, id, type, content}) => {
|
async ({url, id, type, content}) => {
|
||||||
const deferred = Deferred.create<void>();
|
return await new Promise<HTMLScriptElement>((resolve, reject) => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.type = type;
|
script.type = type;
|
||||||
script.text = content;
|
script.text = content;
|
||||||
|
script.addEventListener(
|
||||||
|
'error',
|
||||||
|
event => {
|
||||||
|
reject(new Error(event.message ?? 'Could not load script'));
|
||||||
|
},
|
||||||
|
{once: true}
|
||||||
|
);
|
||||||
|
if (id) {
|
||||||
|
script.id = id;
|
||||||
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
script.src = url;
|
script.src = url;
|
||||||
script.addEventListener(
|
script.addEventListener(
|
||||||
'load',
|
'load',
|
||||||
() => {
|
() => {
|
||||||
return deferred.resolve();
|
resolve(script);
|
||||||
},
|
},
|
||||||
{once: true}
|
{once: true}
|
||||||
);
|
);
|
||||||
script.addEventListener(
|
|
||||||
'error',
|
|
||||||
event => {
|
|
||||||
deferred.reject(
|
|
||||||
new Error(event.message ?? 'Could not load script')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{once: true}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
deferred.resolve();
|
|
||||||
}
|
|
||||||
if (id) {
|
|
||||||
script.id = id;
|
|
||||||
}
|
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
await deferred.valueOrThrow();
|
} else {
|
||||||
return script;
|
document.head.appendChild(script);
|
||||||
|
resolve(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
LazyArg.create(context => {
|
|
||||||
return context.puppeteerUtil;
|
|
||||||
}),
|
|
||||||
{...options, type, content}
|
{...options, type, content}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -915,9 +920,9 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.mainRealm().transferHandle(
|
return await this.mainRealm().transferHandle(
|
||||||
await this.isolatedRealm().evaluateHandle(
|
await this.isolatedRealm().evaluateHandle(async ({url, content}) => {
|
||||||
async ({Deferred}, {url, content}) => {
|
return await new Promise<HTMLStyleElement | HTMLLinkElement>(
|
||||||
const deferred = Deferred.create<void>();
|
(resolve, reject) => {
|
||||||
let element: HTMLStyleElement | HTMLLinkElement;
|
let element: HTMLStyleElement | HTMLLinkElement;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
element = document.createElement('style');
|
element = document.createElement('style');
|
||||||
|
|
@ -931,14 +936,14 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
'load',
|
'load',
|
||||||
() => {
|
() => {
|
||||||
deferred.resolve();
|
resolve(element);
|
||||||
},
|
},
|
||||||
{once: true}
|
{once: true}
|
||||||
);
|
);
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
'error',
|
'error',
|
||||||
event => {
|
event => {
|
||||||
deferred.reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
(event as ErrorEvent).message ?? 'Could not load style'
|
(event as ErrorEvent).message ?? 'Could not load style'
|
||||||
)
|
)
|
||||||
|
|
@ -947,14 +952,10 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||||
{once: true}
|
{once: true}
|
||||||
);
|
);
|
||||||
document.head.appendChild(element);
|
document.head.appendChild(element);
|
||||||
await deferred.valueOrThrow();
|
|
||||||
return element;
|
return element;
|
||||||
},
|
}
|
||||||
LazyArg.create(context => {
|
);
|
||||||
return context.puppeteerUtil;
|
}, options)
|
||||||
}),
|
|
||||||
options
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
*/
|
*/
|
||||||
import type {Protocol} from 'devtools-protocol';
|
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 {CDPSession} from './CDPSession.js';
|
||||||
import type {Frame} from './Frame.js';
|
import type {Frame} from './Frame.js';
|
||||||
import type {HTTPResponse} from './HTTPResponse.js';
|
import type {HTTPResponse} from './HTTPResponse.js';
|
||||||
|
|
@ -117,6 +121,29 @@ export abstract class HTTPRequest {
|
||||||
*/
|
*/
|
||||||
_redirectChain: 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.
|
* 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
|
* if the interception is allowed to continue (ie, `abort()` and
|
||||||
* `respond()` aren't called).
|
* `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
|
* The `ResponseForRequest` that gets used if the
|
||||||
* interception is allowed to respond (ie, `abort()` is not called).
|
* 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
|
* 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
|
* An InterceptResolutionState object describing the current resolution
|
||||||
|
|
@ -163,13 +199,23 @@ export abstract class HTTPRequest {
|
||||||
* InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
|
* InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
|
||||||
* `disabled`, `none`, or `already-handled`.
|
* `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,
|
* Is `true` if the intercept resolution has already been handled,
|
||||||
* `false` otherwise.
|
* `false` otherwise.
|
||||||
*/
|
*/
|
||||||
abstract isInterceptResolutionHandled(): boolean;
|
isInterceptResolutionHandled(): boolean {
|
||||||
|
return this.interception.handled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an async request handler to the processing queue.
|
* 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
|
* but they are guaranteed to resolve before the request interception
|
||||||
* is finalized.
|
* is finalized.
|
||||||
*/
|
*/
|
||||||
abstract enqueueInterceptAction(
|
enqueueInterceptAction(
|
||||||
pendingHandler: () => void | PromiseLike<unknown>
|
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
|
* Awaits pending interception handlers and then decides how to fulfill
|
||||||
* the request interception.
|
* 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
|
* 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.
|
* Exception is immediately thrown if the request interception is not enabled.
|
||||||
*/
|
*/
|
||||||
abstract continue(
|
async continue(
|
||||||
overrides?: ContinueRequestOverrides,
|
overrides: ContinueRequestOverrides = {},
|
||||||
priority?: number
|
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.
|
* 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.
|
* Exception is immediately thrown if the request interception is not enabled.
|
||||||
*/
|
*/
|
||||||
abstract respond(
|
async respond(
|
||||||
response: Partial<ResponseForRequest>,
|
response: Partial<ResponseForRequest>,
|
||||||
priority?: number
|
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.
|
* Aborts a request.
|
||||||
|
|
@ -379,7 +521,33 @@ export abstract class HTTPRequest {
|
||||||
* {@link Page.setRequestInterception}. If it is not enabled, this method will
|
* {@link Page.setRequestInterception}. If it is not enabled, this method will
|
||||||
* throw an exception immediately.
|
* 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',
|
'510': 'Not Extended',
|
||||||
'511': 'Network Authentication Required',
|
'511': 'Network Authentication Required',
|
||||||
} as const;
|
} 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.
|
* 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>;
|
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> {
|
async text(): Promise<string> {
|
||||||
const content = await this.buffer();
|
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 {Accessibility} from '../cdp/Accessibility.js';
|
||||||
import type {Coverage} from '../cdp/Coverage.js';
|
import type {Coverage} from '../cdp/Coverage.js';
|
||||||
import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.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 {Tracing} from '../cdp/Tracing.js';
|
||||||
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
|
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -131,6 +131,14 @@ export interface Metrics {
|
||||||
JSHeapTotalSize?: number;
|
JSHeapTotalSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface Credentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
|
@ -274,7 +282,7 @@ export interface ScreenshotOptions {
|
||||||
*/
|
*/
|
||||||
path?: string;
|
path?: string;
|
||||||
/**
|
/**
|
||||||
* Specifies the region of the page to clip.
|
* Specifies the region of the page/element to clip.
|
||||||
*/
|
*/
|
||||||
clip?: ScreenshotClip;
|
clip?: ScreenshotClip;
|
||||||
/**
|
/**
|
||||||
|
|
@ -644,7 +652,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||||
*
|
*
|
||||||
* @deprecated We no longer support intercepting drag payloads. Use the new
|
* @deprecated We no longer support intercepting drag payloads. Use the new
|
||||||
* drag APIs found on {@link ElementHandle} to drag (or just use the
|
* drag APIs found on {@link ElementHandle} to drag (or just use the
|
||||||
* {@link Page | Page.mouse}).
|
* {@link Page.mouse}).
|
||||||
*/
|
*/
|
||||||
abstract isDragInterceptionEnabled(): boolean;
|
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
|
* @deprecated We no longer support intercepting drag payloads. Use the new
|
||||||
* drag APIs found on {@link ElementHandle} to drag (or just use the
|
* 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>;
|
abstract setDragInterception(enabled: boolean): Promise<void>;
|
||||||
|
|
||||||
|
|
@ -1342,7 +1350,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||||
*
|
*
|
||||||
* Functions installed via `page.exposeFunction` survive navigations.
|
* Functions installed via `page.exposeFunction` survive navigations.
|
||||||
*
|
*
|
||||||
* :::note
|
* :::
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* An example of adding an `md5` function into the page:
|
* 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}
|
* This is either the viewport set with the previous {@link Page.setViewport}
|
||||||
* call or the default viewport set via
|
* call or the default viewport set via
|
||||||
* {@link BrowserConnectOptions | BrowserConnectOptions.defaultViewport}.
|
* {@link BrowserConnectOptions.defaultViewport |
|
||||||
|
* BrowserConnectOptions.defaultViewport}.
|
||||||
*/
|
*/
|
||||||
abstract viewport(): Viewport | null;
|
abstract viewport(): Viewport | null;
|
||||||
|
|
||||||
|
|
@ -2458,7 +2467,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||||
};
|
};
|
||||||
if (options.type === undefined && options.path !== undefined) {
|
if (options.type === undefined && options.path !== undefined) {
|
||||||
const filePath = options.path;
|
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
|
const extension = filePath
|
||||||
.slice(filePath.lastIndexOf('.') + 1)
|
.slice(filePath.lastIndexOf('.') + 1)
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
@ -2609,7 +2618,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method fetches an element with `selector`, scrolls it into view if
|
* 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
|
* element. If there's no element matching `selector`, the method throws an
|
||||||
* error.
|
* error.
|
||||||
*
|
*
|
||||||
|
|
@ -2660,7 +2669,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method fetches an element with `selector`, scrolls it into view if
|
* 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.
|
* to hover over the center of the element.
|
||||||
* If there's no element matching `selector`, the method throws an error.
|
* If there's no element matching `selector`, the method throws an error.
|
||||||
* @param selector - A
|
* @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
|
* 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.
|
* to tap in the center of the element.
|
||||||
* If there's no element matching `selector`, the method throws an error.
|
* If there's no element matching `selector`, the method throws an error.
|
||||||
* @param selector - A
|
* @param selector - A
|
||||||
|
|
|
||||||
|
|
@ -172,19 +172,23 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
retryAndRaceWithSignalAndTimer: <T>(
|
retryAndRaceWithSignalAndTimer: <T>(
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal,
|
||||||
|
cause?: Error
|
||||||
): OperatorFunction<T, T> => {
|
): OperatorFunction<T, T> => {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
if (signal) {
|
if (signal) {
|
||||||
candidates.push(
|
candidates.push(
|
||||||
fromEvent(signal, 'abort').pipe(
|
fromEvent(signal, 'abort').pipe(
|
||||||
map(() => {
|
map(() => {
|
||||||
|
if (signal.reason instanceof Error) {
|
||||||
|
signal.reason.cause = cause;
|
||||||
|
}
|
||||||
throw signal.reason;
|
throw signal.reason;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
candidates.push(timeout(this._timeout));
|
candidates.push(timeout(this._timeout, cause));
|
||||||
return pipe(
|
return pipe(
|
||||||
retry({delay: RETRY_DELAY}),
|
retry({delay: RETRY_DELAY}),
|
||||||
raceWith<T, never[]>(...candidates)
|
raceWith<T, never[]>(...candidates)
|
||||||
|
|
@ -368,6 +372,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||||
options?: Readonly<LocatorClickOptions>
|
options?: Readonly<LocatorClickOptions>
|
||||||
): Observable<void> {
|
): Observable<void> {
|
||||||
const signal = options?.signal;
|
const signal = options?.signal;
|
||||||
|
const cause = new Error('Locator.click');
|
||||||
return this._wait(options).pipe(
|
return this._wait(options).pipe(
|
||||||
this.operators.conditions(
|
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>
|
options?: Readonly<ActionOptions>
|
||||||
): Observable<void> {
|
): Observable<void> {
|
||||||
const signal = options?.signal;
|
const signal = options?.signal;
|
||||||
|
const cause = new Error('Locator.fill');
|
||||||
return this._wait(options).pipe(
|
return this._wait(options).pipe(
|
||||||
this.operators.conditions(
|
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>
|
options?: Readonly<ActionOptions>
|
||||||
): Observable<void> {
|
): Observable<void> {
|
||||||
const signal = options?.signal;
|
const signal = options?.signal;
|
||||||
|
const cause = new Error('Locator.hover');
|
||||||
return this._wait(options).pipe(
|
return this._wait(options).pipe(
|
||||||
this.operators.conditions(
|
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>
|
options?: Readonly<LocatorScrollOptions>
|
||||||
): Observable<void> {
|
): Observable<void> {
|
||||||
const signal = options?.signal;
|
const signal = options?.signal;
|
||||||
|
const cause = new Error('Locator.scroll');
|
||||||
return this._wait(options).pipe(
|
return this._wait(options).pipe(
|
||||||
this.operators.conditions(
|
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
|
* @public
|
||||||
*/
|
*/
|
||||||
async waitHandle(options?: Readonly<ActionOptions>): Promise<HandleFor<T>> {
|
async waitHandle(options?: Readonly<ActionOptions>): Promise<HandleFor<T>> {
|
||||||
|
const cause = new Error('Locator.waitHandle');
|
||||||
return await firstValueFrom(
|
return await firstValueFrom(
|
||||||
this._wait(options).pipe(
|
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(
|
export async function connectBidiOverCdp(
|
||||||
cdp: CdpConnection,
|
cdp: CdpConnection,
|
||||||
// TODO: replace with `BidiMapper.MapperOptions`, once it's exported in
|
options: BidiMapper.MapperOptions
|
||||||
// https://github.com/puppeteer/puppeteer/pull/11415.
|
|
||||||
options: {acceptInsecureCerts: boolean}
|
|
||||||
): Promise<BidiConnection> {
|
): Promise<BidiConnection> {
|
||||||
const transportBiDi = new NoOpTransport();
|
const transportBiDi = new NoOpTransport();
|
||||||
const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
|
const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import {
|
||||||
import {BrowserContextEvent} from '../api/BrowserContext.js';
|
import {BrowserContextEvent} from '../api/BrowserContext.js';
|
||||||
import type {Page} from '../api/Page.js';
|
import type {Page} from '../api/Page.js';
|
||||||
import type {Target} from '../api/Target.js';
|
import type {Target} from '../api/Target.js';
|
||||||
import {UnsupportedOperation} from '../common/Errors.js';
|
|
||||||
import {EventEmitter} from '../common/EventEmitter.js';
|
import {EventEmitter} from '../common/EventEmitter.js';
|
||||||
import {debugError} from '../common/util.js';
|
import {debugError} from '../common/util.js';
|
||||||
import type {Viewport} from '../common/Viewport.js';
|
import type {Viewport} from '../common/Viewport.js';
|
||||||
|
|
@ -50,7 +49,7 @@ export class BidiBrowser extends Browser {
|
||||||
readonly protocol = 'webDriverBiDi';
|
readonly protocol = 'webDriverBiDi';
|
||||||
|
|
||||||
// TODO: Update generator to include fully module
|
// TODO: Update generator to include fully module
|
||||||
static readonly subscribeModules: string[] = [
|
static readonly subscribeModules: [string, ...string[]] = [
|
||||||
'browsingContext',
|
'browsingContext',
|
||||||
'network',
|
'network',
|
||||||
'log',
|
'log',
|
||||||
|
|
@ -133,8 +132,8 @@ export class BidiBrowser extends Browser {
|
||||||
return !this.#browserName.toLocaleLowerCase().includes('firefox');
|
return !this.#browserName.toLocaleLowerCase().includes('firefox');
|
||||||
}
|
}
|
||||||
|
|
||||||
override userAgent(): never {
|
override async userAgent(): Promise<string> {
|
||||||
throw new UnsupportedOperation();
|
return this.#browserCore.session.capabilities.userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#createBrowserContext(userContext: UserContext) {
|
#createBrowserContext(userContext: UserContext) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
|
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
|
||||||
|
|
||||||
|
import type {CommandOptions} from '../api/CDPSession.js';
|
||||||
import {CDPSession} from '../api/CDPSession.js';
|
import {CDPSession} from '../api/CDPSession.js';
|
||||||
import type {Connection as CdpConnection} from '../cdp/Connection.js';
|
import type {Connection as CdpConnection} from '../cdp/Connection.js';
|
||||||
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.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>(
|
override async send<T extends keyof ProtocolMapping.Commands>(
|
||||||
method: T,
|
method: T,
|
||||||
params?: ProtocolMapping.Commands[T]['paramsType'][0]
|
params?: ProtocolMapping.Commands[T]['paramsType'][0],
|
||||||
|
options?: CommandOptions
|
||||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||||
if (this.#connection === undefined) {
|
if (this.#connection === undefined) {
|
||||||
throw new UnsupportedOperation(
|
throw new UnsupportedOperation(
|
||||||
|
|
@ -74,11 +76,15 @@ export class BidiCdpSession extends CDPSession {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const session = await this.#sessionId.valueOrThrow();
|
const session = await this.#sessionId.valueOrThrow();
|
||||||
const {result} = await this.#connection.send('cdp.sendCommand', {
|
const {result} = await this.#connection.send(
|
||||||
|
'cdp.sendCommand',
|
||||||
|
{
|
||||||
method: method,
|
method: method,
|
||||||
params: params,
|
params: params,
|
||||||
session,
|
session,
|
||||||
});
|
},
|
||||||
|
options?.timeout
|
||||||
|
);
|
||||||
return result.result;
|
return result.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,12 @@ export class BidiConnection
|
||||||
|
|
||||||
send<T extends keyof Commands>(
|
send<T extends keyof Commands>(
|
||||||
method: T,
|
method: T,
|
||||||
params: Commands[T]['params']
|
params: Commands[T]['params'],
|
||||||
|
timeout?: number
|
||||||
): Promise<{result: Commands[T]['returnType']}> {
|
): Promise<{result: Commands[T]['returnType']}> {
|
||||||
assert(!this.#closed, 'Protocol error: Connection closed.');
|
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({
|
const stringifiedMessage = JSON.stringify({
|
||||||
id,
|
id,
|
||||||
method,
|
method,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
|
||||||
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
|
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
|
||||||
import type {Awaitable, NodeFor} from '../common/types.js';
|
import type {Awaitable, NodeFor} from '../common/types.js';
|
||||||
import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
|
import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
|
||||||
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
|
|
||||||
import {BidiCdpSession} from './CDPSession.js';
|
import {BidiCdpSession} from './CDPSession.js';
|
||||||
import type {BrowsingContext} from './core/BrowsingContext.js';
|
import type {BrowsingContext} from './core/BrowsingContext.js';
|
||||||
|
|
@ -114,13 +115,13 @@ export class BidiFrame extends Frame {
|
||||||
this.browsingContext.on('request', ({request}) => {
|
this.browsingContext.on('request', ({request}) => {
|
||||||
const httpRequest = BidiHTTPRequest.from(request, this);
|
const httpRequest = BidiHTTPRequest.from(request, this);
|
||||||
request.once('success', () => {
|
request.once('success', () => {
|
||||||
// SAFETY: BidiHTTPRequest will create this before here.
|
|
||||||
this.page().trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
|
this.page().trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
|
||||||
});
|
});
|
||||||
|
|
||||||
request.once('error', () => {
|
request.once('error', () => {
|
||||||
this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
|
this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
|
||||||
});
|
});
|
||||||
|
void httpRequest.finalizeInterceptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.browsingContext.on('navigation', ({navigation}) => {
|
this.browsingContext.on('navigation', ({navigation}) => {
|
||||||
|
|
@ -300,10 +301,18 @@ export class BidiFrame extends Frame {
|
||||||
// readiness=interactive.
|
// readiness=interactive.
|
||||||
//
|
//
|
||||||
// Related: https://bugzilla.mozilla.org/show_bug.cgi?id=1846601
|
// Related: https://bugzilla.mozilla.org/show_bug.cgi?id=1846601
|
||||||
this.browsingContext.navigate(
|
this.browsingContext
|
||||||
url,
|
.navigate(url, Bidi.BrowsingContext.ReadinessState.Interactive)
|
||||||
Bidi.BrowsingContext.ReadinessState.Interactive
|
.catch(error => {
|
||||||
),
|
if (
|
||||||
|
isErrorLike(error) &&
|
||||||
|
error.message.includes('net::ERR_HTTP_RESPONSE_CODE_FAILURE')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}),
|
||||||
]).catch(
|
]).catch(
|
||||||
rewriteNavigationError(
|
rewriteNavigationError(
|
||||||
url,
|
url,
|
||||||
|
|
@ -351,11 +360,7 @@ export class BidiFrame extends Frame {
|
||||||
}),
|
}),
|
||||||
raceWith(
|
raceWith(
|
||||||
fromEmitterEvent(navigation, 'fragment'),
|
fromEmitterEvent(navigation, 'fragment'),
|
||||||
fromEmitterEvent(navigation, 'failed').pipe(
|
fromEmitterEvent(navigation, 'failed'),
|
||||||
map(({url}) => {
|
|
||||||
throw new Error(`Navigation failed: ${url}`);
|
|
||||||
})
|
|
||||||
),
|
|
||||||
fromEmitterEvent(navigation, 'aborted').pipe(
|
fromEmitterEvent(navigation, 'aborted').pipe(
|
||||||
map(({url}) => {
|
map(({url}) => {
|
||||||
throw new Error(`Navigation aborted: ${url}`);
|
throw new Error(`Navigation aborted: ${url}`);
|
||||||
|
|
@ -401,11 +406,9 @@ export class BidiFrame extends Frame {
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const httpRequest = requests.get(request)!;
|
const lastRequest = request.lastRedirect ?? request;
|
||||||
const lastRedirect = httpRequest.redirectChain().at(-1);
|
const httpRequest = requests.get(lastRequest)!;
|
||||||
return (
|
return httpRequest.response();
|
||||||
lastRedirect !== undefined ? lastRedirect : httpRequest
|
|
||||||
).response();
|
|
||||||
}),
|
}),
|
||||||
raceWith(
|
raceWith(
|
||||||
timeout(ms),
|
timeout(ms),
|
||||||
|
|
@ -471,6 +474,7 @@ export class BidiFrame extends Frame {
|
||||||
targetId: this._id,
|
targetId: this._id,
|
||||||
flatten: true,
|
flatten: true,
|
||||||
});
|
});
|
||||||
|
await this.browsingContext.subscribe([Bidi.ChromiumBidi.BiDiModule.Cdp]);
|
||||||
return new BidiCdpSession(this, sessionId);
|
return new BidiCdpSession(this, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,12 @@ import type {
|
||||||
ContinueRequestOverrides,
|
ContinueRequestOverrides,
|
||||||
ResponseForRequest,
|
ResponseForRequest,
|
||||||
} from '../api/HTTPRequest.js';
|
} 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 {PageEvent} from '../api/Page.js';
|
||||||
import {UnsupportedOperation} from '../common/Errors.js';
|
import {UnsupportedOperation} from '../common/Errors.js';
|
||||||
|
|
||||||
|
|
@ -26,41 +31,61 @@ export const requests = new WeakMap<Request, BidiHTTPRequest>();
|
||||||
export class BidiHTTPRequest extends HTTPRequest {
|
export class BidiHTTPRequest extends HTTPRequest {
|
||||||
static from(
|
static from(
|
||||||
bidiRequest: Request,
|
bidiRequest: Request,
|
||||||
frame: BidiFrame | undefined
|
frame: BidiFrame,
|
||||||
|
redirect?: BidiHTTPRequest
|
||||||
): BidiHTTPRequest {
|
): BidiHTTPRequest {
|
||||||
const request = new BidiHTTPRequest(bidiRequest, frame);
|
const request = new BidiHTTPRequest(bidiRequest, frame, redirect);
|
||||||
request.#initialize();
|
request.#initialize();
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
#redirectBy: BidiHTTPRequest | undefined;
|
||||||
#redirect: BidiHTTPRequest | undefined;
|
|
||||||
#response: BidiHTTPResponse | null = null;
|
#response: BidiHTTPResponse | null = null;
|
||||||
override readonly id: string;
|
override readonly id: string;
|
||||||
readonly #frame: BidiFrame | undefined;
|
readonly #frame: BidiFrame;
|
||||||
readonly #request: Request;
|
readonly #request: Request;
|
||||||
|
|
||||||
private constructor(request: Request, frame: BidiFrame | undefined) {
|
private constructor(
|
||||||
|
request: Request,
|
||||||
|
frame: BidiFrame,
|
||||||
|
redirectBy?: BidiHTTPRequest
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
requests.set(request, this);
|
requests.set(request, this);
|
||||||
|
|
||||||
|
this.interception.enabled = request.isBlocked;
|
||||||
|
|
||||||
this.#request = request;
|
this.#request = request;
|
||||||
this.#frame = frame;
|
this.#frame = frame;
|
||||||
|
this.#redirectBy = redirectBy;
|
||||||
this.id = request.id;
|
this.id = request.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
override get client(): CDPSession {
|
override get client(): CDPSession {
|
||||||
throw new UnsupportedOperation();
|
return this.#frame.client;
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
this.#request.on('redirect', request => {
|
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.#request.once('success', data => {
|
||||||
this.#response = BidiHTTPResponse.from(data, this);
|
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 {
|
override url(): string {
|
||||||
|
|
@ -68,7 +93,7 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
override resourceType(): ResourceType {
|
override resourceType(): ResourceType {
|
||||||
return this.initiator().type.toLowerCase() as ResourceType;
|
throw new UnsupportedOperation();
|
||||||
}
|
}
|
||||||
|
|
||||||
override method(): string {
|
override method(): string {
|
||||||
|
|
@ -87,12 +112,19 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||||
throw new UnsupportedOperation();
|
throw new UnsupportedOperation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get #extraHTTPHeaders(): Record<string, string> {
|
||||||
|
return this.#frame?.page()._extraHTTPHeaders ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
override headers(): Record<string, string> {
|
override headers(): Record<string, string> {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
for (const header of this.#request.headers) {
|
for (const header of this.#request.headers) {
|
||||||
headers[header.name.toLowerCase()] = header.value.value;
|
headers[header.name.toLowerCase()] = header.value.value;
|
||||||
}
|
}
|
||||||
return headers;
|
return {
|
||||||
|
...headers,
|
||||||
|
...this.#extraHTTPHeaders,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
override response(): BidiHTTPResponse | null {
|
override response(): BidiHTTPResponse | null {
|
||||||
|
|
@ -115,65 +147,167 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
override redirectChain(): BidiHTTPRequest[] {
|
override redirectChain(): BidiHTTPRequest[] {
|
||||||
if (this.#redirect === undefined) {
|
if (this.#redirectBy === undefined) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const redirects = [this.#redirect];
|
const redirects = [this.#redirectBy];
|
||||||
for (const redirect of redirects) {
|
for (const redirect of redirects) {
|
||||||
if (redirect.#redirect !== undefined) {
|
if (redirect.#redirectBy !== undefined) {
|
||||||
redirects.push(redirect.#redirect);
|
redirects.push(redirect.#redirectBy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return redirects;
|
return redirects;
|
||||||
}
|
}
|
||||||
|
|
||||||
override enqueueInterceptAction(
|
override frame(): BidiFrame {
|
||||||
pendingHandler: () => void | PromiseLike<unknown>
|
return this.#frame;
|
||||||
): void {
|
|
||||||
// Execute the handler when interception is not supported
|
|
||||||
void pendingHandler();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override frame(): BidiFrame | null {
|
override async continue(
|
||||||
return this.#frame ?? null;
|
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 {
|
override async _continue(
|
||||||
throw new UnsupportedOperation();
|
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 {
|
override async _abort(): Promise<void> {
|
||||||
throw new UnsupportedOperation();
|
this.interception.handled = true;
|
||||||
|
return await this.#request.failRequest().catch(error => {
|
||||||
|
this.interception.handled = false;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override responseForRequest(): never {
|
override async _respond(
|
||||||
throw new UnsupportedOperation();
|
response: Partial<ResponseForRequest>,
|
||||||
}
|
|
||||||
|
|
||||||
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>,
|
|
||||||
_priority?: number
|
_priority?: number
|
||||||
): never {
|
): Promise<void> {
|
||||||
throw new UnsupportedOperation();
|
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() {
|
#initialize() {
|
||||||
|
if (this.#data.fromCache) {
|
||||||
|
this.#request
|
||||||
|
.frame()
|
||||||
|
?.page()
|
||||||
|
.trustedEmitter.emit(PageEvent.RequestServedFromCache, this.#request);
|
||||||
|
}
|
||||||
this.#request.frame()?.page().trustedEmitter.emit(PageEvent.Response, this);
|
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 {WaitForOptions} from '../api/Frame.js';
|
||||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||||
import type {
|
import type {
|
||||||
MediaFeature,
|
Credentials,
|
||||||
GeolocationOptions,
|
GeolocationOptions,
|
||||||
|
MediaFeature,
|
||||||
PageEvents,
|
PageEvents,
|
||||||
} from '../api/Page.js';
|
} from '../api/Page.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -27,13 +28,22 @@ import {Accessibility} from '../cdp/Accessibility.js';
|
||||||
import {Coverage} from '../cdp/Coverage.js';
|
import {Coverage} from '../cdp/Coverage.js';
|
||||||
import {EmulationManager} from '../cdp/EmulationManager.js';
|
import {EmulationManager} from '../cdp/EmulationManager.js';
|
||||||
import {Tracing} from '../cdp/Tracing.js';
|
import {Tracing} from '../cdp/Tracing.js';
|
||||||
import type {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js';
|
import type {
|
||||||
import type {DeleteCookiesRequest} from '../common/Cookie.js';
|
Cookie,
|
||||||
|
CookieParam,
|
||||||
|
CookieSameSite,
|
||||||
|
DeleteCookiesRequest,
|
||||||
|
} from '../common/Cookie.js';
|
||||||
import {UnsupportedOperation} from '../common/Errors.js';
|
import {UnsupportedOperation} from '../common/Errors.js';
|
||||||
import {EventEmitter} from '../common/EventEmitter.js';
|
import {EventEmitter} from '../common/EventEmitter.js';
|
||||||
import type {PDFOptions} from '../common/PDFOptions.js';
|
import type {PDFOptions} from '../common/PDFOptions.js';
|
||||||
import type {Awaitable} from '../common/types.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 type {Viewport} from '../common/Viewport.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {bubble} from '../util/decorators.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 {BidiBrowserContext} from './BrowserContext.js';
|
||||||
import type {BidiCdpSession} from './CDPSession.js';
|
import type {BidiCdpSession} from './CDPSession.js';
|
||||||
import type {BrowsingContext} from './core/BrowsingContext.js';
|
import type {BrowsingContext} from './core/BrowsingContext.js';
|
||||||
import {BidiElementHandle} from './ElementHandle.js';
|
|
||||||
import {BidiFrame} from './Frame.js';
|
import {BidiFrame} from './Frame.js';
|
||||||
import type {BidiHTTPResponse} from './HTTPResponse.js';
|
import type {BidiHTTPResponse} from './HTTPResponse.js';
|
||||||
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
|
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
|
||||||
|
|
@ -161,21 +170,28 @@ export class BidiPage extends Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
async focusedFrame(): Promise<BidiFrame> {
|
async focusedFrame(): Promise<BidiFrame> {
|
||||||
using frame = await this.mainFrame()
|
using handle = (await this.mainFrame()
|
||||||
.isolatedRealm()
|
.isolatedRealm()
|
||||||
.evaluateHandle(() => {
|
.evaluateHandle(() => {
|
||||||
let frame: HTMLIFrameElement | undefined;
|
let win = window;
|
||||||
let win: Window | null = window;
|
while (
|
||||||
while (win?.document.activeElement instanceof HTMLIFrameElement) {
|
win.document.activeElement instanceof win.HTMLIFrameElement ||
|
||||||
frame = win.document.activeElement;
|
win.document.activeElement instanceof win.HTMLFrameElement
|
||||||
win = frame.contentWindow;
|
) {
|
||||||
|
if (win.document.activeElement.contentWindow === null) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return frame;
|
win = win.document.activeElement.contentWindow as typeof win;
|
||||||
|
}
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
if (!(frame instanceof BidiElementHandle)) {
|
assert(frame);
|
||||||
return this.mainFrame();
|
return frame;
|
||||||
}
|
|
||||||
return await frame.contentFrame();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override frames(): BidiFrame[] {
|
override frames(): BidiFrame[] {
|
||||||
|
|
@ -311,6 +327,17 @@ export class BidiPage extends Page {
|
||||||
preferCSSPageSize,
|
preferCSSPageSize,
|
||||||
} = parsePDFOptions(options, 'cm');
|
} = parsePDFOptions(options, 'cm');
|
||||||
const pageRanges = ranges ? ranges.split(', ') : [];
|
const pageRanges = ranges ? ranges.split(', ') : [];
|
||||||
|
|
||||||
|
await firstValueFrom(
|
||||||
|
from(
|
||||||
|
this.mainFrame()
|
||||||
|
.isolatedRealm()
|
||||||
|
.evaluate(() => {
|
||||||
|
return document.fonts.ready;
|
||||||
|
})
|
||||||
|
).pipe(raceWith(timeout(ms)))
|
||||||
|
);
|
||||||
|
|
||||||
const data = await firstValueFrom(
|
const data = await firstValueFrom(
|
||||||
from(
|
from(
|
||||||
this.#frame.browsingContext.print({
|
this.#frame.browsingContext.print({
|
||||||
|
|
@ -489,8 +516,71 @@ export class BidiPage extends Page {
|
||||||
return [...this.#workers];
|
return [...this.#workers];
|
||||||
}
|
}
|
||||||
|
|
||||||
override setRequestInterception(): never {
|
#userInterception?: string;
|
||||||
throw new UnsupportedOperation();
|
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 {
|
override setDragInterception(): never {
|
||||||
|
|
@ -603,14 +693,6 @@ export class BidiPage extends Page {
|
||||||
await this.#frame.removeExposedFunction(name);
|
await this.#frame.removeExposedFunction(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
override authenticate(): never {
|
|
||||||
throw new UnsupportedOperation();
|
|
||||||
}
|
|
||||||
|
|
||||||
override setExtraHTTPHeaders(): never {
|
|
||||||
throw new UnsupportedOperation();
|
|
||||||
}
|
|
||||||
|
|
||||||
override metrics(): never {
|
override metrics(): never {
|
||||||
throw new UnsupportedOperation();
|
throw new UnsupportedOperation();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,20 +51,17 @@ export class Browser extends EventEmitter<{
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
#closed = false;
|
#closed = false;
|
||||||
#reason: string | undefined;
|
#reason: string | undefined;
|
||||||
readonly #disposables = new DisposableStack();
|
readonly #disposables = new DisposableStack();
|
||||||
readonly #userContexts = new Map<string, UserContext>();
|
readonly #userContexts = new Map<string, UserContext>();
|
||||||
readonly session: Session;
|
readonly session: Session;
|
||||||
readonly #sharedWorkers = new Map<string, SharedWorkerRealm>();
|
readonly #sharedWorkers = new Map<string, SharedWorkerRealm>();
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(session: Session) {
|
private constructor(session: Session) {
|
||||||
super();
|
super();
|
||||||
// keep-sorted start
|
|
||||||
this.session = session;
|
this.session = session;
|
||||||
// keep-sorted end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #initialize() {
|
async #initialize() {
|
||||||
|
|
@ -141,7 +138,6 @@ export class Browser extends EventEmitter<{
|
||||||
return userContext;
|
return userContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start block=yes
|
|
||||||
get closed(): boolean {
|
get closed(): boolean {
|
||||||
return this.#closed;
|
return this.#closed;
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +154,6 @@ export class Browser extends EventEmitter<{
|
||||||
get userContexts(): Iterable<UserContext> {
|
get userContexts(): Iterable<UserContext> {
|
||||||
return this.#userContexts.values();
|
return this.#userContexts.values();
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
@inertIfDisposed
|
@inertIfDisposed
|
||||||
dispose(reason?: string, closed = false): void {
|
dispose(reason?: string, closed = false): void {
|
||||||
|
|
@ -199,6 +194,16 @@ export class Browser extends EventEmitter<{
|
||||||
return script;
|
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 => {
|
@throwIfDisposed<Browser>(browser => {
|
||||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
return browser.#reason!;
|
return browser.#reason!;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ import {Request} from './Request.js';
|
||||||
import type {UserContext} from './UserContext.js';
|
import type {UserContext} from './UserContext.js';
|
||||||
import {UserPrompt} from './UserPrompt.js';
|
import {UserPrompt} from './UserPrompt.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type AddInterceptOptions = Omit<
|
||||||
|
Bidi.Network.AddInterceptParameters,
|
||||||
|
'contexts'
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
@ -121,7 +129,6 @@ export class BrowsingContext extends EventEmitter<{
|
||||||
return browsingContext;
|
return browsingContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
#navigation: Navigation | undefined;
|
#navigation: Navigation | undefined;
|
||||||
#reason?: string;
|
#reason?: string;
|
||||||
#url: string;
|
#url: string;
|
||||||
|
|
@ -133,7 +140,6 @@ export class BrowsingContext extends EventEmitter<{
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly parent: BrowsingContext | undefined;
|
readonly parent: BrowsingContext | undefined;
|
||||||
readonly userContext: UserContext;
|
readonly userContext: UserContext;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
context: UserContext,
|
context: UserContext,
|
||||||
|
|
@ -142,12 +148,11 @@ export class BrowsingContext extends EventEmitter<{
|
||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
// keep-sorted start
|
|
||||||
this.#url = url;
|
this.#url = url;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.userContext = context;
|
this.userContext = context;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
this.defaultRealm = this.#createWindowRealm();
|
this.defaultRealm = this.#createWindowRealm();
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +280,6 @@ export class BrowsingContext extends EventEmitter<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start block=yes
|
|
||||||
get #session() {
|
get #session() {
|
||||||
return this.userContext.browser.session;
|
return this.userContext.browser.session;
|
||||||
}
|
}
|
||||||
|
|
@ -306,7 +310,6 @@ export class BrowsingContext extends EventEmitter<{
|
||||||
get url(): string {
|
get url(): string {
|
||||||
return this.#url;
|
return this.#url;
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
#createWindowRealm(sandbox?: string) {
|
#createWindowRealm(sandbox?: string) {
|
||||||
const realm = WindowRealm.from(this, sandbox);
|
const realm = WindowRealm.from(this, sandbox);
|
||||||
|
|
@ -478,11 +481,26 @@ export class BrowsingContext extends EventEmitter<{
|
||||||
functionDeclaration,
|
functionDeclaration,
|
||||||
{
|
{
|
||||||
...options,
|
...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 => {
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
// SAFETY: Disposal implies this exists.
|
// SAFETY: Disposal implies this exists.
|
||||||
return context.#reason!;
|
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 {
|
[disposeSymbol](): void {
|
||||||
this.#reason ??=
|
this.#reason ??=
|
||||||
'Browsing context already closed, probably because the user context closed.';
|
'Browsing context already closed, probably because the user context closed.';
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,31 @@ export interface Commands {
|
||||||
params: Bidi.Storage.SetCookieParameters;
|
params: Bidi.Storage.SetCookieParameters;
|
||||||
returnType: 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 {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
import {inertIfDisposed} from '../../util/decorators.js';
|
import {inertIfDisposed} from '../../util/decorators.js';
|
||||||
import {Deferred} from '../../util/Deferred.js';
|
|
||||||
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import type {BrowsingContext} from './BrowsingContext.js';
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
|
|
@ -39,19 +38,16 @@ export class Navigation extends EventEmitter<{
|
||||||
return navigation;
|
return navigation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
#request: Request | undefined;
|
#request: Request | undefined;
|
||||||
#navigation: Navigation | undefined;
|
#navigation: Navigation | undefined;
|
||||||
readonly #browsingContext: BrowsingContext;
|
readonly #browsingContext: BrowsingContext;
|
||||||
readonly #disposables = new DisposableStack();
|
readonly #disposables = new DisposableStack();
|
||||||
readonly #id = new Deferred<string | null>();
|
#id?: string | null;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(context: BrowsingContext) {
|
private constructor(context: BrowsingContext) {
|
||||||
super();
|
super();
|
||||||
// keep-sorted start
|
|
||||||
this.#browsingContext = context;
|
this.#browsingContext = context;
|
||||||
// keep-sorted end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
|
|
@ -69,7 +65,6 @@ export class Navigation extends EventEmitter<{
|
||||||
browsingContextEmitter.on('request', ({request}) => {
|
browsingContextEmitter.on('request', ({request}) => {
|
||||||
if (
|
if (
|
||||||
request.navigation === undefined ||
|
request.navigation === undefined ||
|
||||||
this.#request !== undefined ||
|
|
||||||
// If a request with a navigation ID comes in, then the navigation ID is
|
// If a request with a navigation ID comes in, then the navigation ID is
|
||||||
// for this navigation.
|
// for this navigation.
|
||||||
!this.#matches(request.navigation)
|
!this.#matches(request.navigation)
|
||||||
|
|
@ -79,6 +74,13 @@ export class Navigation extends EventEmitter<{
|
||||||
|
|
||||||
this.#request = request;
|
this.#request = request;
|
||||||
this.emit('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(
|
const sessionEmitter = this.#disposables.use(
|
||||||
|
|
@ -139,14 +141,13 @@ export class Navigation extends EventEmitter<{
|
||||||
if (this.#navigation !== undefined && !this.#navigation.disposed) {
|
if (this.#navigation !== undefined && !this.#navigation.disposed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.#id.resolved()) {
|
if (this.#id === undefined) {
|
||||||
this.#id.resolve(navigation);
|
this.#id = navigation;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.#id.value() === navigation;
|
return this.#id === navigation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start block=yes
|
|
||||||
get #session() {
|
get #session() {
|
||||||
return this.#browsingContext.userContext.browser.session;
|
return this.#browsingContext.userContext.browser.session;
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +160,6 @@ export class Navigation extends EventEmitter<{
|
||||||
get navigation(): Navigation | undefined {
|
get navigation(): Navigation | undefined {
|
||||||
return this.#navigation;
|
return this.#navigation;
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
@inertIfDisposed
|
@inertIfDisposed
|
||||||
private dispose(): void {
|
private dispose(): void {
|
||||||
|
|
|
||||||
|
|
@ -44,22 +44,19 @@ export abstract class Realm extends EventEmitter<{
|
||||||
/** Emitted when a shared worker is created in the realm. */
|
/** Emitted when a shared worker is created in the realm. */
|
||||||
sharedworker: SharedWorkerRealm;
|
sharedworker: SharedWorkerRealm;
|
||||||
}> {
|
}> {
|
||||||
// keep-sorted start
|
|
||||||
#reason?: string;
|
#reason?: string;
|
||||||
protected readonly disposables = new DisposableStack();
|
protected readonly disposables = new DisposableStack();
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly origin: string;
|
readonly origin: string;
|
||||||
// keep-sorted end
|
protected executionContextId?: number;
|
||||||
|
|
||||||
protected constructor(id: string, origin: string) {
|
protected constructor(id: string, origin: string) {
|
||||||
super();
|
super();
|
||||||
// keep-sorted start
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.origin = origin;
|
this.origin = origin;
|
||||||
// keep-sorted end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start block=yes
|
|
||||||
get disposed(): boolean {
|
get disposed(): boolean {
|
||||||
return this.#reason !== undefined;
|
return this.#reason !== undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +64,6 @@ export abstract class Realm extends EventEmitter<{
|
||||||
get target(): Bidi.Script.Target {
|
get target(): Bidi.Script.Target {
|
||||||
return {realm: this.id};
|
return {realm: this.id};
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
@inertIfDisposed
|
@inertIfDisposed
|
||||||
protected dispose(reason?: string): void {
|
protected dispose(reason?: string): void {
|
||||||
|
|
@ -127,11 +123,15 @@ export abstract class Realm extends EventEmitter<{
|
||||||
return realm.#reason!;
|
return realm.#reason!;
|
||||||
})
|
})
|
||||||
async resolveExecutionContextId(): Promise<number> {
|
async resolveExecutionContextId(): Promise<number> {
|
||||||
|
if (!this.executionContextId) {
|
||||||
const {result} = await (this.session.connection as BidiConnection).send(
|
const {result} = await (this.session.connection as BidiConnection).send(
|
||||||
'cdp.resolveRealm',
|
'cdp.resolveRealm',
|
||||||
{realm: this.id}
|
{realm: this.id}
|
||||||
);
|
);
|
||||||
return result.executionContextId;
|
this.executionContextId = result.executionContextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.executionContextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
[disposeSymbol](): void {
|
[disposeSymbol](): void {
|
||||||
|
|
@ -154,19 +154,16 @@ export class WindowRealm extends Realm {
|
||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
readonly browsingContext: BrowsingContext;
|
readonly browsingContext: BrowsingContext;
|
||||||
readonly sandbox?: string;
|
readonly sandbox?: string;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||||
|
|
||||||
private constructor(context: BrowsingContext, sandbox?: string) {
|
private constructor(context: BrowsingContext, sandbox?: string) {
|
||||||
super('', '');
|
super('', '');
|
||||||
// keep-sorted start
|
|
||||||
this.browsingContext = context;
|
this.browsingContext = context;
|
||||||
this.sandbox = sandbox;
|
this.sandbox = sandbox;
|
||||||
// keep-sorted end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialize(): void {
|
#initialize(): void {
|
||||||
|
|
@ -188,6 +185,7 @@ export class WindowRealm extends Realm {
|
||||||
}
|
}
|
||||||
(this as any).id = info.realm;
|
(this as any).id = info.realm;
|
||||||
(this as any).origin = info.origin;
|
(this as any).origin = info.origin;
|
||||||
|
this.executionContextId = undefined;
|
||||||
this.emit('updated', this);
|
this.emit('updated', this);
|
||||||
});
|
});
|
||||||
sessionEmitter.on('script.realmCreated', info => {
|
sessionEmitter.on('script.realmCreated', info => {
|
||||||
|
|
@ -242,10 +240,8 @@ export class DedicatedWorkerRealm extends Realm {
|
||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||||
readonly owners: Set<DedicatedWorkerOwnerRealm>;
|
readonly owners: Set<DedicatedWorkerOwnerRealm>;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
owner: DedicatedWorkerOwnerRealm,
|
owner: DedicatedWorkerOwnerRealm,
|
||||||
|
|
@ -300,10 +296,8 @@ export class SharedWorkerRealm extends Realm {
|
||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||||
readonly browser: Browser;
|
readonly browser: Browser;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(browser: Browser, id: string, origin: string) {
|
private constructor(browser: Browser, id: string, origin: string) {
|
||||||
super(id, origin);
|
super(id, origin);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ export class Request extends EventEmitter<{
|
||||||
/** Emitted when the request is redirected. */
|
/** Emitted when the request is redirected. */
|
||||||
redirect: Request;
|
redirect: Request;
|
||||||
/** Emitted when the request succeeds. */
|
/** Emitted when the request succeeds. */
|
||||||
|
authenticate: void;
|
||||||
|
/** Emitted when the request succeeds. */
|
||||||
success: Bidi.Network.ResponseData;
|
success: Bidi.Network.ResponseData;
|
||||||
/** Emitted when the request fails. */
|
/** Emitted when the request fails. */
|
||||||
error: string;
|
error: string;
|
||||||
|
|
@ -32,24 +34,21 @@ export class Request extends EventEmitter<{
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
#error?: string;
|
#error?: string;
|
||||||
#redirect?: Request;
|
#redirect?: Request;
|
||||||
#response?: Bidi.Network.ResponseData;
|
#response?: Bidi.Network.ResponseData;
|
||||||
readonly #browsingContext: BrowsingContext;
|
readonly #browsingContext: BrowsingContext;
|
||||||
readonly #disposables = new DisposableStack();
|
readonly #disposables = new DisposableStack();
|
||||||
readonly #event: Bidi.Network.BeforeRequestSentParameters;
|
readonly #event: Bidi.Network.BeforeRequestSentParameters;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
browsingContext: BrowsingContext,
|
browsingContext: BrowsingContext,
|
||||||
event: Bidi.Network.BeforeRequestSentParameters
|
event: Bidi.Network.BeforeRequestSentParameters
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
// keep-sorted start
|
|
||||||
this.#browsingContext = browsingContext;
|
this.#browsingContext = browsingContext;
|
||||||
this.#event = event;
|
this.#event = event;
|
||||||
// keep-sorted end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
|
|
@ -77,6 +76,17 @@ export class Request extends EventEmitter<{
|
||||||
this.emit('redirect', this.#redirect);
|
this.emit('redirect', this.#redirect);
|
||||||
this.dispose();
|
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 => {
|
sessionEmitter.on('network.fetchError', event => {
|
||||||
if (
|
if (
|
||||||
event.context !== this.#browsingContext.id ||
|
event.context !== this.#browsingContext.id ||
|
||||||
|
|
@ -107,7 +117,6 @@ export class Request extends EventEmitter<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start block=yes
|
|
||||||
get #session() {
|
get #session() {
|
||||||
return this.#browsingContext.userContext.browser.session;
|
return this.#browsingContext.userContext.browser.session;
|
||||||
}
|
}
|
||||||
|
|
@ -135,13 +144,82 @@ export class Request extends EventEmitter<{
|
||||||
get redirect(): Request | undefined {
|
get redirect(): Request | undefined {
|
||||||
return this.#redirect;
|
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 {
|
get response(): Bidi.Network.ResponseData | undefined {
|
||||||
return this.#response;
|
return this.#response;
|
||||||
}
|
}
|
||||||
get url(): string {
|
get url(): string {
|
||||||
return this.#event.request.url;
|
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
|
@inertIfDisposed
|
||||||
private dispose(): void {
|
private dispose(): void {
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,9 @@ export class Session
|
||||||
platformName: '',
|
platformName: '',
|
||||||
setWindowRect: false,
|
setWindowRect: false,
|
||||||
webSocketUrl: '',
|
webSocketUrl: '',
|
||||||
|
userAgent: '',
|
||||||
},
|
},
|
||||||
};
|
} satisfies Bidi.Session.NewResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = new Session(connection, result);
|
const session = new Session(connection, result);
|
||||||
|
|
@ -80,21 +81,18 @@ export class Session
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
#reason: string | undefined;
|
#reason: string | undefined;
|
||||||
readonly #disposables = new DisposableStack();
|
readonly #disposables = new DisposableStack();
|
||||||
readonly #info: Bidi.Session.NewResult;
|
readonly #info: Bidi.Session.NewResult;
|
||||||
readonly browser!: Browser;
|
readonly browser!: Browser;
|
||||||
@bubble()
|
@bubble()
|
||||||
accessor connection: Connection;
|
accessor connection: Connection;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
|
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
|
||||||
super();
|
super();
|
||||||
// keep-sorted start
|
|
||||||
this.#info = info;
|
this.#info = info;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
// keep-sorted end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #initialize(): Promise<void> {
|
async #initialize(): Promise<void> {
|
||||||
|
|
@ -120,7 +118,6 @@ export class Session
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start block=yes
|
|
||||||
get capabilities(): Bidi.Session.NewResult['capabilities'] {
|
get capabilities(): Bidi.Session.NewResult['capabilities'] {
|
||||||
return this.#info.capabilities;
|
return this.#info.capabilities;
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +130,6 @@ export class Session
|
||||||
get id(): string {
|
get id(): string {
|
||||||
return this.#info.sessionId;
|
return this.#info.sessionId;
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
@inertIfDisposed
|
@inertIfDisposed
|
||||||
private dispose(reason?: string): void {
|
private dispose(reason?: string): void {
|
||||||
|
|
@ -163,9 +159,27 @@ export class Session
|
||||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
return session.#reason!;
|
return session.#reason!;
|
||||||
})
|
})
|
||||||
async subscribe(events: string[]): Promise<void> {
|
async subscribe(
|
||||||
|
events: [string, ...string[]],
|
||||||
|
contexts?: [string, ...string[]]
|
||||||
|
): Promise<void> {
|
||||||
await this.send('session.subscribe', {
|
await this.send('session.subscribe', {
|
||||||
events,
|
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;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
#reason?: string;
|
#reason?: string;
|
||||||
// Note these are only top-level contexts.
|
// Note these are only top-level contexts.
|
||||||
readonly #browsingContexts = new Map<string, BrowsingContext>();
|
readonly #browsingContexts = new Map<string, BrowsingContext>();
|
||||||
readonly #disposables = new DisposableStack();
|
readonly #disposables = new DisposableStack();
|
||||||
readonly #id: string;
|
readonly #id: string;
|
||||||
readonly browser: Browser;
|
readonly browser: Browser;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(browser: Browser, id: string) {
|
private constructor(browser: Browser, id: string) {
|
||||||
super();
|
super();
|
||||||
// keep-sorted start
|
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
this.browser = browser;
|
this.browser = browser;
|
||||||
// keep-sorted end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
|
|
@ -110,7 +107,6 @@ export class UserContext extends EventEmitter<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start block=yes
|
|
||||||
get #session() {
|
get #session() {
|
||||||
return this.browser.session;
|
return this.browser.session;
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +122,6 @@ export class UserContext extends EventEmitter<{
|
||||||
get id(): string {
|
get id(): string {
|
||||||
return this.#id;
|
return this.#id;
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
@inertIfDisposed
|
@inertIfDisposed
|
||||||
private dispose(reason?: string): void {
|
private dispose(reason?: string): void {
|
||||||
|
|
@ -227,8 +222,7 @@ export class UserContext extends EventEmitter<{
|
||||||
origin,
|
origin,
|
||||||
descriptor,
|
descriptor,
|
||||||
state,
|
state,
|
||||||
// @ts-expect-error not standard implementation.
|
userContext: this.#id,
|
||||||
'goog:userContext': this.#id,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,23 +49,20 @@ export class UserPrompt extends EventEmitter<{
|
||||||
return userPrompt;
|
return userPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
|
||||||
#reason?: string;
|
#reason?: string;
|
||||||
#result?: UserPromptResult;
|
#result?: UserPromptResult;
|
||||||
readonly #disposables = new DisposableStack();
|
readonly #disposables = new DisposableStack();
|
||||||
readonly browsingContext: BrowsingContext;
|
readonly browsingContext: BrowsingContext;
|
||||||
readonly info: Bidi.BrowsingContext.UserPromptOpenedParameters;
|
readonly info: Bidi.BrowsingContext.UserPromptOpenedParameters;
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
context: BrowsingContext,
|
context: BrowsingContext,
|
||||||
info: Bidi.BrowsingContext.UserPromptOpenedParameters
|
info: Bidi.BrowsingContext.UserPromptOpenedParameters
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
// keep-sorted start
|
|
||||||
this.browsingContext = context;
|
this.browsingContext = context;
|
||||||
this.info = info;
|
this.info = info;
|
||||||
// keep-sorted end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
|
|
@ -89,7 +86,6 @@ export class UserPrompt extends EventEmitter<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start block=yes
|
|
||||||
get #session() {
|
get #session() {
|
||||||
return this.browsingContext.userContext.browser.session;
|
return this.browsingContext.userContext.browser.session;
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +101,6 @@ export class UserPrompt extends EventEmitter<{
|
||||||
get result(): UserPromptResult | undefined {
|
get result(): UserPromptResult | undefined {
|
||||||
return this.#result;
|
return this.#result;
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
|
||||||
|
|
||||||
@inertIfDisposed
|
@inertIfDisposed
|
||||||
private dispose(reason?: string): void {
|
private dispose(reason?: string): void {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,16 @@ const queryAXTree = async (
|
||||||
role,
|
role,
|
||||||
});
|
});
|
||||||
return nodes.filter((node: Protocol.Accessibility.AXNode) => {
|
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 {
|
import {
|
||||||
Browser as BrowserBase,
|
Browser as BrowserBase,
|
||||||
BrowserEvent,
|
BrowserEvent,
|
||||||
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
|
|
||||||
type BrowserCloseCallback,
|
type BrowserCloseCallback,
|
||||||
type BrowserContextOptions,
|
type BrowserContextOptions,
|
||||||
type IsPageTargetCallback,
|
type IsPageTargetCallback,
|
||||||
type Permission,
|
|
||||||
type TargetFilterCallback,
|
type TargetFilterCallback,
|
||||||
} from '../api/Browser.js';
|
} 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 {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
|
||||||
import type {Page} from '../api/Page.js';
|
import type {Page} from '../api/Page.js';
|
||||||
import type {Target} from '../api/Target.js';
|
import type {Target} from '../api/Target.js';
|
||||||
import type {Viewport} from '../common/Viewport.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 {ChromeTargetManager} from './ChromeTargetManager.js';
|
||||||
import type {Connection} from './Connection.js';
|
import type {Connection} from './Connection.js';
|
||||||
import {FirefoxTargetManager} from './FirefoxTargetManager.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: {
|
options: {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
|
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
|
||||||
|
ignoreSameDocumentNavigation?: boolean;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<HTTPResponse | null> {
|
): Promise<HTTPResponse | null> {
|
||||||
const {
|
const {
|
||||||
|
|
@ -220,14 +221,22 @@ export class CdpFrame extends Frame {
|
||||||
);
|
);
|
||||||
const error = await Deferred.race([
|
const error = await Deferred.race([
|
||||||
watcher.terminationPromise(),
|
watcher.terminationPromise(),
|
||||||
watcher.sameDocumentNavigationPromise(),
|
...(options.ignoreSameDocumentNavigation
|
||||||
|
? []
|
||||||
|
: [watcher.sameDocumentNavigationPromise()]),
|
||||||
watcher.newDocumentNavigationPromise(),
|
watcher.newDocumentNavigationPromise(),
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
if (error) {
|
if (error) {
|
||||||
throw 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 {
|
} finally {
|
||||||
watcher.dispose();
|
watcher.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export class FrameTree<FrameType extends Frame> {
|
||||||
// frameID -> childFrameIDs
|
// frameID -> childFrameIDs
|
||||||
#childIds = new Map<string, Set<string>>();
|
#childIds = new Map<string, Set<string>>();
|
||||||
#mainFrame?: FrameType;
|
#mainFrame?: FrameType;
|
||||||
|
#isMainFrameStale = false;
|
||||||
#waitRequests = new Map<string, Set<Deferred<FrameType>>>();
|
#waitRequests = new Map<string, Set<Deferred<FrameType>>>();
|
||||||
|
|
||||||
getMainFrame(): FrameType | undefined {
|
getMainFrame(): FrameType | undefined {
|
||||||
|
|
@ -59,8 +60,9 @@ export class FrameTree<FrameType extends Frame> {
|
||||||
this.#childIds.set(frame._parentId, new Set());
|
this.#childIds.set(frame._parentId, new Set());
|
||||||
}
|
}
|
||||||
this.#childIds.get(frame._parentId)!.add(frame._id);
|
this.#childIds.get(frame._parentId)!.add(frame._id);
|
||||||
} else if (!this.#mainFrame) {
|
} else if (!this.#mainFrame || this.#isMainFrameStale) {
|
||||||
this.#mainFrame = frame;
|
this.#mainFrame = frame;
|
||||||
|
this.#isMainFrameStale = false;
|
||||||
}
|
}
|
||||||
this.#waitRequests.get(frame._id)?.forEach(request => {
|
this.#waitRequests.get(frame._id)?.forEach(request => {
|
||||||
return request.resolve(frame);
|
return request.resolve(frame);
|
||||||
|
|
@ -73,7 +75,7 @@ export class FrameTree<FrameType extends Frame> {
|
||||||
if (frame._parentId) {
|
if (frame._parentId) {
|
||||||
this.#childIds.get(frame._parentId)?.delete(frame._id);
|
this.#childIds.get(frame._parentId)?.delete(frame._id);
|
||||||
} else {
|
} 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 {Frame} from '../api/Frame.js';
|
||||||
import {
|
import {
|
||||||
type ContinueRequestOverrides,
|
type ContinueRequestOverrides,
|
||||||
type ErrorCode,
|
|
||||||
headersArray,
|
headersArray,
|
||||||
HTTPRequest,
|
HTTPRequest,
|
||||||
InterceptResolutionAction,
|
|
||||||
type InterceptResolutionState,
|
|
||||||
type ResourceType,
|
type ResourceType,
|
||||||
type ResponseForRequest,
|
type ResponseForRequest,
|
||||||
STATUS_TEXTS,
|
STATUS_TEXTS,
|
||||||
|
handleError,
|
||||||
} from '../api/HTTPRequest.js';
|
} from '../api/HTTPRequest.js';
|
||||||
import type {ProtocolError} from '../common/Errors.js';
|
|
||||||
import {debugError, isString} from '../common/util.js';
|
import {debugError, isString} from '../common/util.js';
|
||||||
import {assert} from '../util/assert.js';
|
|
||||||
|
|
||||||
import type {CdpHTTPResponse} from './HTTPResponse.js';
|
import type {CdpHTTPResponse} from './HTTPResponse.js';
|
||||||
|
|
||||||
|
|
@ -34,8 +30,7 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
|
|
||||||
#client: CDPSession;
|
#client: CDPSession;
|
||||||
#isNavigationRequest: boolean;
|
#isNavigationRequest: boolean;
|
||||||
#allowInterception: boolean;
|
|
||||||
#interceptionHandled = false;
|
|
||||||
#url: string;
|
#url: string;
|
||||||
#resourceType: ResourceType;
|
#resourceType: ResourceType;
|
||||||
|
|
||||||
|
|
@ -44,13 +39,6 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
#postData?: string;
|
#postData?: string;
|
||||||
#headers: Record<string, string> = {};
|
#headers: Record<string, string> = {};
|
||||||
#frame: Frame | null;
|
#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;
|
#initiator?: Protocol.Network.Initiator;
|
||||||
|
|
||||||
override get client(): CDPSession {
|
override get client(): CDPSession {
|
||||||
|
|
@ -96,7 +84,6 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
this.#isNavigationRequest =
|
this.#isNavigationRequest =
|
||||||
data.requestId === data.loaderId && data.type === 'Document';
|
data.requestId === data.loaderId && data.type === 'Document';
|
||||||
this._interceptionId = interceptionId;
|
this._interceptionId = interceptionId;
|
||||||
this.#allowInterception = allowInterception;
|
|
||||||
this.#url = data.request.url;
|
this.#url = data.request.url;
|
||||||
this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
|
this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
|
||||||
this.#method = data.request.method;
|
this.#method = data.request.method;
|
||||||
|
|
@ -104,10 +91,10 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
this.#hasPostData = data.request.hasPostData ?? false;
|
this.#hasPostData = data.request.hasPostData ?? false;
|
||||||
this.#frame = frame;
|
this.#frame = frame;
|
||||||
this._redirectChain = redirectChain;
|
this._redirectChain = redirectChain;
|
||||||
this.#continueRequestOverrides = {};
|
|
||||||
this.#interceptHandlers = [];
|
|
||||||
this.#initiator = data.initiator;
|
this.#initiator = data.initiator;
|
||||||
|
|
||||||
|
this.interception.enabled = allowInterception;
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data.request.headers)) {
|
for (const [key, value] of Object.entries(data.request.headers)) {
|
||||||
this.#headers[key.toLowerCase()] = value;
|
this.#headers[key.toLowerCase()] = value;
|
||||||
}
|
}
|
||||||
|
|
@ -117,59 +104,6 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
return this.#url;
|
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 {
|
override resourceType(): ResourceType {
|
||||||
return this.#resourceType;
|
return this.#resourceType;
|
||||||
}
|
}
|
||||||
|
|
@ -231,46 +165,12 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
override async continue(
|
/**
|
||||||
overrides: ContinueRequestOverrides = {},
|
* @internal
|
||||||
priority?: number
|
*/
|
||||||
): Promise<void> {
|
async _continue(overrides: ContinueRequestOverrides = {}): 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> {
|
|
||||||
const {url, method, postData, headers} = overrides;
|
const {url, method, postData, headers} = overrides;
|
||||||
this.#interceptionHandled = true;
|
this.interception.handled = true;
|
||||||
|
|
||||||
const postDataBinaryBase64 = postData
|
const postDataBinaryBase64 = postData
|
||||||
? Buffer.from(postData).toString('base64')
|
? Buffer.from(postData).toString('base64')
|
||||||
|
|
@ -290,45 +190,13 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
headers: headers ? headersArray(headers) : undefined,
|
headers: headers ? headersArray(headers) : undefined,
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.#interceptionHandled = false;
|
this.interception.handled = false;
|
||||||
return handleError(error);
|
return handleError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override async respond(
|
async _respond(response: Partial<ResponseForRequest>): Promise<void> {
|
||||||
response: Partial<ResponseForRequest>,
|
this.interception.handled = true;
|
||||||
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;
|
|
||||||
|
|
||||||
const responseBody: Buffer | null =
|
const responseBody: Buffer | null =
|
||||||
response.body && isString(response.body)
|
response.body && isString(response.body)
|
||||||
|
|
@ -371,43 +239,15 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
body: responseBody ? responseBody.toString('base64') : undefined,
|
body: responseBody ? responseBody.toString('base64') : undefined,
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.#interceptionHandled = false;
|
this.interception.handled = false;
|
||||||
return handleError(error);
|
return handleError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override async abort(
|
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(
|
|
||||||
errorReason: Protocol.Network.ErrorReason | null
|
errorReason: Protocol.Network.ErrorReason | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.#interceptionHandled = true;
|
this.interception.handled = true;
|
||||||
if (this._interceptionId === undefined) {
|
if (this._interceptionId === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
|
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
|
||||||
|
|
@ -421,30 +261,3 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||||
.catch(handleError);
|
.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 {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
|
||||||
import type {Frame} from '../api/Frame.js';
|
import type {Frame} from '../api/Frame.js';
|
||||||
|
import type {Credentials} from '../api/Page.js';
|
||||||
import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
|
import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
|
||||||
import {
|
import {
|
||||||
NetworkManagerEvent,
|
NetworkManagerEvent,
|
||||||
|
|
@ -24,14 +25,6 @@ import {
|
||||||
type FetchRequestId,
|
type FetchRequestId,
|
||||||
} from './NetworkEventManager.js';
|
} from './NetworkEventManager.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export interface Credentials {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
|
@ -147,18 +140,16 @@ export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setExtraHTTPHeaders(
|
async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void> {
|
||||||
extraHTTPHeaders: Record<string, string>
|
const extraHTTPHeaders: Record<string, string> = {};
|
||||||
): Promise<void> {
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
this.#extraHTTPHeaders = {};
|
|
||||||
for (const key of Object.keys(extraHTTPHeaders)) {
|
|
||||||
const value = extraHTTPHeaders[key];
|
|
||||||
assert(
|
assert(
|
||||||
isString(value),
|
isString(value),
|
||||||
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`
|
`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));
|
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 {HTTPRequest} from '../api/HTTPRequest.js';
|
||||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||||
import type {JSHandle} from '../api/JSHandle.js';
|
import type {JSHandle} from '../api/JSHandle.js';
|
||||||
|
import type {Credentials} from '../api/Page.js';
|
||||||
import {
|
import {
|
||||||
Page,
|
Page,
|
||||||
PageEvent,
|
PageEvent,
|
||||||
|
|
@ -71,7 +72,7 @@ import {FrameManagerEvent} from './FrameManagerEvents.js';
|
||||||
import {CdpKeyboard, CdpMouse, CdpTouchscreen} from './Input.js';
|
import {CdpKeyboard, CdpMouse, CdpTouchscreen} from './Input.js';
|
||||||
import {MAIN_WORLD} from './IsolatedWorlds.js';
|
import {MAIN_WORLD} from './IsolatedWorlds.js';
|
||||||
import {releaseObject} from './JSHandle.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 {CdpTarget} from './Target.js';
|
||||||
import type {TargetManager} from './TargetManager.js';
|
import type {TargetManager} from './TargetManager.js';
|
||||||
import {TargetManagerEvent} from './TargetManager.js';
|
import {TargetManagerEvent} from './TargetManager.js';
|
||||||
|
|
@ -916,7 +917,10 @@ export class CdpPage extends Page {
|
||||||
options?: WaitForOptions
|
options?: WaitForOptions
|
||||||
): Promise<HTTPResponse | null> {
|
): Promise<HTTPResponse | null> {
|
||||||
const [result] = await Promise.all([
|
const [result] = await Promise.all([
|
||||||
this.waitForNavigation(options),
|
this.waitForNavigation({
|
||||||
|
...options,
|
||||||
|
ignoreSameDocumentNavigation: true,
|
||||||
|
}),
|
||||||
this.#primaryTargetClient.send('Page.reload'),
|
this.#primaryTargetClient.send('Page.reload'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -1130,6 +1134,16 @@ export class CdpPage extends Page {
|
||||||
await this.#emulationManager.setTransparentBackgroundColor();
|
await this.#emulationManager.setTransparentBackgroundColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await firstValueFrom(
|
||||||
|
from(
|
||||||
|
this.mainFrame()
|
||||||
|
.isolatedRealm()
|
||||||
|
.evaluate(() => {
|
||||||
|
return document.fonts.ready;
|
||||||
|
})
|
||||||
|
).pipe(raceWith(timeout(ms)))
|
||||||
|
);
|
||||||
|
|
||||||
const printCommandPromise = this.#primaryTargetClient.send(
|
const printCommandPromise = this.#primaryTargetClient.send(
|
||||||
'Page.printToPDF',
|
'Page.printToPDF',
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -31,17 +31,14 @@ export class CallbackRegistry {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// We still throw sync errors synchronously and clean up the scheduled
|
// We still throw sync errors synchronously and clean up the scheduled
|
||||||
// callback.
|
// callback.
|
||||||
callback.promise
|
callback.promise.catch(debugError).finally(() => {
|
||||||
.valueOrThrow()
|
|
||||||
.catch(debugError)
|
|
||||||
.finally(() => {
|
|
||||||
this.#callbacks.delete(callback.id);
|
this.#callbacks.delete(callback.id);
|
||||||
});
|
});
|
||||||
callback.reject(error as Error);
|
callback.reject(error as Error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
// Must only have sync code up until here.
|
// Must only have sync code up until here.
|
||||||
return callback.promise.valueOrThrow().finally(() => {
|
return callback.promise.finally(() => {
|
||||||
this.#callbacks.delete(callback.id);
|
this.#callbacks.delete(callback.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -148,8 +145,8 @@ export class Callback {
|
||||||
return this.#id;
|
return this.#id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get promise(): Deferred<unknown> {
|
get promise(): Promise<unknown> {
|
||||||
return this.#deferred;
|
return this.#deferred.valueOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
get error(): ProtocolError {
|
get error(): ProtocolError {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export interface Configuration {
|
||||||
/**
|
/**
|
||||||
* Tells Puppeteer to not chrome-headless-shell download during installation.
|
* 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;
|
skipChromeHeadlessShellDownload?: boolean;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ export class PuppeteerError extends Error {
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor(message?: string) {
|
constructor(message?: string, options?: ErrorOptions) {
|
||||||
super(message);
|
super(message, options);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export interface PDFOptions {
|
||||||
headerTemplate?: string;
|
headerTemplate?: string;
|
||||||
/**
|
/**
|
||||||
* HTML template for the print footer. Has the same constraints and support
|
* 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;
|
footerTemplate?: string;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -312,12 +312,12 @@ export function validateDialogType(
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function timeout(ms: number): Observable<never> {
|
export function timeout(ms: number, cause?: Error): Observable<never> {
|
||||||
return ms === 0
|
return ms === 0
|
||||||
? NEVER
|
? NEVER
|
||||||
: timer(ms).pipe(
|
: timer(ms).pipe(
|
||||||
map(() => {
|
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');
|
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.
|
// Merge default disabled features with user-provided ones, if any.
|
||||||
const disabledFeatures = [
|
const disabledFeatures = [
|
||||||
'Translate',
|
'Translate',
|
||||||
|
|
@ -174,9 +177,13 @@ export class ChromeLauncher extends ProductLauncher {
|
||||||
'MediaRouter',
|
'MediaRouter',
|
||||||
'OptimizationHints',
|
'OptimizationHints',
|
||||||
// https://crbug.com/1492053
|
// https://crbug.com/1492053
|
||||||
'ProcessPerSiteUpToMainFrameThreshold',
|
turnOnExperimentalFeaturesForTesting
|
||||||
|
? ''
|
||||||
|
: 'ProcessPerSiteUpToMainFrameThreshold',
|
||||||
...userDisabledFeatures,
|
...userDisabledFeatures,
|
||||||
];
|
].filter(feature => {
|
||||||
|
return feature !== '';
|
||||||
|
});
|
||||||
|
|
||||||
const userEnabledFeatures = getFeatures('--enable-features', options.args);
|
const userEnabledFeatures = getFeatures('--enable-features', options.args);
|
||||||
if (options.args && userEnabledFeatures.length > 0) {
|
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.
|
// Merge default enabled features with user-provided ones, if any.
|
||||||
const enabledFeatures = [
|
const enabledFeatures = [
|
||||||
'NetworkServiceInProcess2',
|
// Add features to enable by default here.
|
||||||
...userEnabledFeatures,
|
...userEnabledFeatures,
|
||||||
];
|
].filter(feature => {
|
||||||
|
return feature !== '';
|
||||||
|
});
|
||||||
|
|
||||||
const chromeArguments = [
|
const chromeArguments = [
|
||||||
'--allow-pre-commit-input',
|
'--allow-pre-commit-input',
|
||||||
|
|
@ -201,7 +210,9 @@ export class ChromeLauncher extends ProductLauncher {
|
||||||
'--disable-default-apps',
|
'--disable-default-apps',
|
||||||
'--disable-dev-shm-usage',
|
'--disable-dev-shm-usage',
|
||||||
'--disable-extensions',
|
'--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-hang-monitor',
|
||||||
'--disable-infobars',
|
'--disable-infobars',
|
||||||
'--disable-ipc-flooding-protection',
|
'--disable-ipc-flooding-protection',
|
||||||
|
|
@ -220,7 +231,9 @@ export class ChromeLauncher extends ProductLauncher {
|
||||||
'--use-mock-keychain',
|
'--use-mock-keychain',
|
||||||
`--disable-features=${disabledFeatures.join(',')}`,
|
`--disable-features=${disabledFeatures.join(',')}`,
|
||||||
`--enable-features=${enabledFeatures.join(',')}`,
|
`--enable-features=${enabledFeatures.join(',')}`,
|
||||||
];
|
].filter(arg => {
|
||||||
|
return arg !== '';
|
||||||
|
});
|
||||||
const {
|
const {
|
||||||
devtools = false,
|
devtools = false,
|
||||||
headless = !devtools,
|
headless = !devtools,
|
||||||
|
|
|
||||||
|
|
@ -41,15 +41,19 @@ export class NodeWebSocketTransport implements ConnectionTransport {
|
||||||
constructor(ws: NodeWebSocket) {
|
constructor(ws: NodeWebSocket) {
|
||||||
this.#ws = ws;
|
this.#ws = ws;
|
||||||
this.#ws.addEventListener('message', event => {
|
this.#ws.addEventListener('message', event => {
|
||||||
|
setImmediate(() => {
|
||||||
if (this.onmessage) {
|
if (this.onmessage) {
|
||||||
this.onmessage.call(null, event.data);
|
this.onmessage.call(null, event.data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
this.#ws.addEventListener('close', () => {
|
this.#ws.addEventListener('close', () => {
|
||||||
|
setImmediate(() => {
|
||||||
if (this.onclose) {
|
if (this.onclose) {
|
||||||
this.onclose.call(null);
|
this.onclose.call(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
// Silently ignore all errors - we don't know what to do with them.
|
// Silently ignore all errors - we don't know what to do with them.
|
||||||
this.#ws.addEventListener('error', () => {});
|
this.#ws.addEventListener('error', () => {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,12 @@ export abstract class ProductLauncher {
|
||||||
|
|
||||||
const launchArgs = await this.computeLaunchArguments(options);
|
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 usePipe = launchArgs.args.includes('--remote-debugging-pipe');
|
||||||
|
|
||||||
const onProcessExit = async () => {
|
const onProcessExit = async () => {
|
||||||
|
|
|
||||||
|
|
@ -136,11 +136,11 @@ export class PuppeteerNode extends Puppeteer {
|
||||||
* specified.
|
* specified.
|
||||||
*
|
*
|
||||||
* When using with `puppeteer-core`,
|
* When using with `puppeteer-core`,
|
||||||
* {@link LaunchOptions | options.executablePath} or
|
* {@link LaunchOptions.executablePath | options.executablePath} or
|
||||||
* {@link LaunchOptions | options.channel} must be provided.
|
* {@link LaunchOptions.channel | options.channel} must be provided.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* You can use {@link LaunchOptions | options.ignoreDefaultArgs}
|
* You can use {@link LaunchOptions.ignoreDefaultArgs | options.ignoreDefaultArgs}
|
||||||
* to filter out `--mute-audio` from default arguments:
|
* to filter out `--mute-audio` from default arguments:
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const PUPPETEER_REVISIONS = Object.freeze({
|
export const PUPPETEER_REVISIONS = Object.freeze({
|
||||||
chrome: '122.0.6261.94',
|
chrome: '123.0.6312.122',
|
||||||
'chrome-headless-shell': '122.0.6261.94',
|
'chrome-headless-shell': '123.0.6312.122',
|
||||||
firefox: 'latest',
|
firefox: 'latest',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export const interpolateFunction = <T extends (...args: never[]) => unknown>(
|
||||||
for (const [name, jsValue] of Object.entries(replacements)) {
|
for (const [name, jsValue] of Object.entries(replacements)) {
|
||||||
value = value.replace(
|
value = value.replace(
|
||||||
new RegExp(`PLACEHOLDER\\(\\s*(?:'${name}'|"${name}")\\s*\\)`, 'g'),
|
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
|
// are still valid. Without, we may get calls like ()=>{...}() which is
|
||||||
// not valid.
|
// not valid.
|
||||||
`(${jsValue})`
|
`(${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-core bumped from 21.0.2 to 21.0.3
|
||||||
* @puppeteer/browsers bumped from 1.5.1 to 1.6.0
|
* @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)
|
## [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",
|
"name": "puppeteer",
|
||||||
"version": "22.4.0",
|
"version": "22.6.5",
|
||||||
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"puppeteer",
|
"puppeteer",
|
||||||
|
|
@ -124,8 +124,9 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cosmiconfig": "9.0.0",
|
"cosmiconfig": "9.0.0",
|
||||||
"puppeteer-core": "22.4.0",
|
"puppeteer-core": "22.6.5",
|
||||||
"@puppeteer/browsers": "2.1.0"
|
"@puppeteer/browsers": "2.2.2",
|
||||||
|
"devtools-protocol": "0.0.1262051"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.17.15"
|
"@types/node": "18.17.15"
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,17 @@ export const getConfiguration = (): Configuration => {
|
||||||
downloadHost;
|
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 =
|
configuration.cacheDirectory =
|
||||||
process.env['PUPPETEER_CACHE_DIR'] ??
|
process.env['PUPPETEER_CACHE_DIR'] ??
|
||||||
process.env['npm_config_puppeteer_cache_dir'] ??
|
process.env['npm_config_puppeteer_cache_dir'] ??
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@ module.exports = {
|
||||||
selector:
|
selector:
|
||||||
'CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflake"], CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflakeOnly"]',
|
'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",
|
"id": "chrome-bidi",
|
||||||
"platforms": ["linux"],
|
"platforms": ["linux"],
|
||||||
"parameters": ["chrome", "chrome-headless-shell", "webDriverBiDi"],
|
"parameters": ["chrome", "headless", "webDriverBiDi"],
|
||||||
"expectedLineCoverage": 56
|
"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',
|
executablePath: 'node',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
"assets"
|
"assets"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "10.3.10",
|
"glob": "10.3.12",
|
||||||
"mocha": "10.3.0"
|
"mocha": "10.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import {spawnSync} from 'child_process';
|
||||||
|
import {existsSync} from 'fs';
|
||||||
import {readdir} from 'fs/promises';
|
import {readdir} from 'fs/promises';
|
||||||
import {platform} from 'os';
|
import {platform} from 'os';
|
||||||
import {join} from 'path';
|
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>
|
ElementHandle<HTMLButtonElement>
|
||||||
>;
|
>;
|
||||||
const ids = await getIds(found);
|
const ids = await getIds(found);
|
||||||
expect(ids).toEqual([
|
expect(ids).toEqual(['node5', 'node6', 'node8', 'node10', 'node21']);
|
||||||
'node5',
|
|
||||||
'node6',
|
|
||||||
'node7',
|
|
||||||
'node8',
|
|
||||||
'node10',
|
|
||||||
'node21',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
it('should find by role "heading"', async () => {
|
it('should find by role "heading"', async () => {
|
||||||
const {page} = await setupPage();
|
const {page} = await setupPage();
|
||||||
const found = await page.$$('aria/[role="heading"]');
|
const found = await page.$$('aria/[role="heading"]');
|
||||||
const ids = await getIds(found);
|
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 () => {
|
it('should find both ignored and unignored', async () => {
|
||||||
const {page} = await setupPage();
|
const {page} = await setupPage();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
|
|
||||||
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
import {getTestState, launch, setupTestBrowserHooks} from './mocha-utils.js';
|
||||||
|
|
||||||
describe('Browser specs', function () {
|
describe('Browser specs', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
|
|
@ -64,6 +64,23 @@ describe('Browser specs', function () {
|
||||||
expect(remoteBrowser.process()).toBe(null);
|
expect(remoteBrowser.process()).toBe(null);
|
||||||
await remoteBrowser.disconnect();
|
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', () => {
|
describe('Browser.isConnected', () => {
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ describe('Target.createCDPSession', function () {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
).rejects.toThrowError(
|
).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;
|
return 2 * 3;
|
||||||
})
|
})
|
||||||
).toBe(6);
|
).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 {puppeteer} = await getTestState({skipLaunch: true});
|
||||||
const originalBrowser = await launchBrowser(launchOptions);
|
const originalBrowser = await launchBrowser(launchOptions);
|
||||||
|
|
||||||
|
|
@ -87,7 +87,8 @@ describe('DevTools', function () {
|
||||||
return 2 * 3;
|
return 2 * 3;
|
||||||
})
|
})
|
||||||
).toBe(6);
|
).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 () => {
|
it('should open devtools when "devtools: true" option is given', async () => {
|
||||||
const browser = await launchBrowser(
|
const browser = await launchBrowser(
|
||||||
|
|
|
||||||
|
|
@ -385,8 +385,15 @@ describe('ElementHandle specs', function () {
|
||||||
await page.setContent(
|
await page.setContent(
|
||||||
`<iframe name='frame' style='position: absolute; left: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
|
`<iframe name='frame' style='position: absolute; left: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
|
||||||
);
|
);
|
||||||
const frame = await page.waitForFrame(frame => {
|
const frame = await page.waitForFrame(async frame => {
|
||||||
return frame.name() === '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();
|
using handle = await frame.locator('button').waitHandle();
|
||||||
|
|
@ -395,8 +402,15 @@ describe('ElementHandle specs', function () {
|
||||||
await page.setContent(
|
await page.setContent(
|
||||||
`<iframe name='frame2' style='position: absolute; top: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
|
`<iframe name='frame2' style='position: absolute; top: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
|
||||||
);
|
);
|
||||||
const frame2 = await page.waitForFrame(frame => {
|
const frame2 = await page.waitForFrame(async frame => {
|
||||||
return frame.name() === 'frame2';
|
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();
|
using handle2 = await frame2.locator('button').waitHandle();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
|
import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
|
||||||
import type {Frame} from 'puppeteer-core/internal/api/Frame.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 {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -78,7 +79,7 @@ describe('Frame specs', function () {
|
||||||
const {page, server} = await getTestState();
|
const {page, server} = await getTestState();
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/frames/nested-frames.html');
|
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/nested-frames.html',
|
||||||
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
||||||
' http://localhost:<PORT>/frames/frame.html (uno)',
|
' http://localhost:<PORT>/frames/frame.html (uno)',
|
||||||
|
|
@ -232,23 +233,6 @@ describe('Frame specs', function () {
|
||||||
expect(page.frames()).toHaveLength(2);
|
expect(page.frames()).toHaveLength(2);
|
||||||
expect(page.frames()[1]!.url()).toBe(server.EMPTY_PAGE);
|
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 () => {
|
it('should report frame.parent()', async () => {
|
||||||
const {page, server} = await getTestState();
|
const {page, server} = await getTestState();
|
||||||
|
|
||||||
|
|
@ -306,4 +290,35 @@ describe('Frame specs', function () {
|
||||||
expect(page.mainFrame().client).toBeInstanceOf(CDPSession);
|
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'
|
'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]', () => {
|
describe('JSHandle[Symbol.dispose]', () => {
|
||||||
|
|
|
||||||
|
|
@ -186,8 +186,15 @@ describe('Keyboard', function () {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"</iframe>
|
<iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"</iframe>
|
||||||
`);
|
`);
|
||||||
const frame = await page.waitForFrame(frame => {
|
const frame = await page.waitForFrame(async frame => {
|
||||||
return frame.name() === 'test';
|
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');
|
await frame.focus('textarea');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,30 @@ describe('Launcher specs', function () {
|
||||||
const {close} = await launch({});
|
const {close} = await launch({});
|
||||||
await close();
|
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 () {
|
it('should have default url when launching browser', async function () {
|
||||||
const {browser, close} = await launch({}, {createContext: false});
|
const {browser, close} = await launch({}, {createContext: false});
|
||||||
try {
|
try {
|
||||||
|
|
@ -166,7 +190,9 @@ describe('Launcher specs', function () {
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
return (waitError = 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 () => {
|
it('userDataDir option', async () => {
|
||||||
const userDataDir = await mkdtemp(TMP_FOLDER);
|
const userDataDir = await mkdtemp(TMP_FOLDER);
|
||||||
|
|
@ -591,6 +617,20 @@ describe('Launcher specs', function () {
|
||||||
});
|
});
|
||||||
expect(error.message).toContain('either pipe or debugging port');
|
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 () {
|
describe('Puppeteer.launch', function () {
|
||||||
|
|
@ -793,7 +833,7 @@ describe('Launcher specs', function () {
|
||||||
const restoredPage = pages.find(page => {
|
const restoredPage = pages.find(page => {
|
||||||
return page.url() === server.PREFIX + '/frames/nested-frames.html';
|
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/nested-frames.html',
|
||||||
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
||||||
' http://localhost:<PORT>/frames/frame.html (uno)',
|
' http://localhost:<PORT>/frames/frame.html (uno)',
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,27 @@ describe('navigation', function () {
|
||||||
const response = await page.goto(server.PREFIX + '/grid.html');
|
const response = await page.goto(server.PREFIX + '/grid.html');
|
||||||
expect(response!.status()).toBe(200);
|
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 () => {
|
it('should navigate to empty page with networkidle0', async () => {
|
||||||
const {page, server} = await getTestState();
|
const {page, server} = await getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -722,7 +722,7 @@ describe('network', function () {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// In headful, an error is thrown instead of 401.
|
// In headful, an error is thrown instead of 401.
|
||||||
if (
|
if (
|
||||||
!(error as Error).message.startsWith(
|
!(error as Error).message?.includes(
|
||||||
'net::ERR_INVALID_AUTH_CREDENTIALS'
|
'net::ERR_INVALID_AUTH_CREDENTIALS'
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
@ -772,7 +772,7 @@ describe('network', function () {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// In headful, an error is thrown instead of 401.
|
// In headful, an error is thrown instead of 401.
|
||||||
if (
|
if (
|
||||||
!(error as Error).message.startsWith(
|
!(error as Error).message?.includes(
|
||||||
'net::ERR_INVALID_AUTH_CREDENTIALS'
|
'net::ERR_INVALID_AUTH_CREDENTIALS'
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -506,11 +506,14 @@ describe('Page', function () {
|
||||||
console.log(1, 2, 3, globalThis);
|
console.log(1, 2, 3, globalThis);
|
||||||
});
|
});
|
||||||
const log = await logPromise;
|
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(log.args()).toHaveLength(4);
|
||||||
expect(await (await log.args()[3]!.getProperty('test')).jsonValue()).toBe(
|
using property = await log.args()[3]!.getProperty('test');
|
||||||
1
|
expect(await property.jsonValue()).toBe(1);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('should trigger correct Log', async () => {
|
it('should trigger correct Log', async () => {
|
||||||
const {page, server, isChrome} = await getTestState();
|
const {page, server, isChrome} = await getTestState();
|
||||||
|
|
@ -1210,13 +1213,15 @@ describe('Page', function () {
|
||||||
expect(result).toBe(36);
|
expect(result).toBe(36);
|
||||||
await page.removeExposedFunction('compute');
|
await page.removeExposedFunction('compute');
|
||||||
|
|
||||||
let error: Error | null = null;
|
const error = await page
|
||||||
await page
|
|
||||||
.evaluate(async function () {
|
.evaluate(async function () {
|
||||||
return (globalThis as any).compute(9, 4);
|
return (globalThis as any).compute(9, 4);
|
||||||
})
|
})
|
||||||
.catch(_error => {
|
.then(() => {
|
||||||
return (error = _error);
|
return null;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
return error;
|
||||||
});
|
});
|
||||||
expect(error).toBeTruthy();
|
expect(error).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ describe('cooperative request interception', function () {
|
||||||
|
|
||||||
describe('Page.setRequestInterception', function () {
|
describe('Page.setRequestInterception', function () {
|
||||||
const expectedActions: ActionResult[] = ['abort', 'continue', 'respond'];
|
const expectedActions: ActionResult[] = ['abort', 'continue', 'respond'];
|
||||||
while (expectedActions.length > 0) {
|
for (const expectedAction of expectedActions) {
|
||||||
const expectedAction = expectedActions.pop();
|
|
||||||
it(`should cooperatively ${expectedAction} by priority`, async () => {
|
it(`should cooperatively ${expectedAction} by priority`, async () => {
|
||||||
const {page, server} = await getTestState();
|
const {page, server} = await getTestState();
|
||||||
|
|
||||||
|
|
@ -94,24 +93,36 @@ describe('cooperative request interception', function () {
|
||||||
const {page, server} = await getTestState();
|
const {page, server} = await getTestState();
|
||||||
|
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
let requestError;
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
if (isFavicon(request)) {
|
if (isFavicon(request)) {
|
||||||
void request.continue({}, 0);
|
void request.continue({}, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
expect(request).toBeTruthy();
|
||||||
expect(request.url()).toContain('empty.html');
|
expect(request.url()).toContain('empty.html');
|
||||||
expect(request.headers()['user-agent']).toBeTruthy();
|
expect(request.headers()['user-agent']).toBeTruthy();
|
||||||
expect(request.method()).toBe('GET');
|
expect(request.method()).toBe('GET');
|
||||||
expect(request.postData()).toBe(undefined);
|
expect(request.postData()).toBe(undefined);
|
||||||
expect(request.isNavigationRequest()).toBe(true);
|
expect(request.isNavigationRequest()).toBe(true);
|
||||||
expect(request.resourceType()).toBe('document');
|
expect(request.resourceType()).toBe('document');
|
||||||
expect(request.frame() === page.mainFrame()).toBe(true);
|
|
||||||
expect(request.frame()!.url()).toBe('about:blank');
|
expect(request.frame()!.url()).toBe('about:blank');
|
||||||
|
expect(request.frame() === page.mainFrame()).toBe(true);
|
||||||
|
} catch (error) {
|
||||||
|
requestError = error;
|
||||||
|
} finally {
|
||||||
void request.continue({}, 0);
|
void request.continue({}, 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||||
expect(response!.ok()).toBe(true);
|
if (requestError) {
|
||||||
expect(response!.remoteAddress().port).toBe(server.PORT);
|
throw requestError;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.ok()).toBe(true);
|
||||||
|
expect(response.remoteAddress().port).toBe(server.PORT);
|
||||||
});
|
});
|
||||||
// @see https://github.com/puppeteer/puppeteer/pull/3105
|
// @see https://github.com/puppeteer/puppeteer/pull/3105
|
||||||
it('should work when POST is redirected with 302', async () => {
|
it('should work when POST is redirected with 302', async () => {
|
||||||
|
|
@ -141,16 +152,24 @@ describe('cooperative request interception', function () {
|
||||||
|
|
||||||
server.setRedirect('/rrredirect', '/empty.html');
|
server.setRedirect('/rrredirect', '/empty.html');
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
let requestError;
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
const headers = Object.assign({}, request.headers(), {
|
const headers = Object.assign({}, request.headers(), {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
});
|
});
|
||||||
void request.continue({headers}, 0);
|
void request.continue({headers}, 0);
|
||||||
|
try {
|
||||||
expect(request.continueRequestOverrides()).toEqual({headers});
|
expect(request.continueRequestOverrides()).toEqual({headers});
|
||||||
|
} catch (error) {
|
||||||
|
requestError = error;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Make sure that the goto does not time out.
|
// Make sure that the goto does not time out.
|
||||||
await page.goto(server.PREFIX + '/rrredirect');
|
await page.goto(server.PREFIX + '/rrredirect');
|
||||||
|
|
||||||
|
if (requestError) {
|
||||||
|
throw requestError;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// @see https://github.com/puppeteer/puppeteer/issues/4743
|
// @see https://github.com/puppeteer/puppeteer/issues/4743
|
||||||
it('should be able to remove headers', async () => {
|
it('should be able to remove headers', async () => {
|
||||||
|
|
@ -220,11 +239,20 @@ describe('cooperative request interception', function () {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
});
|
});
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
let requestError;
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
|
try {
|
||||||
expect(request.headers()['foo']).toBe('bar');
|
expect(request.headers()['foo']).toBe('bar');
|
||||||
|
} catch (error) {
|
||||||
|
requestError = error;
|
||||||
|
} finally {
|
||||||
void request.continue({}, 0);
|
void request.continue({}, 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const response = await page.goto(server.EMPTY_PAGE);
|
const response = await page.goto(server.EMPTY_PAGE);
|
||||||
|
if (requestError) {
|
||||||
|
throw requestError;
|
||||||
|
}
|
||||||
expect(response!.ok()).toBe(true);
|
expect(response!.ok()).toBe(true);
|
||||||
});
|
});
|
||||||
// @see https://github.com/puppeteer/puppeteer/issues/4337
|
// @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.setExtraHTTPHeaders({referer: server.EMPTY_PAGE});
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
let requestError;
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
|
try {
|
||||||
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
||||||
|
} catch (error) {
|
||||||
|
requestError = error;
|
||||||
|
} finally {
|
||||||
void request.continue({}, 0);
|
void request.continue({}, 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const response = await page.goto(server.EMPTY_PAGE);
|
const response = await page.goto(server.EMPTY_PAGE);
|
||||||
|
if (requestError) {
|
||||||
|
throw requestError;
|
||||||
|
}
|
||||||
expect(response!.ok()).toBe(true);
|
expect(response!.ok()).toBe(true);
|
||||||
});
|
});
|
||||||
it('should be abortable', async () => {
|
it('should be abortable', async () => {
|
||||||
|
|
@ -340,7 +377,7 @@ describe('cooperative request interception', function () {
|
||||||
if (isChrome) {
|
if (isChrome) {
|
||||||
expect(error.message).toContain('net::ERR_FAILED');
|
expect(error.message).toContain('net::ERR_FAILED');
|
||||||
} else {
|
} else {
|
||||||
expect(error.message).toContain('NS_ERROR_FAILURE');
|
expect(error.message).toContain('NS_ERROR_ABORT');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should work with redirects', async () => {
|
it('should work with redirects', async () => {
|
||||||
|
|
@ -947,14 +984,26 @@ describe('cooperative request interception', function () {
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
void request.continue();
|
void request.continue();
|
||||||
});
|
});
|
||||||
|
let requestError;
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
|
try {
|
||||||
expect(request.isInterceptResolutionHandled()).toBeTruthy();
|
expect(request.isInterceptResolutionHandled()).toBeTruthy();
|
||||||
|
} catch (error) {
|
||||||
|
requestError = error;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
const {action} = request.interceptResolutionState();
|
const {action} = request.interceptResolutionState();
|
||||||
|
try {
|
||||||
expect(action).toBe(InterceptResolutionAction.AlreadyHandled);
|
expect(action).toBe(InterceptResolutionAction.AlreadyHandled);
|
||||||
|
} catch (error) {
|
||||||
|
requestError = error;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
if (requestError) {
|
||||||
|
throw requestError;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -22,23 +22,34 @@ describe('request interception', function () {
|
||||||
const {page, server} = await getTestState();
|
const {page, server} = await getTestState();
|
||||||
|
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
let requestError;
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
if (isFavicon(request)) {
|
if (isFavicon(request)) {
|
||||||
void request.continue();
|
void request.continue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
expect(request).toBeTruthy();
|
||||||
expect(request.url()).toContain('empty.html');
|
expect(request.url()).toContain('empty.html');
|
||||||
expect(request.headers()['user-agent']).toBeTruthy();
|
expect(request.headers()['user-agent']).toBeTruthy();
|
||||||
expect(request.headers()['accept']).toBeTruthy();
|
|
||||||
expect(request.method()).toBe('GET');
|
expect(request.method()).toBe('GET');
|
||||||
expect(request.postData()).toBe(undefined);
|
expect(request.postData()).toBe(undefined);
|
||||||
expect(request.isNavigationRequest()).toBe(true);
|
expect(request.isNavigationRequest()).toBe(true);
|
||||||
expect(request.resourceType()).toBe('document');
|
expect(request.resourceType()).toBe('document');
|
||||||
expect(request.frame() === page.mainFrame()).toBe(true);
|
|
||||||
expect(request.frame()!.url()).toBe('about:blank');
|
expect(request.frame()!.url()).toBe('about:blank');
|
||||||
|
expect(request.frame() === page.mainFrame()).toBe(true);
|
||||||
|
} catch (error) {
|
||||||
|
requestError = error;
|
||||||
|
} finally {
|
||||||
void request.continue();
|
void request.continue();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||||
|
if (requestError) {
|
||||||
|
throw requestError;
|
||||||
|
}
|
||||||
|
|
||||||
expect(response.ok()).toBe(true);
|
expect(response.ok()).toBe(true);
|
||||||
expect(response.remoteAddress().port).toBe(server.PORT);
|
expect(response.remoteAddress().port).toBe(server.PORT);
|
||||||
});
|
});
|
||||||
|
|
@ -76,7 +87,11 @@ describe('request interception', function () {
|
||||||
});
|
});
|
||||||
void request.continue({headers});
|
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
|
// @see https://github.com/puppeteer/puppeteer/issues/4743
|
||||||
it('should be able to remove headers', async () => {
|
it('should be able to remove headers', async () => {
|
||||||
|
|
@ -162,11 +177,21 @@ describe('request interception', function () {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
});
|
});
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
let requestError;
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
|
try {
|
||||||
expect(request.headers()['foo']).toBe('bar');
|
expect(request.headers()['foo']).toBe('bar');
|
||||||
|
} catch (error) {
|
||||||
|
requestError = error;
|
||||||
|
} finally {
|
||||||
void request.continue();
|
void request.continue();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||||
|
if (requestError) {
|
||||||
|
throw requestError;
|
||||||
|
}
|
||||||
expect(response.ok()).toBe(true);
|
expect(response.ok()).toBe(true);
|
||||||
});
|
});
|
||||||
// @see https://github.com/puppeteer/puppeteer/issues/4337
|
// @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.setExtraHTTPHeaders({referer: server.EMPTY_PAGE});
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
page.on('request', request => {
|
let request!: HTTPRequest;
|
||||||
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
page.on('request', req => {
|
||||||
|
request = req;
|
||||||
void request.continue();
|
void request.continue();
|
||||||
});
|
});
|
||||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||||
|
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
|
||||||
expect(response.ok()).toBe(true);
|
expect(response.ok()).toBe(true);
|
||||||
});
|
});
|
||||||
it('should be abortable', async () => {
|
it('should be abortable', async () => {
|
||||||
|
|
@ -267,7 +294,7 @@ describe('request interception', function () {
|
||||||
if (isChrome) {
|
if (isChrome) {
|
||||||
expect(error.message).toContain('net::ERR_FAILED');
|
expect(error.message).toContain('net::ERR_FAILED');
|
||||||
} else {
|
} else {
|
||||||
expect(error.message).toContain('NS_ERROR_FAILURE');
|
expect(error.message).toContain('NS_ERROR_ABORT');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should work with redirects', async () => {
|
it('should work with redirects', async () => {
|
||||||
|
|
@ -493,7 +520,7 @@ describe('request interception', function () {
|
||||||
))!;
|
))!;
|
||||||
expect(response.status()).toBe(200);
|
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();
|
const {page, server} = await getTestState();
|
||||||
|
|
||||||
// The requestWillBeSent will report URL as-is, whereas interception will
|
// The requestWillBeSent will report URL as-is, whereas interception will
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,33 @@ describe('Screenshots', function () {
|
||||||
|
|
||||||
await context.close();
|
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', () => {
|
describe('Cdp', () => {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue