feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+1
View File
@@ -0,0 +1 @@
chrome@149.0.7826.0
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+635
View File
@@ -0,0 +1,635 @@
# WebDriver BiDi for Chromium [![chromium-bidi on npm](https://img.shields.io/npm/v/chromium-bidi)](https://www.npmjs.com/package/chromium-bidi)
## CI status
![E2E Tests](https://github.com/GoogleChromeLabs/chromium-bidi/actions/workflows/e2e.yml/badge.svg)
![Unit Tests](https://github.com/GoogleChromeLabs/chromium-bidi/actions/workflows/unit.yml/badge.svg)
![WPT Tests](https://github.com/GoogleChromeLabs/chromium-bidi/actions/workflows/wpt.yml/badge.svg)
![Pre-commit](https://github.com/GoogleChromeLabs/chromium-bidi/actions/workflows/pre-commit.yml/badge.svg)
This is an implementation of the
[WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) protocol with some
extensions (**BiDi+**)
for Chromium, implemented as a JavaScript layer translating between BiDi and CDP,
running inside a Chrome tab.
Current status can be checked
at [WPT WebDriver BiDi status](https://wpt.fyi/results/webdriver/tests/bidi).
## Performance Benchmarks
The project continuously monitors the performance and overhead of the WebDriver BiDi implementation.
- **Dashboard:** [Chromium-BiDi Performance Benchmarks](https://googlechromelabs.github.io/chromium-bidi/bench/)
- **Details:** Refer to [docs/benchmark.md](docs/benchmark.md) for detailed information about the benchmarking infrastructure, methodology, and statistical analysis.
Note that performance data can be sensitive to CI environment fluctuations, especially on macOS.
## BiDi+
**"BiDi+"** is an extension of the WebDriver BiDi protocol. In addition to [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) it has:
### Command `goog:cdp.sendCommand`
```cddl
CdpSendCommandCommand = {
method: "goog:cdp.sendCommand",
params: CdpSendCommandParameters,
}
CdpSendCommandParameters = {
method: text,
params: any,
session?: text,
}
CdpSendCommandResult = {
result: any,
session: text,
}
```
The command runs the
described [CDP command](https://chromedevtools.github.io/devtools-protocol)
and returns the result.
### Command `goog:cdp.getSession`
```cddl
CdpGetSessionCommand = {
method: "goog:cdp.getSession",
params: CdpGetSessionParameters,
}
CdpGetSessionParameters = {
context: BrowsingContext,
}
CdpGetSessionResult = {
session: text,
}
```
The command returns the default CDP session for the selected browsing context.
### Command `goog:cdp.resolveRealm`
```cddl
CdpResolveRealmCommand = {
method: "goog:cdp.resolveRealm",
params: CdpResolveRealmParameters,
}
CdpResolveRealmParameters = {
realm: Script.Realm,
}
CdpResolveRealmResult = {
executionContextId: text,
}
```
The command returns resolves a BiDi realm to its CDP execution context ID.
### Events `goog:cdp`
```cddl
CdpEventReceivedEvent = {
method: "goog:cdp.<CDP Event Name>",
params: CdpEventReceivedParameters,
}
CdpEventReceivedParameters = {
event: text,
params: any,
session: text,
}
```
The event contains a CDP event.
### Field `goog:channel`
Each command can be extended with a `goog:channel`:
```cddl
Command = {
id: js-uint,
"goog:channel"?: text,
CommandData,
Extensible,
}
```
If provided and non-empty string, the very same `goog:channel` is added to the response:
```cddl
CommandResponse = {
id: js-uint,
"goog:channel"?: text,
result: ResultData,
Extensible,
}
ErrorResponse = {
id: js-uint / null,
"goog:channel"?: text,
error: ErrorCode,
message: text,
?stacktrace: text,
Extensible
}
```
When client uses
commands [`session.subscribe`](https://w3c.github.io/webdriver-bidi/#command-session-subscribe)
and [`session.unsubscribe`](https://w3c.github.io/webdriver-bidi/#command-session-unsubscribe)
with `goog:channel`, the subscriptions are handled per channel, and the corresponding
`goog:channel` filed is added to the event message:
```cddl
Event = {
"goog:channel"?: text,
EventData,
Extensible,
}
```
## Dev Setup
### `npm`
This is a Node.js project, so install dependencies as usual:
```sh
npm install
```
### `cargo`
<!-- TODO(jrandolf): Remove after binaries get published -->
We use [cddlconv](https://github.com/google/cddlconv) to generate our WebDriverBiDi types before building.
1. Install [Rust](https://rustup.rs/).
2. Run `cargo install --git https://github.com/google/cddlconv.git cddlconv`
### pre-commit.com integration
Refer to the documentation at [.pre-commit-config.yaml](.pre-commit-config.yaml).
```sh
pre-commit install --hook-type pre-push
```
Re-installing pre-commit locally:
```
pre-commit clean && pip install pre-commit
```
### Starting WebDriver BiDi Server
This will run the server on port `8080`:
```sh
npm run server
```
Use the `PORT=` environment variable or `--port=` argument to run it on another port:
```sh
PORT=8081 npm run server
npm run server -- --port=8081
```
Use the `DEBUG` environment variable to see debug info:
```sh
DEBUG=* npm run server
```
Use the `DEBUG_DEPTH` (default: `10`) environment variable to see debug deeply nested objects:
```sh
DEBUG_DEPTH=100 DEBUG=* npm run server
```
Use the `CHANNEL=...` environment variable with one of the following values to run
the specific Chrome channel: `stable`, `beta`, `canary`, `dev`, `local`. Default is
`local`. The `local` channel means the pinned in `.browser` Chrome version will be
downloaded if it is not yet in cache. Otherwise, the requested Chrome version should
be installed.
```sh
CHANNEL=dev npm run server
```
Use the CLI argument `--verbose` to have CDP events printed to the console. Note: you have to enable debugging output `bidi:mapper:debug:*` as well.
```sh
DEBUG=bidi:mapper:debug:* npm run server -- --verbose
```
or
```sh
DEBUG=* npm run server -- --verbose
```
### Starting on Linux and Mac
TODO: verify it works on Windows.
You can also run the server by using `npm run server`. It will write
output to the file `log.txt`:
```sh
npm run server -- --port=8081 --headless=false
```
### Running with in other project
Sometimes it good to verify that a change will not affect thing downstream for other packages.
There is a useful `puppeteer` label you can add to any PR to run Puppeteer test with your changes.
It will bundle `chromium-bidi` and install it in Puppeteer project then run that package test.
## Running
### Unit tests
Running:
```sh
npm run unit
```
### E2E tests
The e2e tests serve the following purposes:
1. Brief checks of the scenarios (the detailed check is done in WPT)
2. Test Chromium-specific behavior nuances
3. Add a simple setup for engaging the specific command
The E2E tests are written using Python, in order to more-or-less align with the web-platform-tests.
#### Installation
Python 3.10+ and some dependencies are required:
```sh
python -m pip install --user pipenv
pipenv install
```
#### Running
The E2E tests require BiDi server running on the same host. By default, tests
try to connect to the port `8080`. The server can be run from the project root:
```sh
npm run e2e # alias to to e2e:headless
npm run e2e:headful
npm run e2e:headless
```
This commands will run `./tools/run-e2e.mjs`, which will log the PyTest output to console,
Additionally the output is also recorded under `./logs/<DATE>.e2e.log`, this will contain
both the PyTest logs and in the event of `FAILED` test all the Chromium-BiDi logs.
If you need to see the logs for all test run the command with `VERBOSE=true`.
Simply pass `npm run e2e -- tests/<PathOrFile>` and the e2e will run only the selected one.
You run a specific test by running `npm run e2e -- -k <TestName>`.
Use `CHROMEDRIVER` environment to run tests in `chromedriver` instead of NodeJS runner:
```shell
CHROMEDRIVER=true npm run e2e
```
Use the `PORT` environment variable to connect to another port:
```sh
PORT=8081 npm run e2e
```
Use the `HEADLESS` to run the tests in headless (new or old) or headful modes.
Values: `new`, `old`, `false`, default: `new`.
```sh
HEADLESS=new npm run e2e
```
#### Updating snapshots
```sh
npm run e2e -- --snapshot-update true
```
See https://github.com/tophat/syrupy for more information.
### Local http server
E2E tests use local http
server [`pytest-httpserver`](https://pytest-httpserver.readthedocs.io/), which is run
automatically with the tests. However,
sometimes it is useful to run the http server outside the test
case, for example for manual debugging. This can be done by running:
```sh
pipenv run local_http_server
```
...or directly:
```sh
python tests/tools/local_http_server.py
```
### Examples
Refer to [examples/README.md](examples/README.md).
## WPT (Web Platform Tests)
WPT is added as
a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules). To get run
WPT tests:
### Check out and setup WPT
#### 1. Check out WPT
```sh
git submodule update --init
```
#### 2. Go to the WPT folder
```sh
cd wpt
```
#### 3. Set up virtualenv
Follow the [_System
Setup_](https://web-platform-tests.org/running-tests/from-local-system.html#system-setup)
instructions.
#### 4. Setup `hosts` file
Follow
the [`hosts` File Setup](https://web-platform-tests.org/running-tests/from-local-system.html#hosts-file-setup)
instructions.
##### 4.a On Linux, macOS or other UNIX-like system
```sh
./wpt make-hosts-file | sudo tee -a /etc/hosts
```
##### 4.b On **Windows**
This must be run in a PowerShell session with Administrator privileges:
```sh
python wpt make-hosts-file | Out-File $env:SystemRoot\System32\drivers\etc\hosts -Encoding ascii -Append
```
If you are behind a proxy, you also need to make sure the domains above are excluded
from your proxy lookups.
#### 5. Set `BROWSER_BIN`
Set the `BROWSER_BIN` environment variable to a Chrome, Edge or Chromium binary to launch.
For example, on macOS:
```sh
# Chrome
export BROWSER_BIN="/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
export BROWSER_BIN="/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev"
export BROWSER_BIN="/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta"
export BROWSER_BIN="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
export BROWSER_BIN="/Applications/Chromium.app/Contents/MacOS/Chromium"
# Edge
export BROWSER_BIN="/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary"
export BROWSER_BIN="/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
```
### Run WPT tests
#### 1. Make sure you have Chrome Dev installed
https://www.google.com/chrome/dev/
#### 2. Build Chromedriver BiDi
Oneshot:
```sh
npm run build
```
Continuously:
```sh
npm run build --watch
```
#### 3. Run
```sh
npm run wpt -- webdriver/tests/bidi/
```
### Update WPT expectations if needed
```sh
UPDATE_EXPECTATIONS=true npm run wpt -- webdriver/tests/bidi/
```
## How does it work?
The architecture is described in the
[WebDriver BiDi in Chrome Context implementation plan](https://docs.google.com/document/d/1VfQ9tv0wPSnb5TI-MOobjoQ5CXLnJJx9F_PxOMQc8kY)
.
There are 2 main modules:
1. backend WS server in `src`. It runs webSocket server, and for each ws connection
runs an instance of browser with BiDi Mapper.
2. front-end BiDi Mapper in `src/bidiMapper`. Gets BiDi commands from the backend,
and map them to CDP commands.
## Contributing
The BiDi commands are processed in the `src/bidiMapper/commandProcessor.ts`. To add a
new command, add it to `_processCommand`, write and call processor for it.
### Publish new `npm` release
#### Release branches
`chromium-bidi` maintains release branches corresponding to Chrome releases. The
branches are named using the following pattern: `releases/m$MAJOR_VERSION`.
The new release branch is created as soon a new major browser version is
published by the
[update-browser-version](https://github.com/GoogleChromeLabs/chromium-bidi/blob/main/.github/workflows/update-browser-version.yml)
job:
- the PR created by this job should be marked as a feature and it should cause the
major package version to be bumped.
- once the browser version is bumped, the commit preceding the version bump
should be used to create a release branch for major version pinned before the bump.
Changes that need to be cherry-picked into the release branch should be marked
as patches. Either major or minor version bumps are not allowed on the release
branch.
Example workflow:
```mermaid
gitGraph
commit id: "feat: featA"
commit id: "release: v0.5.0"
branch release/m129
checkout main
commit id: "feat: roll Chrome to M130 from 129"
commit id: "release: v0.6.0"
commit id: "fix: for m129"
checkout release/m129
cherry-pick id: "fix: for m129"
commit id: "release: v0.5.1 "
```
Currently, the releases from release branches are not automated.
#### Automatic release
We use [release-please](https://github.com/googleapis/release-please) to automate releases. When a release should be done, check for the release PR in our [pull requests](https://github.com/GoogleChromeLabs/chromium-bidi/pulls) and merge it.
#### Manual release
1. Dry-run
```sh
npm publish --dry-run
```
1. Open a PR bumping the chromium-bidi version number in `package.json` for review:
```sh
npm version patch -m 'chore: Release v%s' --no-git-tag-version
```
Instead of `patch`, use `minor` or `major` [as needed](https://semver.org/).
1. After the PR is reviewed, [create a GitHub release](https://github.com/GoogleChromeLabs/chromium-bidi/releases/new) specifying the tag name matching the bumped version.
Our CI then automatically publishes the new release to npm based on the tag name.
#### Roll into Chromium
This section assumes you already have a Chromium set-up locally,
and knowledge on [how to submit changes to the repo](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/contributing.md).
Otherwise submit an issue for a project maintainer.
1. Create a new branch in chromium `src/`.
2. Update the mapper version:
```shell
third_party/bidimapper/roll_bidimapper
```
3. Submit a CL with bug `42323268` ([link](https://crbug.com/42323268)).
4. [Regenerate WPT expectations or baselines](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/testing/run_web_platform_tests.md#test-expectations-and-baselines):
4.1. Trigger a build and test run:
```shell
third_party/blink/tools/blink_tool.py rebaseline-cl --build="linux-blink-rel" --verbose
```
4.2. Once the test completes on the builder, rerun that command to update the
baselines. Update test expectations if there are any crashes or timeouts.
Commit the changes (if any), and upload the new patch to the CL.
5. Add appropriate reviewers or comment the CL link on the PR.
## Adding new command
Want to add a shiny new command to WebDriver BiDi for Chromium? Here's the playbook:
### Prerequisites
#### Specification
The WebDriver BiDi [module](https://w3c.github.io/webdriver-bidi/#protocol-modules), [command](https://w3c.github.io/webdriver-bidi/#commands), or [event](https://w3c.github.io/webdriver-bidi/#events) must be specified either in the [WebDriver BiDi specification](https://w3c.github.io/webdriver-bidi) or as an extension in a separate specification (e.g., the [Permissions specification](https://www.w3.org/TR/permissions/#automation-webdriver-bidi)). The specification should include the command's type definitions in valid [CDDL](https://datatracker.ietf.org/doc/html/rfc8610) format.
#### WPT wdspec tests
You'll need tests to prove your command works as expected. These tests should be written using [WPT wdspec](https://web-platform-tests.org/writing-tests/wdspec.html) and submitted along with the spec itself. Don't forget to roll the WPT repo into the Mapper ([dependabot](https://github.com/GoogleChromeLabs/chromium-bidi/network/updates/10663151/jobs) can help, and you will likely need to tweak some expectations afterward).
#### CDP implementation
Make sure Chromium already has the CDP methods your command will rely on.
### Update CDDL types
1. If your command lives in a separate spec, add a link to that spec in the ["Build WebDriverBiDi types"](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/.github/workflows/update-bidi-types.yml#L27) GitHub action (check out the ["bluetooth" pull request](https://github.com/GoogleChromeLabs/chromium-bidi/pull/2585) for an example).
2. Run the ["Update WebdriverBiDi types"](https://github.com/GoogleChromeLabs/chromium-bidi/actions/workflows/update-bidi-types.yml) GitHub action. This will create a pull request with your new types. If you added a command, this PR will have a failing check complaining about a non-exhaustive switch statement:
> error: Switch is not exhaustive. Cases not matched: "{NEW_COMMAND_NAME}" @typescript-eslint/switch-exhaustiveness-check
3. Update the created pull request. Add your new command to [`CommandProcessor.#processCommand`](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/bidiMapper/CommandProcessor.ts#L140). For now, just have it throw an UnknownErrorException (see the [example](https://github.com/GoogleChromeLabs/chromium-bidi/pull/2647/files#diff-7f06ce28b8514fd75b759d217bff9f5a471b657bcf78bd893cc291c7945c1cacR169) for how to do this).
```typescript
case '{NEW_COMMAND_NAME}':
throw new UnknownErrorException(
`Method ${command.method} is not implemented.`,
);
```
4. Merge it! Standard PR process: create, review, merge.
### Implement the new command
[`CommandProcessor.#processCommand`](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/bidiMapper/CommandProcessor.ts#L140) handles parsing parameters and running your command.
#### (only if the new command has non-empty parameters) parse command parameters
If your command has parameters, update the [`BidiCommandParameterParser`](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/bidiMapper/BidiParser.ts#L31) and implement the parsing logic in [`BidiNoOpParser`](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/bidiMapper/BidiNoOpParser.ts#L209), [`BidiParser`](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/bidiTab/BidiParser.ts#L182) and [`protocol-parser`](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/protocol-parser/protocol-parser.ts#L386). Look at the [example](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/bidiMapper/BidiParser.ts#L97) for guidance.
#### Implement the new command
Write the core logic for your command in the appropriate domain processor. Again, [example](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/bidiMapper/modules/permissions/PermissionsProcessor.ts#L32) is your friend.
#### Call the module processor's method
Call your new module processor method from `CommandProcessor.#processCommand`, passing in the parsed parameters. [Example](https://github.com/GoogleChromeLabs/chromium-bidi/blob/0f971303281aba1910786035facc5eb54a833232/src/bidiMapper/CommandProcessor.ts#L313).
#### Add e2e tests
Write end-to-end tests for your command, including the happy path and any edge cases that might trip things up. Focus on testing the code in the mapper.
#### Update WPT expectations
Your WPT tests will probably fail now.
> Tests with unexpected results: PASS [expected FAIL] ...
Update the expectations in a draft PR with the "update-expectations" label. This will trigger an automated PR "test: update the expectations for PR" that you'll need to merge to your branch.
#### Merge it!
Mark your PR as ready, get it reviewed, and merge it in.
### Roll in ChromeDriver
This bit usually involves the core devs:
1. [Release](#automatic-release) your changes.
2. [Roll the changes into ChromeDriver](#roll-into-chromium).
+56
View File
@@ -0,0 +1,56 @@
Name: mitt
URL: https://github.com/developit/mitt
Version: 3.0.1
License: MIT
MIT License
Copyright (c) 2021 Jason Miller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-------------------- DEPENDENCY DIVIDER --------------------
Name: zod
URL: https://zod.dev
Version: 3.25.76
License: MIT
MIT License
Copyright (c) 2025 Colin McDonnell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview The entry point to the BiDi Mapper namespace.
* Other modules should only access exports defined in this file.
* XXX: Add ESlint rule for this (https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-restricted-paths.md)
*/
export { BidiServer } from './BidiServer.js';
export { MapperOptions } from './MapperOptions.js';
export type { CdpConnection } from '../cdp/CdpConnection.js';
export type { CdpClient } from '../cdp/CdpClient.js';
export { EventEmitter } from '../utils/EventEmitter.js';
export type { BidiTransport } from './BidiTransport.js';
export { OutgoingMessage } from './OutgoingMessage.js';
export type { BidiCommandParameterParser } from './BidiParser.js';
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview The entry point to the BiDi Mapper namespace.
* Other modules should only access exports defined in this file.
* XXX: Add ESlint rule for this (https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-restricted-paths.md)
*/
export { BidiServer } from './BidiServer.js';
export { EventEmitter } from '../utils/EventEmitter.js';
export { OutgoingMessage } from './OutgoingMessage.js';
//# sourceMappingURL=BidiMapper.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"BidiMapper.js","sourceRoot":"","sources":["../../src/bidiMapper/BidiMapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;GAIG;AACH,OAAO,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAI3C,OAAO,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAEtD,OAAO,EAAC,eAAe,EAAC,MAAM,sBAAsB,CAAC"}
+94
View File
@@ -0,0 +1,94 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Browser, BrowsingContext, Cdp, Emulation, Input, Network, Script, Session, Storage, Permissions, Bluetooth, WebExtension, UAClientHints } from '../protocol/protocol.js';
import type { BidiCommandParameterParser } from './BidiParser.js';
export declare class BidiNoOpParser implements BidiCommandParameterParser {
parseDisableSimulationParameters(params: unknown): Bluetooth.DisableSimulationParameters;
parseHandleRequestDevicePromptParams(params: unknown): Bluetooth.HandleRequestDevicePromptParameters;
parseSimulateAdapterParameters(params: unknown): Bluetooth.SimulateAdapterParameters;
parseSimulateAdvertisementParameters(params: unknown): Bluetooth.SimulateAdvertisementParameters;
parseSimulateCharacteristicParameters(params: unknown): Bluetooth.SimulateCharacteristicParameters;
parseSimulateCharacteristicResponseParameters(params: unknown): Bluetooth.SimulateCharacteristicResponseParameters;
parseSimulateDescriptorParameters(params: unknown): Bluetooth.SimulateDescriptorParameters;
parseSimulateDescriptorResponseParameters(params: unknown): Bluetooth.SimulateDescriptorResponseParameters;
parseSimulateGattConnectionResponseParameters(params: unknown): Bluetooth.SimulateGattConnectionResponseParameters;
parseSimulateGattDisconnectionParameters(params: unknown): Bluetooth.SimulateGattDisconnectionParameters;
parseSimulatePreconnectedPeripheralParameters(params: unknown): Bluetooth.SimulatePreconnectedPeripheralParameters;
parseSimulateServiceParameters(params: unknown): Bluetooth.SimulateServiceParameters;
parseCreateUserContextParameters(params: unknown): Browser.CreateUserContextParameters;
parseRemoveUserContextParameters(params: unknown): Browser.RemoveUserContextParameters;
parseSetClientWindowStateParameters(params: unknown): Browser.SetClientWindowStateParameters;
parseSetDownloadBehaviorParameters(params: unknown): Browser.SetDownloadBehaviorParameters;
parseActivateParams(params: unknown): BrowsingContext.ActivateParameters;
parseCaptureScreenshotParams(params: unknown): BrowsingContext.CaptureScreenshotParameters;
parseCloseParams(params: unknown): BrowsingContext.CloseParameters;
parseCreateParams(params: unknown): BrowsingContext.CreateParameters;
parseGetTreeParams(params: unknown): BrowsingContext.GetTreeParameters;
parseHandleUserPromptParams(params: unknown): BrowsingContext.HandleUserPromptParameters;
parseLocateNodesParams(params: unknown): BrowsingContext.LocateNodesParameters;
parseNavigateParams(params: unknown): BrowsingContext.NavigateParameters;
parsePrintParams(params: unknown): BrowsingContext.PrintParameters;
parseReloadParams(params: unknown): BrowsingContext.ReloadParameters;
parseSetBypassCspParams(params: unknown): BrowsingContext.SetBypassCspParameters;
parseSetViewportParams(params: unknown): BrowsingContext.SetViewportParameters;
parseTraverseHistoryParams(params: unknown): BrowsingContext.TraverseHistoryParameters;
parseGetSessionParams(params: unknown): Cdp.GetSessionParameters;
parseResolveRealmParams(params: unknown): Cdp.ResolveRealmParameters;
parseSendCommandParams(params: unknown): Cdp.SendCommandParameters;
parseSetClientHintsOverrideParams(params: unknown): UAClientHints.UserAgentClientHints.SetClientHintsOverrideCommand['params'];
parseSetForcedColorsModeThemeOverrideParams(params: unknown): Emulation.SetForcedColorsModeThemeOverrideParameters;
parseSetGeolocationOverrideParams(params: unknown): Emulation.SetGeolocationOverrideParameters;
parseSetLocaleOverrideParams(params: unknown): Emulation.SetLocaleOverrideParameters;
parseSetNetworkConditionsParams(params: unknown): Emulation.SetNetworkConditionsParameters;
parseSetScreenOrientationOverrideParams(params: unknown): Emulation.SetScreenOrientationOverrideParameters;
parseSetScreenSettingsOverrideParams(params: unknown): Emulation.SetScreenSettingsOverrideParameters;
parseSetScriptingEnabledParams(params: unknown): Emulation.SetScriptingEnabledParameters;
parseSetScrollbarTypeOverrideParams(params: unknown): Emulation.SetScrollbarTypeOverrideParameters;
parseSetTimezoneOverrideParams(params: unknown): Emulation.SetTimezoneOverrideParameters;
parseSetTouchOverrideParams(params: unknown): Emulation.SetTouchOverrideParameters;
parseSetUserAgentOverrideParams(params: unknown): Emulation.SetUserAgentOverrideParameters;
parseAddPreloadScriptParams(params: unknown): Script.AddPreloadScriptParameters;
parseCallFunctionParams(params: unknown): Script.CallFunctionParameters;
parseDisownParams(params: unknown): Script.DisownParameters;
parseEvaluateParams(params: unknown): Script.EvaluateParameters;
parseGetRealmsParams(params: unknown): Script.GetRealmsParameters;
parseRemovePreloadScriptParams(params: unknown): Script.RemovePreloadScriptParameters;
parsePerformActionsParams(params: unknown): Input.PerformActionsParameters;
parseReleaseActionsParams(params: unknown): Input.ReleaseActionsParameters;
parseSetFilesParams(params: unknown): Input.SetFilesParameters;
parseAddDataCollectorParams(params: unknown): Network.AddDataCollectorParameters;
parseAddInterceptParams(params: unknown): Network.AddInterceptParameters;
parseContinueRequestParams(params: unknown): Network.ContinueRequestParameters;
parseContinueResponseParams(params: unknown): Network.ContinueResponseParameters;
parseContinueWithAuthParams(params: unknown): Network.ContinueWithAuthParameters;
parseDisownDataParams(params: unknown): Network.DisownDataParameters;
parseFailRequestParams(params: unknown): Network.FailRequestParameters;
parseGetDataParams(params: unknown): Network.GetDataParameters;
parseProvideResponseParams(params: unknown): Network.ProvideResponseParameters;
parseRemoveDataCollectorParams(params: unknown): Network.RemoveDataCollectorParameters;
parseRemoveInterceptParams(params: unknown): Network.RemoveInterceptParameters;
parseSetCacheBehaviorParams(params: unknown): Network.SetCacheBehaviorParameters;
parseSetExtraHeadersParams(params: unknown): Network.SetExtraHeadersParameters;
parseSetPermissionsParams(params: unknown): Permissions.SetPermissionParameters;
parseSubscribeParams(params: unknown): Session.SubscribeParameters;
parseUnsubscribeParams(params: unknown): Session.UnsubscribeByAttributesRequest | Session.UnsubscribeByIdRequest;
parseDeleteCookiesParams(params: unknown): Storage.DeleteCookiesParameters;
parseGetCookiesParams(params: unknown): Storage.GetCookiesParameters;
parseSetCookieParams(params: unknown): Storage.SetCookieParameters;
parseInstallParams(params: unknown): WebExtension.InstallParameters;
parseUninstallParams(params: unknown): WebExtension.UninstallParameters;
}
+276
View File
@@ -0,0 +1,276 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class BidiNoOpParser {
// Bluetooth module
// keep-sorted start block=yes
parseDisableSimulationParameters(params) {
return params;
}
parseHandleRequestDevicePromptParams(params) {
return params;
}
parseSimulateAdapterParameters(params) {
return params;
}
parseSimulateAdvertisementParameters(params) {
return params;
}
parseSimulateCharacteristicParameters(params) {
return params;
}
parseSimulateCharacteristicResponseParameters(params) {
return params;
}
parseSimulateDescriptorParameters(params) {
return params;
}
parseSimulateDescriptorResponseParameters(params) {
return params;
}
parseSimulateGattConnectionResponseParameters(params) {
return params;
}
parseSimulateGattDisconnectionParameters(params) {
return params;
}
parseSimulatePreconnectedPeripheralParameters(params) {
return params;
}
parseSimulateServiceParameters(params) {
return params;
}
// keep-sorted end
// Browser module
// keep-sorted start block=yes
parseCreateUserContextParameters(params) {
return params;
}
parseRemoveUserContextParameters(params) {
return params;
}
parseSetClientWindowStateParameters(params) {
return params;
}
parseSetDownloadBehaviorParameters(params) {
return params;
}
// keep-sorted end
// Browsing Context module
// keep-sorted start block=yes
parseActivateParams(params) {
return params;
}
parseCaptureScreenshotParams(params) {
return params;
}
parseCloseParams(params) {
return params;
}
parseCreateParams(params) {
return params;
}
parseGetTreeParams(params) {
return params;
}
parseHandleUserPromptParams(params) {
return params;
}
parseLocateNodesParams(params) {
return params;
}
parseNavigateParams(params) {
return params;
}
parsePrintParams(params) {
return params;
}
parseReloadParams(params) {
return params;
}
parseSetBypassCspParams(params) {
return params;
}
parseSetViewportParams(params) {
return params;
}
parseTraverseHistoryParams(params) {
return params;
}
// keep-sorted end
// CDP module
// keep-sorted start block=yes
parseGetSessionParams(params) {
return params;
}
parseResolveRealmParams(params) {
return params;
}
parseSendCommandParams(params) {
return params;
}
// keep-sorted end
// Emulation module
// keep-sorted start block=yes
parseSetClientHintsOverrideParams(params) {
return params;
}
parseSetForcedColorsModeThemeOverrideParams(params) {
return params;
}
parseSetGeolocationOverrideParams(params) {
return params;
}
parseSetLocaleOverrideParams(params) {
return params;
}
parseSetNetworkConditionsParams(params) {
return params;
}
parseSetScreenOrientationOverrideParams(params) {
return params;
}
parseSetScreenSettingsOverrideParams(params) {
return params;
}
parseSetScriptingEnabledParams(params) {
return params;
}
parseSetScrollbarTypeOverrideParams(params) {
return params;
}
parseSetTimezoneOverrideParams(params) {
return params;
}
parseSetTouchOverrideParams(params) {
return params;
}
parseSetUserAgentOverrideParams(params) {
return params;
}
// keep-sorted end
// Script module
// keep-sorted start block=yes
parseAddPreloadScriptParams(params) {
return params;
}
parseCallFunctionParams(params) {
return params;
}
parseDisownParams(params) {
return params;
}
parseEvaluateParams(params) {
return params;
}
parseGetRealmsParams(params) {
return params;
}
parseRemovePreloadScriptParams(params) {
return params;
}
// keep-sorted end
// Input module
// keep-sorted start block=yes
parsePerformActionsParams(params) {
return params;
}
parseReleaseActionsParams(params) {
return params;
}
parseSetFilesParams(params) {
return params;
}
// keep-sorted end
// Network module
// keep-sorted start block=yes
parseAddDataCollectorParams(params) {
return params;
}
parseAddInterceptParams(params) {
return params;
}
parseContinueRequestParams(params) {
return params;
}
parseContinueResponseParams(params) {
return params;
}
parseContinueWithAuthParams(params) {
return params;
}
parseDisownDataParams(params) {
return params;
}
parseFailRequestParams(params) {
return params;
}
parseGetDataParams(params) {
return params;
}
parseProvideResponseParams(params) {
return params;
}
parseRemoveDataCollectorParams(params) {
return params;
}
parseRemoveInterceptParams(params) {
return params;
}
parseSetCacheBehaviorParams(params) {
return params;
}
parseSetExtraHeadersParams(params) {
return params;
}
// keep-sorted end
// Permissions module
// keep-sorted start block=yes
parseSetPermissionsParams(params) {
return params;
}
// keep-sorted end
// Session module
// keep-sorted start block=yes
parseSubscribeParams(params) {
return params;
}
parseUnsubscribeParams(params) {
return params;
}
// keep-sorted end
// Storage module
// keep-sorted start block=yes
parseDeleteCookiesParams(params) {
return params;
}
parseGetCookiesParams(params) {
return params;
}
parseSetCookieParams(params) {
return params;
}
// keep-sorted end
// WebExtenstion module
// keep-sorted start block=yes
parseInstallParams(params) {
return params;
}
parseUninstallParams(params) {
return params;
}
}
//# sourceMappingURL=BidiNoOpParser.js.map
@@ -0,0 +1 @@
{"version":3,"file":"BidiNoOpParser.js","sourceRoot":"","sources":["../../src/bidiMapper/BidiNoOpParser.ts"],"names":[],"mappings":"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"}
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Bluetooth, Browser, BrowsingContext, Cdp, Emulation, Input, Network, Permissions, Script, Session, Storage, WebExtension, UAClientHints } from '../protocol/protocol.js';
export interface BidiCommandParameterParser {
parseDisableSimulationParameters(params: unknown): Bluetooth.DisableSimulationParameters;
parseHandleRequestDevicePromptParams(params: unknown): Bluetooth.HandleRequestDevicePromptParameters;
parseSimulateAdapterParameters(params: unknown): Bluetooth.SimulateAdapterParameters;
parseSimulateAdvertisementParameters(params: unknown): Bluetooth.SimulateAdvertisementParameters;
parseSimulateCharacteristicParameters(params: unknown): Bluetooth.SimulateCharacteristicParameters;
parseSimulateCharacteristicResponseParameters(params: unknown): Bluetooth.SimulateCharacteristicResponseParameters;
parseSimulateDescriptorParameters(params: unknown): Bluetooth.SimulateDescriptorParameters;
parseSimulateDescriptorResponseParameters(params: unknown): Bluetooth.SimulateDescriptorResponseParameters;
parseSimulateGattConnectionResponseParameters(params: unknown): Bluetooth.SimulateGattConnectionResponseParameters;
parseSimulateGattDisconnectionParameters(params: unknown): Bluetooth.SimulateGattDisconnectionParameters;
parseSimulatePreconnectedPeripheralParameters(params: unknown): Bluetooth.SimulatePreconnectedPeripheralParameters;
parseSimulateServiceParameters(params: unknown): Bluetooth.SimulateServiceParameters;
parseCreateUserContextParameters(params: unknown): Browser.CreateUserContextParameters;
parseRemoveUserContextParameters(params: unknown): Browser.RemoveUserContextParameters;
parseSetClientWindowStateParameters(params: unknown): Browser.SetClientWindowStateParameters;
parseSetDownloadBehaviorParameters(params: unknown): Browser.SetDownloadBehaviorParameters;
parseActivateParams(params: unknown): BrowsingContext.ActivateParameters;
parseCaptureScreenshotParams(params: unknown): BrowsingContext.CaptureScreenshotParameters;
parseCloseParams(params: unknown): BrowsingContext.CloseParameters;
parseCreateParams(params: unknown): BrowsingContext.CreateParameters;
parseGetTreeParams(params: unknown): BrowsingContext.GetTreeParameters;
parseHandleUserPromptParams(params: unknown): BrowsingContext.HandleUserPromptParameters;
parseLocateNodesParams(params: unknown): BrowsingContext.LocateNodesParameters;
parseNavigateParams(params: unknown): BrowsingContext.NavigateParameters;
parsePrintParams(params: unknown): BrowsingContext.PrintParameters;
parseReloadParams(params: unknown): BrowsingContext.ReloadParameters;
parseSetBypassCspParams(params: unknown): BrowsingContext.SetBypassCspParameters;
parseSetViewportParams(params: unknown): BrowsingContext.SetViewportParameters;
parseTraverseHistoryParams(params: unknown): BrowsingContext.TraverseHistoryParameters;
parseGetSessionParams(params: unknown): Cdp.GetSessionParameters;
parseResolveRealmParams(params: unknown): Cdp.ResolveRealmParameters;
parseSendCommandParams(params: unknown): Cdp.SendCommandParameters;
parseSetClientHintsOverrideParams(params: unknown): UAClientHints.UserAgentClientHints.SetClientHintsOverrideCommand['params'];
parseSetForcedColorsModeThemeOverrideParams(params: unknown): Emulation.SetForcedColorsModeThemeOverrideParameters;
parseSetGeolocationOverrideParams(params: unknown): Emulation.SetGeolocationOverrideParameters;
parseSetLocaleOverrideParams(params: unknown): Emulation.SetLocaleOverrideParameters;
parseSetNetworkConditionsParams(params: unknown): Emulation.SetNetworkConditionsParameters;
parseSetScreenOrientationOverrideParams(params: unknown): Emulation.SetScreenOrientationOverrideParameters;
parseSetScreenSettingsOverrideParams(params: unknown): Emulation.SetScreenSettingsOverrideParameters;
parseSetScriptingEnabledParams(params: unknown): Emulation.SetScriptingEnabledParameters;
parseSetScrollbarTypeOverrideParams(params: unknown): Emulation.SetScrollbarTypeOverrideParameters;
parseSetTimezoneOverrideParams(params: unknown): Emulation.SetTimezoneOverrideParameters;
parseSetTouchOverrideParams(params: unknown): Emulation.SetTouchOverrideParameters;
parseSetUserAgentOverrideParams(params: unknown): Emulation.SetUserAgentOverrideParameters;
parsePerformActionsParams(params: unknown): Input.PerformActionsParameters;
parseReleaseActionsParams(params: unknown): Input.ReleaseActionsParameters;
parseSetFilesParams(params: unknown): Input.SetFilesParameters;
parseSetPermissionsParams(params: unknown): Permissions.SetPermissionParameters;
parseAddDataCollectorParams(params: unknown): Network.AddDataCollectorParameters;
parseAddInterceptParams(params: unknown): Network.AddInterceptParameters;
parseContinueRequestParams(params: unknown): Network.ContinueRequestParameters;
parseContinueResponseParams(params: unknown): Network.ContinueResponseParameters;
parseContinueWithAuthParams(params: unknown): Network.ContinueWithAuthParameters;
parseDisownDataParams(params: unknown): Network.DisownDataParameters;
parseFailRequestParams(params: unknown): Network.FailRequestParameters;
parseGetDataParams(params: unknown): Network.GetDataParameters;
parseProvideResponseParams(params: unknown): Network.ProvideResponseParameters;
parseRemoveDataCollectorParams(params: unknown): Network.RemoveDataCollectorParameters;
parseRemoveInterceptParams(params: unknown): Network.RemoveInterceptParameters;
parseSetCacheBehaviorParams(params: unknown): Network.SetCacheBehaviorParameters;
parseSetExtraHeadersParams(params: unknown): Network.SetExtraHeadersParameters;
parseAddPreloadScriptParams(params: unknown): Script.AddPreloadScriptParameters;
parseCallFunctionParams(params: unknown): Script.CallFunctionParameters;
parseDisownParams(params: unknown): Script.DisownParameters;
parseEvaluateParams(params: unknown): Script.EvaluateParameters;
parseGetRealmsParams(params: unknown): Script.GetRealmsParameters;
parseRemovePreloadScriptParams(params: unknown): Script.RemovePreloadScriptParameters;
parseSubscribeParams(params: unknown): Session.SubscribeParameters;
parseUnsubscribeParams(params: unknown): Session.UnsubscribeParameters;
parseDeleteCookiesParams(params: unknown): Storage.DeleteCookiesParameters;
parseGetCookiesParams(params: unknown): Storage.GetCookiesParameters;
parseSetCookieParams(params: unknown): Storage.SetCookieParameters;
parseInstallParams(params: unknown): WebExtension.InstallParameters;
parseUninstallParams(params: unknown): WebExtension.UninstallParameters;
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
//# sourceMappingURL=BidiParser.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"BidiParser.js","sourceRoot":"","sources":["../../src/bidiMapper/BidiParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}
+42
View File
@@ -0,0 +1,42 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CdpClient } from '../cdp/CdpClient.js';
import type { CdpConnection } from '../cdp/CdpConnection.js';
import type { ChromiumBidi } from '../protocol/protocol.js';
import { EventEmitter } from '../utils/EventEmitter.js';
import { type LoggerFn } from '../utils/log.js';
import type { Result } from '../utils/result.js';
import type { BidiCommandParameterParser } from './BidiParser.js';
import type { BidiTransport } from './BidiTransport.js';
import type { OutgoingMessage } from './OutgoingMessage.js';
interface BidiServerEvent extends Record<string | symbol, unknown> {
message: ChromiumBidi.Command;
}
export declare class BidiServer extends EventEmitter<BidiServerEvent> {
#private;
private constructor();
/**
* Creates and starts BiDi Mapper instance.
*/
static createAndStart(bidiTransport: BidiTransport, cdpConnection: CdpConnection, browserCdpClient: CdpClient, selfTargetId: string, parser?: BidiCommandParameterParser, logger?: LoggerFn): Promise<BidiServer>;
/**
* Sends BiDi message.
*/
emitOutgoingMessage(messageEntry: Promise<Result<OutgoingMessage>>, event: string): void;
close(): void;
}
export {};
+165
View File
@@ -0,0 +1,165 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from '../utils/EventEmitter.js';
import { LogType } from '../utils/log.js';
import { ProcessingQueue } from '../utils/ProcessingQueue.js';
import { CommandProcessor } from './CommandProcessor.js';
import { BluetoothProcessor } from './modules/bluetooth/BluetoothProcessor.js';
import { ContextConfigStorage } from './modules/browser/ContextConfigStorage.js';
import { UserContextStorage } from './modules/browser/UserContextStorage.js';
import { CdpTargetManager } from './modules/cdp/CdpTargetManager.js';
import { BrowsingContextStorage } from './modules/context/BrowsingContextStorage.js';
import { NetworkStorage } from './modules/network/NetworkStorage.js';
import { PreloadScriptStorage } from './modules/script/PreloadScriptStorage.js';
import { RealmStorage } from './modules/script/RealmStorage.js';
import { EventManager, } from './modules/session/EventManager.js';
import { SpeculationProcessor } from './modules/speculation/SpeculationProcessor.js';
export class BidiServer extends EventEmitter {
#messageQueue;
#transport;
#commandProcessor;
#eventManager;
#browsingContextStorage = new BrowsingContextStorage();
#realmStorage = new RealmStorage();
#preloadScriptStorage = new PreloadScriptStorage();
#bluetoothProcessor;
#speculationProcessor;
#logger;
#handleIncomingMessage = (message) => {
void this.#commandProcessor.processCommand(message).catch((error) => {
this.#logger?.(LogType.debugError, error);
});
};
#processOutgoingMessage = async (messageEntry) => {
const message = messageEntry.message;
if (messageEntry.googChannel !== null) {
message['goog:channel'] = messageEntry.googChannel;
}
await this.#transport.sendMessage(message);
};
constructor(bidiTransport, cdpConnection, browserCdpClient, selfTargetId, defaultUserContextId, defaultUserAgent, parser, logger) {
super();
this.#logger = logger;
this.#messageQueue = new ProcessingQueue(this.#processOutgoingMessage, this.#logger);
this.#transport = bidiTransport;
this.#transport.setOnMessage(this.#handleIncomingMessage);
const contextConfigStorage = new ContextConfigStorage();
const userContextStorage = new UserContextStorage(browserCdpClient);
this.#eventManager = new EventManager(this.#browsingContextStorage, userContextStorage);
const networkStorage = new NetworkStorage(this.#eventManager, this.#browsingContextStorage, browserCdpClient, logger);
this.#bluetoothProcessor = new BluetoothProcessor(this.#eventManager, this.#browsingContextStorage);
this.#speculationProcessor = new SpeculationProcessor(this.#eventManager, this.#logger);
this.#commandProcessor = new CommandProcessor(cdpConnection, browserCdpClient, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.#preloadScriptStorage, networkStorage, contextConfigStorage, this.#bluetoothProcessor, userContextStorage, parser, async (options) => {
// This is required to ignore certificate errors when service worker is fetched.
await browserCdpClient.sendCommand('Security.setIgnoreCertificateErrors', {
ignore: options.acceptInsecureCerts ?? false,
});
contextConfigStorage.updateGlobalConfig({
acceptInsecureCerts: options.acceptInsecureCerts ?? false,
userPromptHandler: options.unhandledPromptBehavior,
prerenderingDisabled: options?.['goog:prerenderingDisabled'] ?? false,
disableNetworkDurableMessages: options?.['goog:disableNetworkDurableMessages'],
});
new CdpTargetManager(cdpConnection, browserCdpClient, selfTargetId, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, networkStorage, contextConfigStorage, this.#bluetoothProcessor, this.#speculationProcessor, this.#preloadScriptStorage, defaultUserContextId, defaultUserAgent, logger);
// Needed to get events about new targets.
await browserCdpClient.sendCommand('Target.setDiscoverTargets', {
discover: true,
});
// Needed to automatically attach to new targets.
await browserCdpClient.sendCommand('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
// Browser session should attach to tab instead of the page, so that
// prerendering is not blocked.
filter: [
{
type: 'page',
exclude: true,
},
{},
],
});
await this.#topLevelContextsLoaded();
}, this.#logger);
this.#eventManager.on("event" /* EventManagerEvents.Event */, ({ message, event }) => {
this.emitOutgoingMessage(message, event);
});
this.#commandProcessor.on("response" /* CommandProcessorEvents.Response */, ({ message, event }) => {
this.emitOutgoingMessage(message, event);
});
}
/**
* Creates and starts BiDi Mapper instance.
*/
static async createAndStart(bidiTransport, cdpConnection, browserCdpClient, selfTargetId, parser, logger) {
const [defaultUserContextId, version] = await Promise.all([
this.#getDefaultUserContextId(browserCdpClient),
// Fetch the default User Agent to be used in `CdpTarget`. This allows to avoid
// round trips to the browser for every target override.
browserCdpClient.sendCommand('Browser.getVersion'),
// Required for `Browser.downloadWillBegin` events.
browserCdpClient.sendCommand('Browser.setDownloadBehavior', {
behavior: 'default',
eventsEnabled: true,
}),
]);
const server = new BidiServer(bidiTransport, cdpConnection, browserCdpClient, selfTargetId, defaultUserContextId, version.userAgent, parser, logger);
return server;
}
static async #getDefaultUserContextId(browserCdpClient) {
// In chromium before `145.0.7578.0`, the default context is not exposed in
// `Target.getBrowserContexts`, but can be observed via `Target.getTargets`.
// If so, try to determine the default browser context by checking which one
// is mentioned in `Target.getTargets` and not in
// `Target.getBrowserContexts`.
// TODO(after 2026-02-24): rely only on `defaultBrowserContextId` from
// `Target.getBrowserContexts` after Chromium 145 reaches stable.
const [{ defaultBrowserContextId, browserContextIds }, { targetInfos }] = await Promise.all([
browserCdpClient.sendCommand('Target.getBrowserContexts'),
browserCdpClient.sendCommand('Target.getTargets'),
]);
if (defaultBrowserContextId) {
return defaultBrowserContextId;
}
for (const info of targetInfos) {
if (info.browserContextId &&
!browserContextIds.includes(info.browserContextId)) {
// The target belongs to a browser context that is not mentioned in
// `Target.getBrowserContexts`. This is the default browser context.
return info.browserContextId;
}
}
// The browser context is unknown.
return 'default';
}
/**
* Sends BiDi message.
*/
emitOutgoingMessage(messageEntry, event) {
this.#messageQueue.add(messageEntry, event);
}
close() {
this.#transport.close();
}
async #topLevelContextsLoaded() {
await Promise.all(this.#browsingContextStorage
.getTopLevelContexts()
.map((c) => c.lifecycleLoaded()));
}
}
//# sourceMappingURL=BidiServer.js.map
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ChromiumBidi } from '../protocol/protocol.js';
export interface BidiTransport {
setOnMessage: (handler: (message: ChromiumBidi.Command) => Promise<void> | void) => void;
sendMessage: (message: ChromiumBidi.Message) => Promise<void> | void;
close(): void;
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
//# sourceMappingURL=BidiTransport.js.map
@@ -0,0 +1 @@
{"version":3,"file":"BidiTransport.js","sourceRoot":"","sources":["../../src/bidiMapper/BidiTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}
@@ -0,0 +1,48 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CdpClient } from '../cdp/CdpClient.js';
import type { CdpConnection } from '../cdp/CdpConnection.js';
import { type ChromiumBidi } from '../protocol/protocol.js';
import { EventEmitter } from '../utils/EventEmitter.js';
import { type LoggerFn } from '../utils/log.js';
import type { Result } from '../utils/result.js';
import type { BidiCommandParameterParser } from './BidiParser.js';
import type { MapperOptions } from './MapperOptions.js';
import type { BluetoothProcessor } from './modules/bluetooth/BluetoothProcessor.js';
import type { ContextConfigStorage } from './modules/browser/ContextConfigStorage.js';
import type { UserContextStorage } from './modules/browser/UserContextStorage.js';
import type { BrowsingContextStorage } from './modules/context/BrowsingContextStorage.js';
import type { NetworkStorage } from './modules/network/NetworkStorage.js';
import type { PreloadScriptStorage } from './modules/script/PreloadScriptStorage.js';
import type { RealmStorage } from './modules/script/RealmStorage.js';
import type { EventManager } from './modules/session/EventManager.js';
import { OutgoingMessage } from './OutgoingMessage.js';
export declare const enum CommandProcessorEvents {
Response = "response"
}
interface CommandProcessorEventsMap extends Record<string | symbol, unknown> {
[CommandProcessorEvents.Response]: {
message: Promise<Result<OutgoingMessage>>;
event: string;
};
}
export declare class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
#private;
constructor(cdpConnection: CdpConnection, browserCdpClient: CdpClient, eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, preloadScriptStorage: PreloadScriptStorage, networkStorage: NetworkStorage, contextConfigStorage: ContextConfigStorage, bluetoothProcessor: BluetoothProcessor, userContextStorage: UserContextStorage, parser: BidiCommandParameterParser | undefined, initConnection: (options: MapperOptions) => Promise<void>, logger?: LoggerFn);
processCommand(command: ChromiumBidi.Command): Promise<void>;
}
export {};
+327
View File
@@ -0,0 +1,327 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Exception, UnknownCommandException, UnknownErrorException, NoSuchFrameException, UnsupportedOperationException, } from '../protocol/protocol.js';
import { EventEmitter } from '../utils/EventEmitter.js';
import { LogType } from '../utils/log.js';
import { BidiNoOpParser } from './BidiNoOpParser.js';
import { BrowserProcessor } from './modules/browser/BrowserProcessor.js';
import { CdpProcessor } from './modules/cdp/CdpProcessor.js';
import { BrowsingContextProcessor } from './modules/context/BrowsingContextProcessor.js';
import { EmulationProcessor } from './modules/emulation/EmulationProcessor.js';
import { InputProcessor } from './modules/input/InputProcessor.js';
import { NetworkProcessor } from './modules/network/NetworkProcessor.js';
import { PermissionsProcessor } from './modules/permissions/PermissionsProcessor.js';
import { ScriptProcessor } from './modules/script/ScriptProcessor.js';
import { SessionProcessor } from './modules/session/SessionProcessor.js';
import { StorageProcessor } from './modules/storage/StorageProcessor.js';
import { WebExtensionProcessor } from './modules/webExtension/WebExtensionProcessor.js';
import { OutgoingMessage } from './OutgoingMessage.js';
export class CommandProcessor extends EventEmitter {
// keep-sorted start
#bluetoothProcessor;
#browserCdpClient;
#browserProcessor;
#browsingContextProcessor;
#cdpProcessor;
#emulationProcessor;
#inputProcessor;
#networkProcessor;
#permissionsProcessor;
#scriptProcessor;
#sessionProcessor;
#storageProcessor;
#webExtensionProcessor;
// keep-sorted end
#parser;
#logger;
constructor(cdpConnection, browserCdpClient, eventManager, browsingContextStorage, realmStorage, preloadScriptStorage, networkStorage, contextConfigStorage, bluetoothProcessor, userContextStorage, parser = new BidiNoOpParser(), initConnection, logger) {
super();
this.#browserCdpClient = browserCdpClient;
this.#parser = parser;
this.#logger = logger;
this.#bluetoothProcessor = bluetoothProcessor;
// keep-sorted start block=yes
this.#browserProcessor = new BrowserProcessor(browserCdpClient, browsingContextStorage, contextConfigStorage, userContextStorage);
this.#browsingContextProcessor = new BrowsingContextProcessor(browserCdpClient, browsingContextStorage, userContextStorage, contextConfigStorage, eventManager);
this.#cdpProcessor = new CdpProcessor(browsingContextStorage, realmStorage, cdpConnection, browserCdpClient);
this.#emulationProcessor = new EmulationProcessor(browsingContextStorage, userContextStorage, contextConfigStorage);
this.#inputProcessor = new InputProcessor(browsingContextStorage);
this.#networkProcessor = new NetworkProcessor(browsingContextStorage, networkStorage, userContextStorage, contextConfigStorage);
this.#permissionsProcessor = new PermissionsProcessor(browserCdpClient);
this.#scriptProcessor = new ScriptProcessor(eventManager, browsingContextStorage, realmStorage, preloadScriptStorage, userContextStorage, logger);
this.#sessionProcessor = new SessionProcessor(eventManager, browserCdpClient, initConnection);
this.#storageProcessor = new StorageProcessor(browserCdpClient, browsingContextStorage, logger);
this.#webExtensionProcessor = new WebExtensionProcessor(browserCdpClient);
// keep-sorted end
}
async #processCommand(command) {
switch (command.method) {
// Bluetooth module
// keep-sorted start block=yes
case 'bluetooth.disableSimulation':
return await this.#bluetoothProcessor.disableSimulation(this.#parser.parseDisableSimulationParameters(command.params));
case 'bluetooth.handleRequestDevicePrompt':
return await this.#bluetoothProcessor.handleRequestDevicePrompt(this.#parser.parseHandleRequestDevicePromptParams(command.params));
case 'bluetooth.simulateAdapter':
return await this.#bluetoothProcessor.simulateAdapter(this.#parser.parseSimulateAdapterParameters(command.params));
case 'bluetooth.simulateAdvertisement':
return await this.#bluetoothProcessor.simulateAdvertisement(this.#parser.parseSimulateAdvertisementParameters(command.params));
case 'bluetooth.simulateCharacteristic':
return await this.#bluetoothProcessor.simulateCharacteristic(this.#parser.parseSimulateCharacteristicParameters(command.params));
case 'bluetooth.simulateCharacteristicResponse':
return await this.#bluetoothProcessor.simulateCharacteristicResponse(this.#parser.parseSimulateCharacteristicResponseParameters(command.params));
case 'bluetooth.simulateDescriptor':
return await this.#bluetoothProcessor.simulateDescriptor(this.#parser.parseSimulateDescriptorParameters(command.params));
case 'bluetooth.simulateDescriptorResponse':
return await this.#bluetoothProcessor.simulateDescriptorResponse(this.#parser.parseSimulateDescriptorResponseParameters(command.params));
case 'bluetooth.simulateGattConnectionResponse':
return await this.#bluetoothProcessor.simulateGattConnectionResponse(this.#parser.parseSimulateGattConnectionResponseParameters(command.params));
case 'bluetooth.simulateGattDisconnection':
return await this.#bluetoothProcessor.simulateGattDisconnection(this.#parser.parseSimulateGattDisconnectionParameters(command.params));
case 'bluetooth.simulatePreconnectedPeripheral':
return await this.#bluetoothProcessor.simulatePreconnectedPeripheral(this.#parser.parseSimulatePreconnectedPeripheralParameters(command.params));
case 'bluetooth.simulateService':
return await this.#bluetoothProcessor.simulateService(this.#parser.parseSimulateServiceParameters(command.params));
// keep-sorted end
// Browser module
// keep-sorted start block=yes
case 'browser.close':
return this.#browserProcessor.close();
case 'browser.createUserContext':
return await this.#browserProcessor.createUserContext(this.#parser.parseCreateUserContextParameters(command.params));
case 'browser.getClientWindows':
return await this.#browserProcessor.getClientWindows();
case 'browser.getUserContexts':
return await this.#browserProcessor.getUserContexts();
case 'browser.removeUserContext':
return await this.#browserProcessor.removeUserContext(this.#parser.parseRemoveUserContextParameters(command.params));
case 'browser.setClientWindowState':
return await this.#browserProcessor.setClientWindowState(this.#parser.parseSetClientWindowStateParameters(command.params));
case 'browser.setDownloadBehavior':
return await this.#browserProcessor.setDownloadBehavior(this.#parser.parseSetDownloadBehaviorParameters(command.params));
// keep-sorted end
// Browsing Context module
// keep-sorted start block=yes
case 'browsingContext.activate':
return await this.#browsingContextProcessor.activate(this.#parser.parseActivateParams(command.params));
case 'browsingContext.captureScreenshot':
return await this.#browsingContextProcessor.captureScreenshot(this.#parser.parseCaptureScreenshotParams(command.params));
case 'browsingContext.close':
return await this.#browsingContextProcessor.close(this.#parser.parseCloseParams(command.params));
case 'browsingContext.create':
return await this.#browsingContextProcessor.create(this.#parser.parseCreateParams(command.params));
case 'browsingContext.getTree':
return this.#browsingContextProcessor.getTree(this.#parser.parseGetTreeParams(command.params));
case 'browsingContext.handleUserPrompt':
return await this.#browsingContextProcessor.handleUserPrompt(this.#parser.parseHandleUserPromptParams(command.params));
case 'browsingContext.locateNodes':
return await this.#browsingContextProcessor.locateNodes(this.#parser.parseLocateNodesParams(command.params));
case 'browsingContext.navigate':
return await this.#browsingContextProcessor.navigate(this.#parser.parseNavigateParams(command.params));
case 'browsingContext.print':
return await this.#browsingContextProcessor.print(this.#parser.parsePrintParams(command.params));
case 'browsingContext.reload':
return await this.#browsingContextProcessor.reload(this.#parser.parseReloadParams(command.params));
case 'browsingContext.setBypassCSP':
this.#parser.parseSetBypassCspParams(command.params);
throw new UnsupportedOperationException(`Method ${command.method} is not implemented.`);
case 'browsingContext.setViewport':
return await this.#browsingContextProcessor.setViewport(this.#parser.parseSetViewportParams(command.params));
case 'browsingContext.traverseHistory':
return await this.#browsingContextProcessor.traverseHistory(this.#parser.parseTraverseHistoryParams(command.params));
// keep-sorted end
// CDP module
// keep-sorted start block=yes
case 'goog:cdp.getSession':
return this.#cdpProcessor.getSession(this.#parser.parseGetSessionParams(command.params));
case 'goog:cdp.resolveRealm':
return this.#cdpProcessor.resolveRealm(this.#parser.parseResolveRealmParams(command.params));
case 'goog:cdp.sendCommand':
return await this.#cdpProcessor.sendCommand(this.#parser.parseSendCommandParams(command.params));
// keep-sorted end
// Emulation module
// keep-sorted start block=yes
case 'emulation.setForcedColorsModeThemeOverride':
this.#parser.parseSetForcedColorsModeThemeOverrideParams(command.params);
throw new UnsupportedOperationException(`Method ${command.method} is not implemented.`);
case 'emulation.setGeolocationOverride':
return await this.#emulationProcessor.setGeolocationOverride(this.#parser.parseSetGeolocationOverrideParams(command.params));
case 'emulation.setLocaleOverride':
return await this.#emulationProcessor.setLocaleOverride(this.#parser.parseSetLocaleOverrideParams(command.params));
case 'emulation.setNetworkConditions':
return await this.#emulationProcessor.setNetworkConditions(this.#parser.parseSetNetworkConditionsParams(command.params));
case 'emulation.setScreenOrientationOverride':
return await this.#emulationProcessor.setScreenOrientationOverride(this.#parser.parseSetScreenOrientationOverrideParams(command.params));
case 'emulation.setScreenSettingsOverride':
return await this.#emulationProcessor.setScreenSettingsOverride(this.#parser.parseSetScreenSettingsOverrideParams(command.params));
case 'emulation.setScriptingEnabled':
return await this.#emulationProcessor.setScriptingEnabled(this.#parser.parseSetScriptingEnabledParams(command.params));
case 'emulation.setScrollbarTypeOverride':
return await this.#emulationProcessor.setScrollbarTypeOverride(this.#parser.parseSetScrollbarTypeOverrideParams(command.params));
case 'emulation.setTimezoneOverride':
return await this.#emulationProcessor.setTimezoneOverride(this.#parser.parseSetTimezoneOverrideParams(command.params));
case 'emulation.setTouchOverride':
return await this.#emulationProcessor.setTouchOverride(this.#parser.parseSetTouchOverrideParams(command.params));
case 'emulation.setUserAgentOverride':
return await this.#emulationProcessor.setUserAgentOverrideParams(this.#parser.parseSetUserAgentOverrideParams(command.params));
case 'userAgentClientHints.setClientHintsOverride':
return await this.#emulationProcessor.setClientHintsOverride(this.#parser.parseSetClientHintsOverrideParams(command.params));
// keep-sorted end
// Input module
// keep-sorted start block=yes
case 'input.performActions':
return await this.#inputProcessor.performActions(this.#parser.parsePerformActionsParams(command.params));
case 'input.releaseActions':
return await this.#inputProcessor.releaseActions(this.#parser.parseReleaseActionsParams(command.params));
case 'input.setFiles':
return await this.#inputProcessor.setFiles(this.#parser.parseSetFilesParams(command.params));
// keep-sorted end
// Network module
// keep-sorted start block=yes
case 'network.addDataCollector':
return await this.#networkProcessor.addDataCollector(this.#parser.parseAddDataCollectorParams(command.params));
case 'network.addIntercept':
return await this.#networkProcessor.addIntercept(this.#parser.parseAddInterceptParams(command.params));
case 'network.continueRequest':
return await this.#networkProcessor.continueRequest(this.#parser.parseContinueRequestParams(command.params));
case 'network.continueResponse':
return await this.#networkProcessor.continueResponse(this.#parser.parseContinueResponseParams(command.params));
case 'network.continueWithAuth':
return await this.#networkProcessor.continueWithAuth(this.#parser.parseContinueWithAuthParams(command.params));
case 'network.disownData':
return this.#networkProcessor.disownData(this.#parser.parseDisownDataParams(command.params));
case 'network.failRequest':
return await this.#networkProcessor.failRequest(this.#parser.parseFailRequestParams(command.params));
case 'network.getData':
return await this.#networkProcessor.getData(this.#parser.parseGetDataParams(command.params));
case 'network.provideResponse':
return await this.#networkProcessor.provideResponse(this.#parser.parseProvideResponseParams(command.params));
case 'network.removeDataCollector':
return await this.#networkProcessor.removeDataCollector(this.#parser.parseRemoveDataCollectorParams(command.params));
case 'network.removeIntercept':
return await this.#networkProcessor.removeIntercept(this.#parser.parseRemoveInterceptParams(command.params));
case 'network.setCacheBehavior':
return await this.#networkProcessor.setCacheBehavior(this.#parser.parseSetCacheBehaviorParams(command.params));
case 'network.setExtraHeaders':
return await this.#networkProcessor.setExtraHeaders(this.#parser.parseSetExtraHeadersParams(command.params));
// keep-sorted end
// Permissions module
// keep-sorted start block=yes
case 'permissions.setPermission':
return await this.#permissionsProcessor.setPermissions(this.#parser.parseSetPermissionsParams(command.params));
// keep-sorted end
// Script module
// keep-sorted start block=yes
case 'script.addPreloadScript':
return await this.#scriptProcessor.addPreloadScript(this.#parser.parseAddPreloadScriptParams(command.params));
case 'script.callFunction':
return await this.#scriptProcessor.callFunction(this.#parser.parseCallFunctionParams(this.#processTargetParams(command.params)));
case 'script.disown':
return await this.#scriptProcessor.disown(this.#parser.parseDisownParams(this.#processTargetParams(command.params)));
case 'script.evaluate':
return await this.#scriptProcessor.evaluate(this.#parser.parseEvaluateParams(this.#processTargetParams(command.params)));
case 'script.getRealms':
return this.#scriptProcessor.getRealms(this.#parser.parseGetRealmsParams(command.params));
case 'script.removePreloadScript':
return await this.#scriptProcessor.removePreloadScript(this.#parser.parseRemovePreloadScriptParams(command.params));
// keep-sorted end
// Session module
// keep-sorted start block=yes
case 'session.end':
throw new UnsupportedOperationException(`Method ${command.method} is not implemented.`);
case 'session.new':
return await this.#sessionProcessor.new(command.params);
case 'session.status':
return this.#sessionProcessor.status();
case 'session.subscribe':
return await this.#sessionProcessor.subscribe(this.#parser.parseSubscribeParams(command.params), command['goog:channel']);
case 'session.unsubscribe':
return await this.#sessionProcessor.unsubscribe(this.#parser.parseUnsubscribeParams(command.params), command['goog:channel']);
// keep-sorted end
// Storage module
// keep-sorted start block=yes
case 'storage.deleteCookies':
return await this.#storageProcessor.deleteCookies(this.#parser.parseDeleteCookiesParams(command.params));
case 'storage.getCookies':
return await this.#storageProcessor.getCookies(this.#parser.parseGetCookiesParams(command.params));
case 'storage.setCookie':
return await this.#storageProcessor.setCookie(this.#parser.parseSetCookieParams(command.params));
// keep-sorted end
// WebExtension module
// keep-sorted start block=yes
case 'webExtension.install':
return await this.#webExtensionProcessor.install(this.#parser.parseInstallParams(command.params));
case 'webExtension.uninstall':
return await this.#webExtensionProcessor.uninstall(this.#parser.parseUninstallParams(command.params));
// keep-sorted end
}
// Intentionally kept outside the switch statement to ensure that
// ESLint @typescript-eslint/switch-exhaustiveness-check triggers if a new
// command is added.
throw new UnknownCommandException(`Unknown command '${command?.method}'.`);
}
// Workaround for as zod.union always take the first schema
// https://github.com/w3c/webdriver-bidi/issues/635
#processTargetParams(params) {
if (typeof params === 'object' &&
params &&
'target' in params &&
typeof params.target === 'object' &&
params.target &&
'context' in params.target) {
delete params.target['realm'];
}
return params;
}
async processCommand(command) {
try {
const result = await this.#processCommand(command);
const response = {
type: 'success',
id: command.id,
result,
};
this.emit("response" /* CommandProcessorEvents.Response */, {
message: OutgoingMessage.createResolved(response, command['goog:channel']),
event: command.method,
});
}
catch (e) {
if (e instanceof Exception) {
this.emit("response" /* CommandProcessorEvents.Response */, {
message: OutgoingMessage.createResolved(e.toErrorResponse(command.id), command['goog:channel']),
event: command.method,
});
}
else {
const error = e;
this.#logger?.(LogType.bidi, error);
// Heuristic required for processing cases when a browsing context is gone
// during the command processing, e.g. like in test
// `test_input_keyDown_closes_browsing_context`.
const errorException = this.#browserCdpClient.isCloseError(e)
? new NoSuchFrameException(`Browsing context is gone`)
: new UnknownErrorException(error.message, error.stack);
this.emit("response" /* CommandProcessorEvents.Response */, {
message: OutgoingMessage.createResolved(errorException.toErrorResponse(command.id), command['goog:channel']),
event: command.method,
});
}
}
}
}
//# sourceMappingURL=CommandProcessor.js.map
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
import type { Session } from '../protocol/generated/webdriver-bidi.js';
export interface MapperOptions {
acceptInsecureCerts?: boolean;
unhandledPromptBehavior?: Session.UserPromptHandler;
'goog:prerenderingDisabled'?: boolean;
'goog:disableNetworkDurableMessages'?: true;
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
export {};
//# sourceMappingURL=MapperOptions.js.map
@@ -0,0 +1 @@
{"version":3,"file":"MapperOptions.js","sourceRoot":"","sources":["../../src/bidiMapper/MapperOptions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG"}
+27
View File
@@ -0,0 +1,27 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { GoogChannel } from '../protocol/chromium-bidi.js';
import type { ChromiumBidi } from '../protocol/protocol.js';
import type { Result } from '../utils/result.js';
export declare class OutgoingMessage {
#private;
private constructor();
static createFromPromise(messagePromise: Promise<Result<ChromiumBidi.Message>>, googChannel: GoogChannel): Promise<Result<OutgoingMessage>>;
static createResolved(message: ChromiumBidi.Message, googChannel?: GoogChannel): Promise<Result<OutgoingMessage>>;
get message(): ChromiumBidi.Message;
get googChannel(): GoogChannel;
}
+48
View File
@@ -0,0 +1,48 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class OutgoingMessage {
#message;
#googChannel;
constructor(message, googChannel = null) {
this.#message = message;
this.#googChannel = googChannel;
}
static createFromPromise(messagePromise, googChannel) {
return messagePromise.then((message) => {
if (message.kind === 'success') {
return {
kind: 'success',
value: new OutgoingMessage(message.value, googChannel),
};
}
return message;
});
}
static createResolved(message, googChannel = null) {
return Promise.resolve({
kind: 'success',
value: new OutgoingMessage(message, googChannel),
});
}
get message() {
return this.#message;
}
get googChannel() {
return this.#googChannel;
}
}
//# sourceMappingURL=OutgoingMessage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"OutgoingMessage.js","sourceRoot":"","sources":["../../src/bidiMapper/OutgoingMessage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,MAAM,OAAO,eAAe;IACjB,QAAQ,CAAuB;IAC/B,YAAY,CAAc;IAEnC,YACE,OAA6B,EAC7B,cAA2B,IAAI;QAE/B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;IAClC,CAAC;IAED,MAAM,CAAC,iBAAiB,CACtB,cAAqD,EACrD,WAAwB;QAExB,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;YACrC,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC/B,OAAO;oBACL,IAAI,EAAE,SAAS;oBACf,KAAK,EAAE,IAAI,eAAe,CAAC,OAAO,CAAC,KAAK,EAAE,WAAW,CAAC;iBACvD,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,cAAc,CACnB,OAA6B,EAC7B,cAA2B,IAAI;QAE/B,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,IAAI,eAAe,CAAC,OAAO,EAAE,WAAW,CAAC;SACjD,CAAC,CAAC;IACL,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;CACF"}
@@ -0,0 +1,37 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Bluetooth, type EmptyResult } from '../../../protocol/protocol.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class BluetoothProcessor {
#private;
constructor(eventManager: EventManager, browsingContextStorage: BrowsingContextStorage);
simulateAdapter(params: Bluetooth.SimulateAdapterParameters): Promise<EmptyResult>;
disableSimulation(params: Bluetooth.DisableSimulationParameters): Promise<EmptyResult>;
simulatePreconnectedPeripheral(params: Bluetooth.SimulatePreconnectedPeripheralParameters): Promise<EmptyResult>;
simulateAdvertisement(params: Bluetooth.SimulateAdvertisementParameters): Promise<EmptyResult>;
simulateCharacteristic(params: Bluetooth.SimulateCharacteristicParameters): Promise<EmptyResult>;
simulateCharacteristicResponse(params: Bluetooth.SimulateCharacteristicResponseParameters): Promise<EmptyResult>;
simulateDescriptor(params: Bluetooth.SimulateDescriptorParameters): Promise<EmptyResult>;
simulateDescriptorResponse(params: Bluetooth.SimulateDescriptorResponseParameters): Promise<EmptyResult>;
simulateGattConnectionResponse(params: Bluetooth.SimulateGattConnectionResponseParameters): Promise<EmptyResult>;
simulateGattDisconnection(params: Bluetooth.SimulateGattDisconnectionParameters): Promise<EmptyResult>;
simulateService(params: Bluetooth.SimulateServiceParameters): Promise<EmptyResult>;
onCdpTargetCreated(cdpTarget: CdpTarget): void;
handleRequestDevicePrompt(params: Bluetooth.HandleRequestDevicePromptParameters): Promise<EmptyResult>;
}
@@ -0,0 +1,407 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, } from '../../../protocol/protocol.js';
/** Represents a base Bluetooth GATT item. */
class BluetoothGattItem {
id;
uuid;
constructor(id, uuid) {
this.id = id;
this.uuid = uuid;
}
}
/** Represents a Bluetooth descriptor. */
class BluetoothDescriptor extends BluetoothGattItem {
characteristic;
constructor(id, uuid, characteristic) {
super(id, uuid);
this.characteristic = characteristic;
}
}
/** Represents a Bluetooth characteristic. */
class BluetoothCharacteristic extends BluetoothGattItem {
descriptors = new Map();
service;
constructor(id, uuid, service) {
super(id, uuid);
this.service = service;
}
}
/** Represents a Bluetooth service. */
class BluetoothService extends BluetoothGattItem {
characteristics = new Map();
device;
constructor(id, uuid, device) {
super(id, uuid);
this.device = device;
}
}
/** Represents a Bluetooth device. */
class BluetoothDevice {
address;
services = new Map();
constructor(address) {
this.address = address;
}
}
export class BluetoothProcessor {
#eventManager;
#browsingContextStorage;
#bluetoothDevices = new Map();
// A map from a characteristic id from CDP to its BluetoothCharacteristic object.
#bluetoothCharacteristics = new Map();
// A map from a descriptor id from CDP to its BluetoothDescriptor object.
#bluetoothDescriptors = new Map();
constructor(eventManager, browsingContextStorage) {
this.#eventManager = eventManager;
this.#browsingContextStorage = browsingContextStorage;
}
#getDevice(address) {
const device = this.#bluetoothDevices.get(address);
if (!device) {
throw new InvalidArgumentException(`Bluetooth device with address ${address} does not exist`);
}
return device;
}
#getService(device, serviceUuid) {
const service = device.services.get(serviceUuid);
if (!service) {
throw new InvalidArgumentException(`Service with UUID ${serviceUuid} on device ${device.address} does not exist`);
}
return service;
}
#getCharacteristic(service, characteristicUuid) {
const characteristic = service.characteristics.get(characteristicUuid);
if (!characteristic) {
throw new InvalidArgumentException(`Characteristic with UUID ${characteristicUuid} does not exist for service ${service.uuid} on device ${service.device.address}`);
}
return characteristic;
}
#getDescriptor(characteristic, descriptorUuid) {
const descriptor = characteristic.descriptors.get(descriptorUuid);
if (!descriptor) {
throw new InvalidArgumentException(`Descriptor with UUID ${descriptorUuid} does not exist for characteristic ${characteristic.uuid} on service ${characteristic.service.uuid} on device ${characteristic.service.device.address}`);
}
return descriptor;
}
async simulateAdapter(params) {
if (params.state === undefined) {
// The bluetooth.simulateAdapter Command
// Step 4.2. If params["state"] does not exist, return error with error code invalid argument.
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
throw new InvalidArgumentException(`Parameter "state" is required for creating a Bluetooth adapter`);
}
const context = this.#browsingContextStorage.getContext(params.context);
// Bluetooth spec requires overriding the existing adapter (step 6). From the CDP
// perspective, we need to disable the emulation first.
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable');
this.#bluetoothDevices.clear();
this.#bluetoothCharacteristics.clear();
this.#bluetoothDescriptors.clear();
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.enable', {
state: params.state,
leSupported: params.leSupported ?? true,
});
return {};
}
async disableSimulation(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable');
this.#bluetoothDevices.clear();
this.#bluetoothCharacteristics.clear();
this.#bluetoothDescriptors.clear();
return {};
}
async simulatePreconnectedPeripheral(params) {
if (this.#bluetoothDevices.has(params.address)) {
throw new InvalidArgumentException(`Bluetooth device with address ${params.address} already exists`);
}
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulatePreconnectedPeripheral', {
address: params.address,
name: params.name,
knownServiceUuids: params.knownServiceUuids,
manufacturerData: params.manufacturerData,
});
this.#bluetoothDevices.set(params.address, new BluetoothDevice(params.address));
return {};
}
async simulateAdvertisement(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateAdvertisement', {
entry: params.scanEntry,
});
return {};
}
async simulateCharacteristic(params) {
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (params.characteristicProperties === undefined) {
throw new InvalidArgumentException(`Parameter "characteristicProperties" is required for adding a Bluetooth characteristic`);
}
if (service.characteristics.has(params.characteristicUuid)) {
throw new InvalidArgumentException(`Characteristic with UUID ${params.characteristicUuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addCharacteristic', {
serviceId: service.id,
characteristicUuid: params.characteristicUuid,
properties: params.characteristicProperties,
});
const characteristic = new BluetoothCharacteristic(response.characteristicId, params.characteristicUuid, service);
service.characteristics.set(params.characteristicUuid, characteristic);
this.#bluetoothCharacteristics.set(characteristic.id, characteristic);
return {};
}
case 'remove': {
if (params.characteristicProperties !== undefined) {
throw new InvalidArgumentException(`Parameter "characteristicProperties" should not be provided for removing a Bluetooth characteristic`);
}
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeCharacteristic', {
characteristicId: characteristic.id,
});
service.characteristics.delete(params.characteristicUuid);
this.#bluetoothCharacteristics.delete(characteristic.id);
return {};
}
default:
throw new InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
async simulateCharacteristicResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateCharacteristicOperationResponse', {
characteristicId: characteristic.id,
type: params.type,
code: params.code,
...(params.data && {
data: btoa(String.fromCharCode(...params.data)),
}),
});
return {};
}
async simulateDescriptor(params) {
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (characteristic.descriptors.has(params.descriptorUuid)) {
throw new InvalidArgumentException(`Descriptor with UUID ${params.descriptorUuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addDescriptor', {
characteristicId: characteristic.id,
descriptorUuid: params.descriptorUuid,
});
const descriptor = new BluetoothDescriptor(response.descriptorId, params.descriptorUuid, characteristic);
characteristic.descriptors.set(params.descriptorUuid, descriptor);
this.#bluetoothDescriptors.set(descriptor.id, descriptor);
return {};
}
case 'remove': {
const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeDescriptor', {
descriptorId: descriptor.id,
});
characteristic.descriptors.delete(params.descriptorUuid);
this.#bluetoothDescriptors.delete(descriptor.id);
return {};
}
default:
throw new InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
async simulateDescriptorResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateDescriptorOperationResponse', {
descriptorId: descriptor.id,
type: params.type,
code: params.code,
...(params.data && {
data: btoa(String.fromCharCode(...params.data)),
}),
});
return {};
}
async simulateGattConnectionResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', {
address: params.address,
type: 'connection',
code: params.code,
});
return {};
}
async simulateGattDisconnection(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTDisconnection', {
address: params.address,
});
return {};
}
async simulateService(params) {
const device = this.#getDevice(params.address);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (device.services.has(params.uuid)) {
throw new InvalidArgumentException(`Service with UUID ${params.uuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addService', {
address: params.address,
serviceUuid: params.uuid,
});
device.services.set(params.uuid, new BluetoothService(response.serviceId, params.uuid, device));
return {};
}
case 'remove': {
const service = this.#getService(device, params.uuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeService', {
serviceId: service.id,
});
device.services.delete(params.uuid);
return {};
}
default:
throw new InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
onCdpTargetCreated(cdpTarget) {
cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => {
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.requestDevicePromptUpdated',
params: {
context: cdpTarget.id,
prompt: event.id,
devices: event.devices,
},
}, cdpTarget.id);
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.gattOperationReceived', async (event) => {
switch (event.type) {
case 'connection':
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.gattConnectionAttempted',
params: {
context: cdpTarget.id,
address: event.address,
},
}, cdpTarget.id);
return;
case 'discovery':
// Chromium Web Bluetooth simulation generates this GATT discovery event when
// a page attempts to get services for a given Bluetooth device for the first time.
// This 'get services' operation is put on hold until a GATT discovery response
// is sent to the simulation.
// Note: Web Bluetooth automation (see https://webbluetoothcg.github.io/web-bluetooth/#automated-testing)
// does not support simulating a GATT discovery response. This is because simulated services, characteristics,
// or descriptors are immediately visible to the simulation, meaning it doesn't have a distinct
// DISCOVERY state. Therefore, this code simulates a successful GATT discovery
// response upon receiving this event.
await cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', {
address: event.address,
type: 'discovery',
code: 0x0,
});
}
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.characteristicOperationReceived', (event) => {
if (!this.#bluetoothCharacteristics.has(event.characteristicId)) {
return;
}
let type;
if (event.type === 'write') {
// write-default-deprecated comes from
// https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattcharacteristic-writevalue,
// which is deprecated so not supported.
if (event.writeType === 'write-default-deprecated') {
return;
}
type = event.writeType;
}
else {
type = event.type;
}
const characteristic = this.#bluetoothCharacteristics.get(event.characteristicId);
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.characteristicEventGenerated',
params: {
context: cdpTarget.id,
address: characteristic.service.device.address,
serviceUuid: characteristic.service.uuid,
characteristicUuid: characteristic.uuid,
type,
...(event.data && {
data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
}),
},
}, cdpTarget.id);
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.descriptorOperationReceived', (event) => {
if (!this.#bluetoothDescriptors.has(event.descriptorId)) {
return;
}
const descriptor = this.#bluetoothDescriptors.get(event.descriptorId);
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.descriptorEventGenerated',
params: {
context: cdpTarget.id,
address: descriptor.characteristic.service.device.address,
serviceUuid: descriptor.characteristic.service.uuid,
characteristicUuid: descriptor.characteristic.uuid,
descriptorUuid: descriptor.uuid,
type: event.type,
...(event.data && {
data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
}),
},
}, cdpTarget.id);
});
}
async handleRequestDevicePrompt(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (params.accept) {
await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.selectPrompt', {
id: params.prompt,
deviceId: params.device,
});
}
else {
await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.cancelPrompt', {
id: params.prompt,
});
}
return {};
}
}
//# sourceMappingURL=BluetoothProcessor.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,37 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Browser, type EmptyResult, type Session } from '../../../protocol/protocol.js';
import type { CdpClient } from '../../BidiMapper.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { ContextConfigStorage } from './ContextConfigStorage.js';
import type { UserContextStorage } from './UserContextStorage.js';
export declare class BrowserProcessor {
#private;
constructor(browserCdpClient: CdpClient, browsingContextStorage: BrowsingContextStorage, configStorage: ContextConfigStorage, userContextStorage: UserContextStorage);
close(): EmptyResult;
createUserContext(params: Record<string, any>): Promise<Browser.CreateUserContextResult>;
removeUserContext(params: Browser.RemoveUserContextParameters): Promise<EmptyResult>;
getUserContexts(): Promise<Browser.GetUserContextsResult>;
setClientWindowState(params: Browser.SetClientWindowStateParameters): Promise<Browser.SetClientWindowStateResult>;
getClientWindows(): Promise<Browser.GetClientWindowsResult>;
setDownloadBehavior(params: Browser.SetDownloadBehaviorParameters): Promise<EmptyResult>;
}
/**
* Proxy config parse implementation:
* https://source.chromium.org/chromium/chromium/src/+/main:net/proxy_resolution/proxy_config.h;drc=743a82d08e59d803c94ee1b8564b8b11dd7b462f;l=107
*/
export declare function getProxyStr(proxyConfig: Session.ProxyConfiguration): string | undefined;
@@ -0,0 +1,289 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, NoSuchUserContextException, UnknownErrorException, UnsupportedOperationException, } from '../../../protocol/protocol.js';
export class BrowserProcessor {
#browserCdpClient;
#browsingContextStorage;
#configStorage;
#userContextStorage;
constructor(browserCdpClient, browsingContextStorage, configStorage, userContextStorage) {
this.#browserCdpClient = browserCdpClient;
this.#browsingContextStorage = browsingContextStorage;
this.#configStorage = configStorage;
this.#userContextStorage = userContextStorage;
}
close() {
// Ensure that it is put at the end of the event loop.
// This way we send back the response before closing the tab.
// Always catch uncaught exceptions.
setTimeout(() => this.#browserCdpClient.sendCommand('Browser.close').catch(() => { }), 0);
return {};
}
async createUserContext(params) {
// `params` is a record to provide legacy `goog:` parameters. Now as the `proxy`
// parameter is specified, we should get rid of `goog:proxyServer` and
// `goog:proxyBypassList` and make the params of type
// `Browser.CreateUserContextParameters`.
const w3cParams = params;
const globalConfig = this.#configStorage.getGlobalConfig();
if (w3cParams.acceptInsecureCerts !== undefined) {
if (w3cParams.acceptInsecureCerts === false &&
globalConfig.acceptInsecureCerts === true)
// TODO: https://github.com/GoogleChromeLabs/chromium-bidi/issues/3398
throw new UnknownErrorException(`Cannot set user context's "acceptInsecureCerts" to false, when a capability "acceptInsecureCerts" is set to true`);
}
const request = {};
if (w3cParams.proxy) {
const proxyStr = getProxyStr(w3cParams.proxy);
if (proxyStr) {
request.proxyServer = proxyStr;
}
if (w3cParams.proxy.noProxy) {
request.proxyBypassList = w3cParams.proxy.noProxy.join(',');
}
}
else {
// TODO: remove after Puppeteer stops using it.
if (params['goog:proxyServer'] !== undefined) {
request.proxyServer = params['goog:proxyServer'];
}
const proxyBypassList = params['goog:proxyBypassList'] ?? undefined;
if (proxyBypassList) {
request.proxyBypassList = proxyBypassList.join(',');
}
}
const context = await this.#browserCdpClient.sendCommand('Target.createBrowserContext', request);
await this.#applyDownloadBehavior(globalConfig.downloadBehavior ?? null, context.browserContextId);
this.#configStorage.updateUserContextConfig(context.browserContextId, {
acceptInsecureCerts: params['acceptInsecureCerts'],
userPromptHandler: params['unhandledPromptBehavior'],
});
return {
userContext: context.browserContextId,
};
}
async removeUserContext(params) {
const userContext = params.userContext;
if (userContext === 'default') {
throw new InvalidArgumentException('`default` user context cannot be removed');
}
try {
await this.#browserCdpClient.sendCommand('Target.disposeBrowserContext', {
browserContextId: userContext,
});
}
catch (err) {
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/target_handler.cc;l=1424;drc=c686e8f4fd379312469fe018f5c390e9c8f20d0d
if (err.message.startsWith('Failed to find context with id')) {
throw new NoSuchUserContextException(err.message);
}
throw err;
}
return {};
}
async getUserContexts() {
return {
userContexts: await this.#userContextStorage.getUserContexts(),
};
}
async #getWindowInfo(targetId) {
const windowInfo = await this.#browserCdpClient.sendCommand('Browser.getWindowForTarget', { targetId });
return {
// `active` is not supported in CDP yet.
active: false,
clientWindow: `${windowInfo.windowId}`,
state: windowInfo.bounds.windowState ?? 'normal',
height: windowInfo.bounds.height ?? 0,
width: windowInfo.bounds.width ?? 0,
x: windowInfo.bounds.left ?? 0,
y: windowInfo.bounds.top ?? 0,
};
}
async setClientWindowState(params) {
const { clientWindow } = params;
const bounds = {
windowState: params.state,
};
if (params.state === 'normal') {
if (params.width !== undefined) {
bounds.width = params.width;
}
if (params.height !== undefined) {
bounds.height = params.height;
}
if (params.x !== undefined) {
bounds.left = params.x;
}
if (params.y !== undefined) {
bounds.top = params.y;
}
}
const windowId = Number.parseInt(clientWindow);
if (isNaN(windowId)) {
throw new InvalidArgumentException('no such client window');
}
await this.#browserCdpClient.sendCommand('Browser.setWindowBounds', {
windowId,
bounds,
});
const result = await this.#browserCdpClient.sendCommand('Browser.getWindowBounds', {
windowId,
});
return {
active: false,
clientWindow: `${windowId}`,
state: result.bounds.windowState ?? 'normal',
height: result.bounds.height ?? 0,
width: result.bounds.width ?? 0,
x: result.bounds.left ?? 0,
y: result.bounds.top ?? 0,
};
}
async getClientWindows() {
const topLevelTargetIds = this.#browsingContextStorage
.getTopLevelContexts()
.map((b) => b.cdpTarget.id);
const clientWindows = await Promise.all(topLevelTargetIds.map(async (targetId) => await this.#getWindowInfo(targetId)));
const uniqueClientWindowIds = new Set();
const uniqueClientWindows = new Array();
// Filter out duplicated client windows.
for (const window of clientWindows) {
if (!uniqueClientWindowIds.has(window.clientWindow)) {
uniqueClientWindowIds.add(window.clientWindow);
uniqueClientWindows.push(window);
}
}
return { clientWindows: uniqueClientWindows };
}
#toCdpDownloadBehavior(downloadBehavior) {
if (downloadBehavior === null)
// CDP "default" behavior.
return {
behavior: 'default',
};
if (downloadBehavior?.type === 'denied')
// Deny all the downloads.
return {
behavior: 'deny',
};
if (downloadBehavior?.type === 'allowed') {
// CDP behavior "allow" means "save downloaded files to the specific download path".
return {
behavior: 'allow',
downloadPath: downloadBehavior.destinationFolder,
};
}
// Unreachable. Handled by params parser.
throw new UnknownErrorException('Unexpected download behavior');
}
async #applyDownloadBehavior(downloadBehavior, userContext) {
await this.#browserCdpClient.sendCommand('Browser.setDownloadBehavior', {
...this.#toCdpDownloadBehavior(downloadBehavior),
browserContextId: userContext === 'default' ? undefined : userContext,
// Required for enabling download events.
eventsEnabled: true,
});
}
async setDownloadBehavior(params) {
let userContexts;
if (params.userContexts === undefined) {
// Global download behavior.
userContexts = (await this.#userContextStorage.getUserContexts()).map((c) => c.userContext);
}
else {
// Download behavior for the specific user contexts.
userContexts = Array.from(await this.#userContextStorage.verifyUserContextIdList(params.userContexts));
}
if (params.userContexts === undefined) {
// Store the global setting to be applied for the future user contexts.
this.#configStorage.updateGlobalConfig({
downloadBehavior: params.downloadBehavior,
});
}
else {
params.userContexts.map((userContext) => this.#configStorage.updateUserContextConfig(userContext, {
downloadBehavior: params.downloadBehavior,
}));
}
await Promise.all(userContexts.map(async (userContext) => {
// Download behavior can be already set per user context, in which case the global
// one should not be applied.
const downloadBehavior = this.#configStorage.getActiveConfig(undefined, userContext)
.downloadBehavior ?? null;
await this.#applyDownloadBehavior(downloadBehavior, userContext);
}));
return {};
}
}
/**
* Proxy config parse implementation:
* https://source.chromium.org/chromium/chromium/src/+/main:net/proxy_resolution/proxy_config.h;drc=743a82d08e59d803c94ee1b8564b8b11dd7b462f;l=107
*/
export function getProxyStr(proxyConfig) {
if (proxyConfig.proxyType === 'direct' ||
proxyConfig.proxyType === 'system') {
// These types imply that Chrome should use its default behavior (e.g., direct
// connection or system-configured proxy). No specific `proxyServer` string is
// needed.
return undefined;
}
if (proxyConfig.proxyType === 'pac') {
throw new UnsupportedOperationException(`PAC proxy configuration is not supported per user context`);
}
if (proxyConfig.proxyType === 'autodetect') {
throw new UnsupportedOperationException(`Autodetect proxy is not supported per user context`);
}
if (proxyConfig.proxyType === 'manual') {
const servers = [];
// HTTP Proxy
if (proxyConfig.httpProxy !== undefined) {
// servers.push(proxyConfig.httpProxy);
servers.push(`http=${proxyConfig.httpProxy}`);
}
// SSL Proxy (uses 'https' scheme)
if (proxyConfig.sslProxy !== undefined) {
// servers.push(proxyConfig.sslProxy);
servers.push(`https=${proxyConfig.sslProxy}`);
}
// SOCKS Proxy
if (proxyConfig.socksProxy !== undefined ||
proxyConfig.socksVersion !== undefined) {
// socksVersion is mandatory and must be a valid integer if socksProxy is
// specified.
if (proxyConfig.socksProxy === undefined) {
throw new InvalidArgumentException(`'socksVersion' cannot be set without 'socksProxy'`);
}
if (proxyConfig.socksVersion === undefined ||
typeof proxyConfig.socksVersion !== 'number' ||
!Number.isInteger(proxyConfig.socksVersion) ||
proxyConfig.socksVersion < 0 ||
proxyConfig.socksVersion > 255) {
throw new InvalidArgumentException(`'socksVersion' must be between 0 and 255`);
}
servers.push(`socks=socks${proxyConfig.socksVersion}://${proxyConfig.socksProxy}`);
}
if (servers.length === 0) {
// If 'manual' proxyType is chosen but no specific proxy servers (http, ssl, socks)
// are provided, it means no proxy server should be configured.
return undefined;
}
return servers.join(';');
}
// Unreachable.
throw new UnknownErrorException(`Unknown proxy type`);
}
//# sourceMappingURL=BrowserProcessor.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Protocol } from 'devtools-protocol';
import type { Browser, BrowsingContext, Emulation, Session, UAClientHints } from '../../../protocol/protocol.js';
/**
* Represents a context configurations. It can be global, per User Context, or per
* Browsing Context. The undefined value means the config will be taken from the upstream
* config. `null` values means the value should be default regardless of the upstream.
*/
export declare class ContextConfig {
acceptInsecureCerts?: boolean;
clientHints?: UAClientHints.UserAgentClientHints.ClientHintsMetadata | null;
devicePixelRatio?: number | null;
disableNetworkDurableMessages?: true;
downloadBehavior?: Browser.DownloadBehavior | null;
emulatedNetworkConditions?: Emulation.NetworkConditions | null;
extraHeaders?: Protocol.Network.Headers;
geolocation?: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null;
locale?: string | null;
maxTouchPoints?: number | null;
prerenderingDisabled?: boolean;
screenArea?: Emulation.ScreenArea | null;
screenOrientation?: Emulation.ScreenOrientation | null;
scriptingEnabled?: false | null;
scrollbarType?: 'classic' | 'overlay' | null;
timezone?: string | null;
userAgent?: string | null;
userPromptHandler?: Session.UserPromptHandler;
viewport?: BrowsingContext.Viewport | null;
/**
* Merges multiple `ContextConfig` objects. The configs are merged in the order they are
* provided. For each property, the value from the last config that defines it will be
* used. The final result will not contain any `undefined` or `null` properties.
* `undefined` values are ignored. `null` values remove the already set value.
*/
static merge(...configs: (ContextConfig | undefined)[]): ContextConfig;
}
@@ -0,0 +1,71 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Represents a context configurations. It can be global, per User Context, or per
* Browsing Context. The undefined value means the config will be taken from the upstream
* config. `null` values means the value should be default regardless of the upstream.
*/
export class ContextConfig {
// keep-sorted start block=yes
acceptInsecureCerts;
clientHints;
devicePixelRatio;
disableNetworkDurableMessages;
downloadBehavior;
emulatedNetworkConditions;
// Extra headers are kept in CDP format.
extraHeaders;
geolocation;
locale;
maxTouchPoints;
prerenderingDisabled;
screenArea;
screenOrientation;
scriptingEnabled;
scrollbarType;
// Timezone is kept in CDP format with GMT prefix for offset values.
timezone;
userAgent;
userPromptHandler;
viewport;
// keep-sorted end
/**
* Merges multiple `ContextConfig` objects. The configs are merged in the order they are
* provided. For each property, the value from the last config that defines it will be
* used. The final result will not contain any `undefined` or `null` properties.
* `undefined` values are ignored. `null` values remove the already set value.
*/
static merge(...configs) {
const result = new ContextConfig();
for (const config of configs) {
if (!config) {
continue;
}
for (const key in config) {
const value = config[key];
if (value === null) {
delete result[key];
}
else if (value !== undefined) {
result[key] = value;
}
}
}
return result;
}
}
//# sourceMappingURL=ContextConfig.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ContextConfig.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/browser/ContextConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH;;;;GAIG;AACH,MAAM,OAAO,aAAa;IACxB,8BAA8B;IAC9B,mBAAmB,CAAW;IAC9B,WAAW,CAAiE;IAC5E,gBAAgB,CAAiB;IACjC,6BAA6B,CAAQ;IACrC,gBAAgB,CAAmC;IACnD,yBAAyB,CAAsC;IAC/D,wCAAwC;IACxC,YAAY,CAA4B;IACxC,WAAW,CAGF;IACT,MAAM,CAAiB;IACvB,cAAc,CAAiB;IAC/B,oBAAoB,CAAW;IAC/B,UAAU,CAA+B;IACzC,iBAAiB,CAAsC;IACvD,gBAAgB,CAAgB;IAChC,aAAa,CAAgC;IAC7C,oEAAoE;IACpE,QAAQ,CAAiB;IACzB,SAAS,CAAiB;IAC1B,iBAAiB,CAA6B;IAC9C,QAAQ,CAAmC;IAC3C,kBAAkB;IAElB;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,GAAG,OAAsC;QACpD,MAAM,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAEnC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,SAAS;YACX,CAAC;YACD,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;gBACzB,MAAM,KAAK,GAAG,MAAM,CAAC,GAA0B,CAAC,CAAC;gBACjD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBACnB,OAAQ,MAAc,CAAC,GAAG,CAAC,CAAC;gBAC9B,CAAC;qBAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBAC9B,MAAc,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
@@ -0,0 +1,42 @@
import { ContextConfig } from './ContextConfig.js';
/**
* Manages context-specific configurations. This class allows setting
* configurations at three levels: global, user context, and browsing context.
*
* When `getActiveConfig` is called, it merges the configurations in a specific
* order of precedence: `global -> user context -> browsing context`. For each
* configuration property, the value from the highest-precedence level that has a
* non-`undefined` value is used.
*
* The `update` methods (`updateGlobalConfig`, `updateUserContextConfig`,
* `updateBrowsingContextConfig`) merge the provided configuration with the
* existing one at the corresponding level. Properties with `undefined` values in
* the provided configuration are ignored, preserving the existing value.
*/
export declare class ContextConfigStorage {
#private;
/**
* Updates the global configuration. Properties with `undefined` values in the
* provided `config` are ignored.
*/
updateGlobalConfig(config: ContextConfig): void;
/**
* Updates the configuration for a specific browsing context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateBrowsingContextConfig(browsingContextId: string, config: ContextConfig): void;
/**
* Updates the configuration for a specific user context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateUserContextConfig(userContext: string, config: ContextConfig): void;
/**
* Returns the current global configuration.
*/
getGlobalConfig(): ContextConfig;
/**
* Calculates the active configuration by merging global, user context, and
* browsing context settings.
*/
getActiveConfig(topLevelBrowsingContextId: string | undefined, userContext: string): ContextConfig;
}
@@ -0,0 +1,92 @@
/*
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ContextConfig } from './ContextConfig.js';
/**
* Manages context-specific configurations. This class allows setting
* configurations at three levels: global, user context, and browsing context.
*
* When `getActiveConfig` is called, it merges the configurations in a specific
* order of precedence: `global -> user context -> browsing context`. For each
* configuration property, the value from the highest-precedence level that has a
* non-`undefined` value is used.
*
* The `update` methods (`updateGlobalConfig`, `updateUserContextConfig`,
* `updateBrowsingContextConfig`) merge the provided configuration with the
* existing one at the corresponding level. Properties with `undefined` values in
* the provided configuration are ignored, preserving the existing value.
*/
export class ContextConfigStorage {
#global = new ContextConfig();
#userContextConfigs = new Map();
#browsingContextConfigs = new Map();
/**
* Updates the global configuration. Properties with `undefined` values in the
* provided `config` are ignored.
*/
updateGlobalConfig(config) {
this.#global = ContextConfig.merge(this.#global, config);
}
/**
* Updates the configuration for a specific browsing context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateBrowsingContextConfig(browsingContextId, config) {
this.#browsingContextConfigs.set(browsingContextId, ContextConfig.merge(this.#browsingContextConfigs.get(browsingContextId), config));
}
/**
* Updates the configuration for a specific user context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateUserContextConfig(userContext, config) {
this.#userContextConfigs.set(userContext, ContextConfig.merge(this.#userContextConfigs.get(userContext), config));
}
/**
* Returns the current global configuration.
*/
getGlobalConfig() {
return this.#global;
}
/**
* Extra headers is a special case. The headers from the different levels have to be
* merged instead of being overridden.
*/
#getExtraHeaders(topLevelBrowsingContextId, userContext) {
const globalHeaders = this.#global.extraHeaders ?? {};
const userContextHeaders = this.#userContextConfigs.get(userContext)?.extraHeaders ?? {};
const browsingContextHeaders = topLevelBrowsingContextId === undefined
? {}
: (this.#browsingContextConfigs.get(topLevelBrowsingContextId)
?.extraHeaders ?? {});
return { ...globalHeaders, ...userContextHeaders, ...browsingContextHeaders };
}
/**
* Calculates the active configuration by merging global, user context, and
* browsing context settings.
*/
getActiveConfig(topLevelBrowsingContextId, userContext) {
let result = ContextConfig.merge(this.#global, this.#userContextConfigs.get(userContext));
if (topLevelBrowsingContextId !== undefined) {
result = ContextConfig.merge(result, this.#browsingContextConfigs.get(topLevelBrowsingContextId));
}
// Extra headers is a special case which have to be treated in a special way.
const extraHeaders = this.#getExtraHeaders(topLevelBrowsingContextId, userContext);
result.extraHeaders =
Object.keys(extraHeaders).length > 0 ? extraHeaders : undefined;
return result;
}
}
//# sourceMappingURL=ContextConfigStorage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ContextConfigStorage.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/browser/ContextConfigStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,oBAAoB;IAC/B,OAAO,GAAG,IAAI,aAAa,EAAE,CAAC;IAC9B,mBAAmB,GAAG,IAAI,GAAG,EAAyB,CAAC;IACvD,uBAAuB,GAAG,IAAI,GAAG,EAAyB,CAAC;IAE3D;;;OAGG;IACH,kBAAkB,CAAC,MAAqB;QACtC,IAAI,CAAC,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC3D,CAAC;IAED;;;OAGG;IACH,2BAA2B,CACzB,iBAAyB,EACzB,MAAqB;QAErB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAC9B,iBAAiB,EACjB,aAAa,CAAC,KAAK,CACjB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,iBAAiB,CAAC,EACnD,MAAM,CACP,CACF,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,uBAAuB,CAAC,WAAmB,EAAE,MAAqB;QAChE,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAC1B,WAAW,EACX,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CACvE,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,eAAe;QACb,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED;;;OAGG;IACH,gBAAgB,CACd,yBAA6C,EAC7C,WAAmB;QAEnB,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QACtD,MAAM,kBAAkB,GACtB,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,YAAY,IAAI,EAAE,CAAC;QAChE,MAAM,sBAAsB,GAC1B,yBAAyB,KAAK,SAAS;YACrC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,yBAAyB,CAAC;gBAC1D,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC;QAE9B,OAAO,EAAC,GAAG,aAAa,EAAE,GAAG,kBAAkB,EAAE,GAAG,sBAAsB,EAAC,CAAC;IAC9E,CAAC;IAED;;;OAGG;IACH,eAAe,CACb,yBAA6C,EAC7C,WAAmB;QAEnB,IAAI,MAAM,GAAG,aAAa,CAAC,KAAK,CAC9B,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,CAC1C,CAAC;QACF,IAAI,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAC5C,MAAM,GAAG,aAAa,CAAC,KAAK,CAC1B,MAAM,EACN,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAC5D,CAAC;QACJ,CAAC;QAED,6EAA6E;QAC7E,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,CACxC,yBAAyB,EACzB,WAAW,CACZ,CAAC;QACF,MAAM,CAAC,YAAY;YACjB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;QAElE,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
@@ -0,0 +1,27 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { type Browser } from '../../../protocol/protocol.js';
export declare class UserContextStorage {
#private;
constructor(browserClient: CdpClient);
getUserContexts(): Promise<[
Browser.UserContextInfo,
...Browser.UserContextInfo[]
]>;
verifyUserContextIdList(userContextIds: Browser.UserContext[]): Promise<Set<string>>;
}
@@ -0,0 +1,52 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NoSuchUserContextException, } from '../../../protocol/protocol.js';
export class UserContextStorage {
#browserClient;
constructor(browserClient) {
this.#browserClient = browserClient;
}
async getUserContexts() {
const result = await this.#browserClient.sendCommand('Target.getBrowserContexts');
return [
{
userContext: 'default',
},
...result.browserContextIds.map((id) => {
return {
userContext: id,
};
}),
];
}
async verifyUserContextIdList(userContextIds) {
const foundContexts = new Set();
if (!userContextIds.length) {
return foundContexts;
}
const userContexts = await this.getUserContexts();
const knownUserContextIds = new Set(userContexts.map((userContext) => userContext.userContext));
for (const userContextId of userContextIds) {
if (!knownUserContextIds.has(userContextId)) {
throw new NoSuchUserContextException(`User context ${userContextId} not found`);
}
foundContexts.add(userContextId);
}
return foundContexts;
}
}
//# sourceMappingURL=UserContextStorage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"UserContextStorage.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/browser/UserContextStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EACL,0BAA0B,GAE3B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,OAAO,kBAAkB;IAC7B,cAAc,CAAY;IAC1B,YAAY,aAAwB;QAClC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,eAAe;QAGnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAClD,2BAA2B,CAC5B,CAAC;QACF,OAAO;YACL;gBACE,WAAW,EAAE,SAAS;aACvB;YACD,GAAG,MAAM,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;gBACrC,OAAO;oBACL,WAAW,EAAE,EAAE;iBAChB,CAAC;YACJ,CAAC,CAAC;SACH,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,cAAqC;QACjE,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuB,CAAC;QACrD,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO,aAAa,CAAC;QACvB,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QAClD,MAAM,mBAAmB,GAAG,IAAI,GAAG,CACjC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,CAC3D,CAAC;QACF,KAAK,MAAM,aAAa,IAAI,cAAc,EAAE,CAAC;YAC3C,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC5C,MAAM,IAAI,0BAA0B,CAClC,gBAAgB,aAAa,YAAY,CAC1C,CAAC;YACJ,CAAC;YACD,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACnC,CAAC;QAED,OAAO,aAAa,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,27 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Cdp } from '../../../protocol/protocol.js';
import type { CdpClient, CdpConnection } from '../../BidiMapper.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
export declare class CdpProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, cdpConnection: CdpConnection, browserCdpClient: CdpClient);
getSession(params: Cdp.GetSessionParameters): Cdp.GetSessionResult;
resolveRealm(params: Cdp.ResolveRealmParameters): Cdp.ResolveRealmResult;
sendCommand(params: Cdp.SendCommandParameters): Promise<Cdp.SendCommandResult>;
}
@@ -0,0 +1,56 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { UnknownErrorException } from '../../../protocol/protocol.js';
export class CdpProcessor {
#browsingContextStorage;
#realmStorage;
#cdpConnection;
#browserCdpClient;
constructor(browsingContextStorage, realmStorage, cdpConnection, browserCdpClient) {
this.#browsingContextStorage = browsingContextStorage;
this.#realmStorage = realmStorage;
this.#cdpConnection = cdpConnection;
this.#browserCdpClient = browserCdpClient;
}
getSession(params) {
const context = params.context;
const sessionId = this.#browsingContextStorage.getContext(context).cdpTarget.cdpSessionId;
if (sessionId === undefined) {
return {};
}
return { session: sessionId };
}
resolveRealm(params) {
const context = params.realm;
const realm = this.#realmStorage.getRealm({ realmId: context });
if (realm === undefined) {
throw new UnknownErrorException(`Could not find realm ${params.realm}`);
}
return { executionContextId: realm.executionContextId };
}
async sendCommand(params) {
const client = params.session
? this.#cdpConnection.getCdpClient(params.session)
: this.#browserCdpClient;
const result = await client.sendCommand(params.method, params.params);
return {
result,
session: params.session,
};
}
}
//# sourceMappingURL=CdpProcessor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"CdpProcessor.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/cdp/CdpProcessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,qBAAqB,EAAW,MAAM,+BAA+B,CAAC;AAK9E,MAAM,OAAO,YAAY;IACd,uBAAuB,CAAyB;IAChD,aAAa,CAAe;IAC5B,cAAc,CAAgB;IAC9B,iBAAiB,CAAY;IAEtC,YACE,sBAA8C,EAC9C,YAA0B,EAC1B,aAA4B,EAC5B,gBAA2B;QAE3B,IAAI,CAAC,uBAAuB,GAAG,sBAAsB,CAAC;QACtD,IAAI,CAAC,aAAa,GAAG,YAAY,CAAC;QAClC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;QACpC,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;IAC5C,CAAC;IAED,UAAU,CAAC,MAAgC;QACzC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC/B,MAAM,SAAS,GACb,IAAI,CAAC,uBAAuB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC;QAC1E,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,EAAC,OAAO,EAAE,SAAS,EAAC,CAAC;IAC9B,CAAC;IAED,YAAY,CAAC,MAAkC;QAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAC,OAAO,EAAE,OAAO,EAAC,CAAC,CAAC;QAC9D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,qBAAqB,CAAC,wBAAwB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,EAAC,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,EAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,WAAW,CACf,MAAiC;QAEjC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO;YAC3B,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC;YAClD,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACtE,OAAO;YACL,MAAM;YACN,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,57 @@
import type { Protocol } from 'devtools-protocol';
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { type Browser, type BrowsingContext, type ChromiumBidi, Emulation, type UAClientHints } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { Result } from '../../../utils/result.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import { type NetworkStorage } from '../network/NetworkStorage.js';
import type { ChannelProxy } from '../script/ChannelProxy.js';
import type { PreloadScriptStorage } from '../script/PreloadScriptStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class CdpTarget {
#private;
readonly userContext: Browser.UserContext;
readonly contextConfigStorage: ContextConfigStorage;
static create(targetId: Protocol.Target.TargetID, cdpClient: CdpClient, browserCdpClient: CdpClient, parentCdpClient: CdpClient, realmStorage: RealmStorage, eventManager: EventManager, preloadScriptStorage: PreloadScriptStorage, browsingContextStorage: BrowsingContextStorage, networkStorage: NetworkStorage, configStorage: ContextConfigStorage, userContext: Browser.UserContext, defaultUserAgent: string, logger?: LoggerFn): CdpTarget;
constructor(targetId: Protocol.Target.TargetID, cdpClient: CdpClient, browserCdpClient: CdpClient, parentCdpClient: CdpClient, eventManager: EventManager, realmStorage: RealmStorage, preloadScriptStorage: PreloadScriptStorage, browsingContextStorage: BrowsingContextStorage, configStorage: ContextConfigStorage, networkStorage: NetworkStorage, userContext: Browser.UserContext, defaultUserAgent: string, logger: LoggerFn | undefined);
/** Returns a deferred that resolves when the target is unblocked. */
get unblocked(): Deferred<Result<void>>;
get id(): Protocol.Target.TargetID;
get cdpClient(): CdpClient;
get parentCdpClient(): CdpClient;
get browserCdpClient(): CdpClient;
/** Needed for CDP escape path. */
get cdpSessionId(): Protocol.Target.SessionID;
/**
* Window id the target belongs to. If not known, returns 0.
*/
get windowId(): number;
toggleFetchIfNeeded(): Promise<void>;
/**
* Toggles CDP "Fetch" domain and enable/disable network cache.
*/
toggleNetworkIfNeeded(): Promise<void>;
toggleSetCacheDisabled(disable?: boolean): Promise<void>;
toggleDeviceAccessIfNeeded(): Promise<void>;
togglePreloadIfNeeded(): Promise<void>;
toggleNetwork(): Promise<void>;
/**
* All the ProxyChannels from all the preload scripts of the given
* BrowsingContext.
*/
getChannels(): ChannelProxy[];
setDeviceMetricsOverride(viewport: BrowsingContext.Viewport | null, devicePixelRatio: number | null, screenOrientation: Emulation.ScreenOrientation | null, screenArea: Emulation.ScreenArea | null, scrollbarType?: 'classic' | 'overlay' | null): Promise<void>;
get topLevelId(): string;
isSubscribedTo(moduleOrEvent: ChromiumBidi.EventNames): boolean;
setGeolocationOverride(geolocation: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null): Promise<void>;
setTouchOverride(maxTouchPoints: number | null): Promise<void>;
setLocaleOverride(locale: string | null): Promise<void>;
setScriptingEnabled(scriptingEnabled: false | null): Promise<void>;
setTimezoneOverride(timezone: string | null): Promise<void>;
setExtraHeaders(headers: Protocol.Network.Headers): Promise<void>;
setUserAgentAndAcceptLanguage(userAgent: string | null | undefined, acceptLanguage: string | null | undefined, clientHints?: UAClientHints.UserAgentClientHints.ClientHintsMetadata | null): Promise<void>;
setEmulatedNetworkConditions(networkConditions: Emulation.NetworkConditions | null): Promise<void>;
}
@@ -0,0 +1,693 @@
import { Bluetooth, Speculation } from '../../../protocol/chromium-bidi.js';
import { UnknownErrorException, UnsupportedOperationException, } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { LogType } from '../../../utils/log.js';
import { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import { LogManager } from '../log/LogManager.js';
import { MAX_TOTAL_COLLECTED_SIZE, } from '../network/NetworkStorage.js';
export class CdpTarget {
#id;
userContext;
#cdpClient;
#browserCdpClient;
#parentCdpClient;
#realmStorage;
#eventManager;
#preloadScriptStorage;
#browsingContextStorage;
#networkStorage;
contextConfigStorage;
#unblocked = new Deferred();
// Default user agent for the target. Required, as emulating client hints without user
// agent is not possible. Cache it to avoid round trips to the browser for every target override.
#defaultUserAgent;
#logger;
/**
* Target's window id. Is filled when the CDP target is created and do not reflect
* moving targets from one window to another. The actual values
* will be set during `#unblock`.
* */
#windowId;
#deviceAccessEnabled = false;
#cacheDisableState = false;
#preloadEnabled = false;
#fetchDomainStages = {
request: false,
response: false,
auth: false,
};
static create(targetId, cdpClient, browserCdpClient, parentCdpClient, realmStorage, eventManager, preloadScriptStorage, browsingContextStorage, networkStorage, configStorage, userContext, defaultUserAgent, logger) {
const cdpTarget = new CdpTarget(targetId, cdpClient, browserCdpClient, parentCdpClient, eventManager, realmStorage, preloadScriptStorage, browsingContextStorage, configStorage, networkStorage, userContext, defaultUserAgent, logger);
LogManager.create(cdpTarget, realmStorage, eventManager, logger);
cdpTarget.#setEventListeners();
// No need to await.
// Deferred will be resolved when the target is unblocked.
void cdpTarget.#unblock();
return cdpTarget;
}
constructor(targetId, cdpClient, browserCdpClient, parentCdpClient, eventManager, realmStorage, preloadScriptStorage, browsingContextStorage, configStorage, networkStorage, userContext, defaultUserAgent, logger) {
this.#defaultUserAgent = defaultUserAgent;
this.userContext = userContext;
this.#id = targetId;
this.#cdpClient = cdpClient;
this.#browserCdpClient = browserCdpClient;
this.#parentCdpClient = parentCdpClient;
this.#eventManager = eventManager;
this.#realmStorage = realmStorage;
this.#preloadScriptStorage = preloadScriptStorage;
this.#networkStorage = networkStorage;
this.#browsingContextStorage = browsingContextStorage;
this.contextConfigStorage = configStorage;
this.#logger = logger;
}
/** Returns a deferred that resolves when the target is unblocked. */
get unblocked() {
return this.#unblocked;
}
get id() {
return this.#id;
}
get cdpClient() {
return this.#cdpClient;
}
get parentCdpClient() {
return this.#parentCdpClient;
}
get browserCdpClient() {
return this.#browserCdpClient;
}
/** Needed for CDP escape path. */
get cdpSessionId() {
// SAFETY we got the client by it's id for creating
return this.#cdpClient.sessionId;
}
/**
* Window id the target belongs to. If not known, returns 0.
*/
get windowId() {
if (this.#windowId === undefined) {
this.#logger?.(LogType.debugError, 'Getting windowId before it was set, returning 0');
}
return this.#windowId ?? 0;
}
/**
* Enables all the required CDP domains and unblocks the target.
*/
async #unblock() {
const config = this.contextConfigStorage.getActiveConfig(this.topLevelId, this.userContext);
const results = await Promise.allSettled([
this.#cdpClient.sendCommand('Page.enable', {
enableFileChooserOpenedEvent: true,
}),
...(this.#ignoreFileDialog()
? []
: [
this.#cdpClient.sendCommand('Page.setInterceptFileChooserDialog', {
enabled: true,
// The intercepted dialog should be canceled.
cancel: true,
}),
]),
// There can be some existing frames in the target, if reconnecting to an
// existing browser instance, e.g. via Puppeteer. Need to restore the browsing
// contexts for the frames to correctly handle further events, like
// `Runtime.executionContextCreated`.
// It's important to schedule this task together with enabling domains commands to
// prepare the tree before the events (e.g. Runtime.executionContextCreated) start
// coming.
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/2282
this.#cdpClient
.sendCommand('Page.getFrameTree')
.then((frameTree) => this.#restoreFrameTreeState(frameTree.frameTree)),
this.#cdpClient.sendCommand('Runtime.enable'),
this.#cdpClient.sendCommand('Page.setLifecycleEventsEnabled', {
enabled: true,
}),
// Enabling CDP Network domain is required for navigation detection:
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/2856.
this.#cdpClient
.sendCommand('Network.enable', {
// If `googDisableNetworkDurableMessages` flag is set, do not enable durable
// messages.
enableDurableMessages: config.disableNetworkDurableMessages !== true,
maxTotalBufferSize: MAX_TOTAL_COLLECTED_SIZE,
})
.then(() => this.toggleNetworkIfNeeded()),
this.#cdpClient.sendCommand('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
}),
this.#updateWindowId(),
this.#setUserContextConfig(config),
this.#initAndEvaluatePreloadScripts(),
this.#cdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
// Resume tab execution as well if it was paused by the debugger.
this.#parentCdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
this.toggleDeviceAccessIfNeeded(),
this.togglePreloadIfNeeded(),
]);
for (const result of results) {
if (result instanceof Error) {
// Ignore errors during configuring targets, just log them.
this.#logger?.(LogType.debugError, 'Error happened when configuring a new target', result);
}
}
this.#unblocked.resolve({
kind: 'success',
value: undefined,
});
}
#restoreFrameTreeState(frameTree) {
const frame = frameTree.frame;
const maybeContext = this.#browsingContextStorage.findContext(frame.id);
if (maybeContext !== undefined) {
// Restoring parent of already known browsing context. This means the target is
// OOPiF and the BiDi session was connected to already existing browser instance.
if (maybeContext.parentId === null &&
frame.parentId !== null &&
frame.parentId !== undefined) {
maybeContext.parentId = frame.parentId;
}
}
if (maybeContext === undefined && frame.parentId !== undefined) {
// Restore not yet known nested frames. The top-level frame is created when the
// target is attached.
const parentBrowsingContext = this.#browsingContextStorage.getContext(frame.parentId);
BrowsingContextImpl.create(frame.id, frame.parentId, this.userContext, parentBrowsingContext.cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.contextConfigStorage, frame.url, undefined, this.#logger);
}
frameTree.childFrames?.map((frameTree) => this.#restoreFrameTreeState(frameTree));
}
async toggleFetchIfNeeded() {
const stages = this.#networkStorage.getInterceptionStages(this.topLevelId);
if (this.#fetchDomainStages.request === stages.request &&
this.#fetchDomainStages.response === stages.response &&
this.#fetchDomainStages.auth === stages.auth) {
return;
}
const patterns = [];
this.#fetchDomainStages = stages;
if (stages.request || stages.auth) {
// CDP quirk we need request interception when we intercept auth
patterns.push({
urlPattern: '*',
requestStage: 'Request',
});
}
if (stages.response) {
patterns.push({
urlPattern: '*',
requestStage: 'Response',
});
}
if (patterns.length) {
await this.#cdpClient.sendCommand('Fetch.enable', {
patterns,
handleAuthRequests: stages.auth,
});
}
else {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
void Promise.allSettled(blockedRequest.map((request) => request.waitNextPhase))
.then(async () => {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
if (blockedRequest.length) {
return await this.toggleFetchIfNeeded();
}
return await this.#cdpClient.sendCommand('Fetch.disable');
})
.catch((error) => {
this.#logger?.(LogType.bidi, 'Disable failed', error);
});
}
}
/**
* Toggles CDP "Fetch" domain and enable/disable network cache.
*/
async toggleNetworkIfNeeded() {
// Although the Network domain remains active, Fetch domain activation and caching
// settings should be managed dynamically.
try {
await Promise.all([
this.toggleSetCacheDisabled(),
this.toggleFetchIfNeeded(),
]);
}
catch (err) {
this.#logger?.(LogType.debugError, err);
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async toggleSetCacheDisabled(disable) {
const defaultCacheDisabled = this.#networkStorage.defaultCacheBehavior === 'bypass';
const cacheDisabled = disable ?? defaultCacheDisabled;
if (this.#cacheDisableState === cacheDisabled) {
return;
}
this.#cacheDisableState = cacheDisabled;
try {
await this.#cdpClient.sendCommand('Network.setCacheDisabled', {
cacheDisabled,
});
}
catch (err) {
this.#logger?.(LogType.debugError, err);
this.#cacheDisableState = !cacheDisabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async toggleDeviceAccessIfNeeded() {
const enabled = this.isSubscribedTo(Bluetooth.EventNames.RequestDevicePromptUpdated);
if (this.#deviceAccessEnabled === enabled) {
return;
}
this.#deviceAccessEnabled = enabled;
try {
await this.#cdpClient.sendCommand(enabled ? 'DeviceAccess.enable' : 'DeviceAccess.disable');
}
catch (err) {
this.#logger?.(LogType.debugError, err);
this.#deviceAccessEnabled = !enabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async togglePreloadIfNeeded() {
const enabled = this.isSubscribedTo(Speculation.EventNames.PrefetchStatusUpdated);
if (this.#preloadEnabled === enabled) {
return;
}
this.#preloadEnabled = enabled;
try {
await this.#cdpClient.sendCommand(enabled ? 'Preload.enable' : 'Preload.disable');
}
catch (err) {
this.#logger?.(LogType.debugError, err);
this.#preloadEnabled = !enabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
/**
* Heuristic checking if the error is due to the session being closed. If so, ignore the
* error.
*/
#isExpectedError(err) {
const error = err;
return ((error.code === -32001 &&
error.message === 'Session with given id not found.') ||
this.#cdpClient.isCloseError(err));
}
#setEventListeners() {
this.#cdpClient.on('*', (event, params) => {
// We may encounter uses for EventEmitter other than CDP events,
// which we want to skip.
if (typeof event !== 'string') {
return;
}
this.#eventManager.registerEvent({
type: 'event',
method: `goog:cdp.${event}`,
params: {
event,
params,
session: this.cdpSessionId,
},
}, this.id);
});
}
async #enableFetch(stages) {
const patterns = [];
if (stages.request || stages.auth) {
// CDP quirk we need request interception when we intercept auth
patterns.push({
urlPattern: '*',
requestStage: 'Request',
});
}
if (stages.response) {
patterns.push({
urlPattern: '*',
requestStage: 'Response',
});
}
if (patterns.length) {
const oldStages = this.#fetchDomainStages;
this.#fetchDomainStages = stages;
try {
await this.#cdpClient.sendCommand('Fetch.enable', {
patterns,
handleAuthRequests: stages.auth,
});
}
catch {
this.#fetchDomainStages = oldStages;
}
}
}
async #disableFetch() {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
if (blockedRequest.length === 0) {
this.#fetchDomainStages = {
request: false,
response: false,
auth: false,
};
await this.#cdpClient.sendCommand('Fetch.disable');
}
}
async toggleNetwork() {
// TODO: respect the data collectors once CDP Network domain is enabled on-demand:
// const networkEnable = this.#networkStorage.getCollectorsForBrowsingContext(this.topLevelId).length > 0;
const stages = this.#networkStorage.getInterceptionStages(this.topLevelId);
const fetchEnable = Object.values(stages).some((value) => value);
const fetchChanged = this.#fetchDomainStages.request !== stages.request ||
this.#fetchDomainStages.response !== stages.response ||
this.#fetchDomainStages.auth !== stages.auth;
this.#logger?.(LogType.debugInfo, 'Toggle Network', `Fetch (${fetchEnable}) ${fetchChanged}`);
if (fetchEnable && fetchChanged) {
await this.#enableFetch(stages);
}
if (!fetchEnable && fetchChanged) {
await this.#disableFetch();
}
}
/**
* All the ProxyChannels from all the preload scripts of the given
* BrowsingContext.
*/
getChannels() {
return this.#preloadScriptStorage
.find()
.flatMap((script) => script.channels);
}
async #updateWindowId() {
const { windowId } = await this.#browserCdpClient.sendCommand('Browser.getWindowForTarget', { targetId: this.id });
this.#windowId = windowId;
}
/** Loads all top-level preload scripts. */
async #initAndEvaluatePreloadScripts() {
await Promise.all(this.#preloadScriptStorage
.find({
// Needed for OOPIF
targetId: this.topLevelId,
})
.map((script) => {
return script.initInTarget(this, true);
}));
}
async setDeviceMetricsOverride(viewport, devicePixelRatio, screenOrientation, screenArea, scrollbarType = null) {
if (viewport === null &&
devicePixelRatio === null &&
screenOrientation === null &&
screenArea === null &&
scrollbarType === null) {
await this.cdpClient.sendCommand('Emulation.clearDeviceMetricsOverride');
return;
}
const metricsOverride = {
width: viewport?.width ?? 0,
height: viewport?.height ?? 0,
deviceScaleFactor: devicePixelRatio ?? 0,
screenOrientation: this.#toCdpScreenOrientationAngle(screenOrientation) ?? undefined,
mobile: false,
screenWidth: screenArea?.width,
screenHeight: screenArea?.height,
scrollbarType: scrollbarType === 'overlay' ? 'overlay' : 'default',
};
await this.cdpClient.sendCommand('Emulation.setDeviceMetricsOverride', metricsOverride);
}
/**
* Immediately schedules all the required commands to configure user context
* configuration and waits for them to finish. It's important to schedule them
* in parallel, so that they are enqueued before any page's scripts.
*/
async #setUserContextConfig(config) {
const promises = [];
promises.push(this.#cdpClient
.sendCommand('Page.setPrerenderingAllowed', {
isAllowed: !config.prerenderingDisabled,
})
.catch(() => {
// Ignore CDP errors, as the command is not supported by iframe targets or
// prerendered pages. Generic catch, as the error can vary between CdpClient
// implementations: Tab vs Puppeteer.
}));
if (config.viewport !== undefined ||
config.devicePixelRatio !== undefined ||
config.screenOrientation !== undefined ||
config.screenArea !== undefined) {
promises.push(this.setDeviceMetricsOverride(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null, config.screenArea ?? null, config.scrollbarType ?? null).catch(() => {
// Ignore CDP errors, as the command is not supported by iframe targets. Generic
// catch, as the error can vary between CdpClient implementations: Tab vs
// Puppeteer.
}));
}
if (config.geolocation !== undefined && config.geolocation !== null) {
promises.push(this.setGeolocationOverride(config.geolocation));
}
if (config.locale !== undefined) {
promises.push(this.setLocaleOverride(config.locale));
}
if (config.timezone !== undefined) {
promises.push(this.setTimezoneOverride(config.timezone));
}
if (config.extraHeaders !== undefined) {
promises.push(this.setExtraHeaders(config.extraHeaders));
}
if (config.userAgent !== undefined ||
config.locale !== undefined ||
config.clientHints !== undefined) {
promises.push(this.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints));
}
if (config.scriptingEnabled !== undefined) {
promises.push(this.setScriptingEnabled(config.scriptingEnabled));
}
if (config.acceptInsecureCerts !== undefined) {
promises.push(this.cdpClient.sendCommand('Security.setIgnoreCertificateErrors', {
ignore: config.acceptInsecureCerts,
}));
}
if (config.emulatedNetworkConditions !== undefined) {
promises.push(this.setEmulatedNetworkConditions(config.emulatedNetworkConditions));
}
if (config.maxTouchPoints !== undefined) {
promises.push(this.setTouchOverride(config.maxTouchPoints));
}
await Promise.all(promises);
}
get topLevelId() {
return (this.#browsingContextStorage.findTopLevelContextId(this.id) ?? this.id);
}
isSubscribedTo(moduleOrEvent) {
return this.#eventManager.subscriptionManager.isSubscribedTo(moduleOrEvent, this.topLevelId);
}
#ignoreFileDialog() {
const config = this.contextConfigStorage.getActiveConfig(this.topLevelId, this.userContext);
return ((config.userPromptHandler?.file ??
config.userPromptHandler?.default ??
"ignore" /* Session.UserPromptHandlerType.Ignore */) ===
"ignore" /* Session.UserPromptHandlerType.Ignore */);
}
async setGeolocationOverride(geolocation) {
if (geolocation === null) {
await this.cdpClient.sendCommand('Emulation.clearGeolocationOverride');
}
else if ('type' in geolocation) {
if (geolocation.type !== 'positionUnavailable') {
// Unreachable. Handled by params parser.
throw new UnknownErrorException(`Unknown geolocation error ${geolocation.type}`);
}
// Omitting latitude, longitude or accuracy emulates position unavailable.
await this.cdpClient.sendCommand('Emulation.setGeolocationOverride', {});
}
else if ('latitude' in geolocation) {
await this.cdpClient.sendCommand('Emulation.setGeolocationOverride', {
latitude: geolocation.latitude,
longitude: geolocation.longitude,
accuracy: geolocation.accuracy ?? 1,
// `null` value is treated as "missing".
altitude: geolocation.altitude ?? undefined,
altitudeAccuracy: geolocation.altitudeAccuracy ?? undefined,
heading: geolocation.heading ?? undefined,
speed: geolocation.speed ?? undefined,
});
}
else {
// Unreachable. Handled by params parser.
throw new UnknownErrorException('Unexpected geolocation coordinates value');
}
}
async setTouchOverride(maxTouchPoints) {
const touchEmulationParams = {
enabled: maxTouchPoints !== null,
};
if (maxTouchPoints !== null) {
touchEmulationParams.maxTouchPoints = maxTouchPoints;
}
await this.cdpClient.sendCommand('Emulation.setTouchEmulationEnabled', touchEmulationParams);
}
#toCdpScreenOrientationAngle(orientation) {
if (orientation === null) {
return null;
}
// https://w3c.github.io/screen-orientation/#the-current-screen-orientation-type-and-angle
if (orientation.natural === "portrait" /* Emulation.ScreenOrientationNatural.Portrait */) {
switch (orientation.type) {
case 'portrait-primary':
return {
angle: 0,
type: 'portraitPrimary',
};
case 'landscape-primary':
return {
angle: 90,
type: 'landscapePrimary',
};
case 'portrait-secondary':
return {
angle: 180,
type: 'portraitSecondary',
};
case 'landscape-secondary':
return {
angle: 270,
type: 'landscapeSecondary',
};
default:
// Unreachable.
throw new UnknownErrorException(`Unexpected screen orientation type ${orientation.type}`);
}
}
if (orientation.natural === "landscape" /* Emulation.ScreenOrientationNatural.Landscape */) {
switch (orientation.type) {
case 'landscape-primary':
return {
angle: 0,
type: 'landscapePrimary',
};
case 'portrait-primary':
return {
angle: 90,
type: 'portraitPrimary',
};
case 'landscape-secondary':
return {
angle: 180,
type: 'landscapeSecondary',
};
case 'portrait-secondary':
return {
angle: 270,
type: 'portraitSecondary',
};
default:
// Unreachable.
throw new UnknownErrorException(`Unexpected screen orientation type ${orientation.type}`);
}
}
// Unreachable.
throw new UnknownErrorException(`Unexpected orientation natural ${orientation.natural}`);
}
async setLocaleOverride(locale) {
if (locale === null) {
await this.cdpClient.sendCommand('Emulation.setLocaleOverride', {});
}
else {
await this.cdpClient.sendCommand('Emulation.setLocaleOverride', {
locale,
});
}
}
async setScriptingEnabled(scriptingEnabled) {
await this.cdpClient.sendCommand('Emulation.setScriptExecutionDisabled', {
value: scriptingEnabled === false,
});
}
async setTimezoneOverride(timezone) {
if (timezone === null) {
await this.cdpClient.sendCommand('Emulation.setTimezoneOverride', {
// If empty, disables the override and restores default host system timezone.
timezoneId: '',
});
}
else {
await this.cdpClient.sendCommand('Emulation.setTimezoneOverride', {
timezoneId: timezone,
});
}
}
async setExtraHeaders(headers) {
await this.cdpClient.sendCommand('Network.setExtraHTTPHeaders', {
headers,
});
}
async setUserAgentAndAcceptLanguage(userAgent, acceptLanguage, clientHints) {
const userAgentMetadata = clientHints
? {
brands: clientHints.brands?.map((b) => ({
brand: b.brand,
version: b.version,
})),
fullVersionList: clientHints.fullVersionList,
platform: clientHints.platform ?? '',
platformVersion: clientHints.platformVersion ?? '',
architecture: clientHints.architecture ?? '',
model: clientHints.model ?? '',
mobile: clientHints.mobile ?? false,
bitness: clientHints.bitness ?? undefined,
wow64: clientHints.wow64 ?? undefined,
formFactors: clientHints.formFactors ?? undefined,
}
: undefined;
await this.cdpClient.sendCommand('Emulation.setUserAgentOverride', {
// `userAgent` is required if `userAgentMetadata` is provided.
userAgent: userAgent || (userAgentMetadata ? this.#defaultUserAgent : ''),
acceptLanguage: acceptLanguage ?? undefined,
// We need to provide the platform to enable platform emulation.
// Note that the value might be different from the one expected by the
// legacy `navigator.platform` (e.g. `Win32` vs `Windows`).
// https://github.com/w3c/webdriver-bidi/issues/1065
platform: clientHints?.platform ?? undefined,
userAgentMetadata,
});
}
async setEmulatedNetworkConditions(networkConditions) {
if (networkConditions !== null && networkConditions.type !== 'offline') {
throw new UnsupportedOperationException(`Unsupported network conditions ${networkConditions.type}`);
}
await Promise.all([
this.cdpClient.sendCommand('Network.emulateNetworkConditionsByRule', {
offline: networkConditions?.type === 'offline',
matchedNetworkConditions: [
{
urlPattern: '',
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
},
],
}),
this.cdpClient.sendCommand('Network.overrideNetworkState', {
offline: networkConditions?.type === 'offline',
// TODO: restore the original `latency` value when emulation is removed.
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
}),
]);
}
}
//# sourceMappingURL=CdpTarget.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
import type { CdpClient } from '../../../cdp/CdpClient.js';
import type { CdpConnection } from '../../../cdp/CdpConnection.js';
import type { Browser } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { BluetoothProcessor } from '../bluetooth/BluetoothProcessor.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { NetworkStorage } from '../network/NetworkStorage.js';
import type { PreloadScriptStorage } from '../script/PreloadScriptStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { SpeculationProcessor } from '../speculation/SpeculationProcessor.js';
export declare class CdpTargetManager {
#private;
constructor(cdpConnection: CdpConnection, browserCdpClient: CdpClient, selfTargetId: string, eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, networkStorage: NetworkStorage, configStorage: ContextConfigStorage, bluetoothProcessor: BluetoothProcessor, speculationProcessor: SpeculationProcessor, preloadScriptStorage: PreloadScriptStorage, defaultUserContextId: Browser.UserContext, defaultUserAgent: string, logger?: LoggerFn);
}
@@ -0,0 +1,248 @@
import { LogType } from '../../../utils/log.js';
import { BrowsingContextImpl, serializeOrigin, } from '../context/BrowsingContextImpl.js';
import { WorkerRealm } from '../script/WorkerRealm.js';
import { CdpTarget } from './CdpTarget.js';
const cdpToBidiTargetTypes = {
service_worker: 'service-worker',
shared_worker: 'shared-worker',
worker: 'dedicated-worker',
};
export class CdpTargetManager {
#browserCdpClient;
#cdpConnection;
#targetKeysToBeIgnoredByAutoAttach = new Set();
#selfTargetId;
#eventManager;
#browsingContextStorage;
#networkStorage;
#bluetoothProcessor;
#preloadScriptStorage;
#realmStorage;
#configStorage;
#speculationProcessor;
#defaultUserContextId;
#defaultUserAgent;
#logger;
constructor(cdpConnection, browserCdpClient, selfTargetId, eventManager, browsingContextStorage, realmStorage, networkStorage, configStorage, bluetoothProcessor, speculationProcessor, preloadScriptStorage, defaultUserContextId, defaultUserAgent, logger) {
this.#cdpConnection = cdpConnection;
this.#browserCdpClient = browserCdpClient;
this.#targetKeysToBeIgnoredByAutoAttach.add(selfTargetId);
this.#selfTargetId = selfTargetId;
this.#eventManager = eventManager;
this.#browsingContextStorage = browsingContextStorage;
this.#preloadScriptStorage = preloadScriptStorage;
this.#networkStorage = networkStorage;
this.#configStorage = configStorage;
this.#bluetoothProcessor = bluetoothProcessor;
this.#speculationProcessor = speculationProcessor;
this.#realmStorage = realmStorage;
this.#defaultUserContextId = defaultUserContextId;
this.#defaultUserAgent = defaultUserAgent;
this.#logger = logger;
this.#setEventListeners(browserCdpClient);
}
/**
* This method is called for each CDP session, since this class is responsible
* for creating and destroying all targets and browsing contexts.
*/
#setEventListeners(cdpClient) {
cdpClient.on('Target.attachedToTarget', (params) => {
this.#handleAttachedToTargetEvent(params, cdpClient);
});
cdpClient.on('Target.detachedFromTarget', this.#handleDetachedFromTargetEvent.bind(this));
cdpClient.on('Target.targetInfoChanged', this.#handleTargetInfoChangedEvent.bind(this));
cdpClient.on('Inspector.targetCrashed', () => {
this.#handleTargetCrashedEvent(cdpClient);
});
cdpClient.on('Page.frameAttached', this.#handleFrameAttachedEvent.bind(this));
cdpClient.on('Page.frameSubtreeWillBeDetached', this.#handleFrameSubtreeWillBeDetached.bind(this));
}
#handleFrameAttachedEvent(params) {
const parentBrowsingContext = this.#browsingContextStorage.findContext(params.parentFrameId);
if (parentBrowsingContext !== undefined) {
BrowsingContextImpl.create(params.frameId, params.parentFrameId, parentBrowsingContext.userContext, parentBrowsingContext.cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.#configStorage,
// At this point, we don't know the URL of the frame yet, so it will be updated
// later.
'about:blank', undefined, this.#logger);
}
}
#handleFrameSubtreeWillBeDetached(params) {
this.#browsingContextStorage.findContext(params.frameId)?.dispose(true);
}
#handleAttachedToTargetEvent(params, parentSessionCdpClient) {
const { sessionId, targetInfo } = params;
const targetCdpClient = this.#cdpConnection.getCdpClient(sessionId);
const detach = async () => {
// Detaches and resumes the target suppressing errors.
await targetCdpClient
.sendCommand('Runtime.runIfWaitingForDebugger')
.then(() => parentSessionCdpClient.sendCommand('Target.detachFromTarget', params))
.catch((error) => this.#logger?.(LogType.debugError, error));
};
// Do not attach to the Mapper target.
if (this.#selfTargetId === targetInfo.targetId) {
void detach();
return;
}
// Service workers are special case because they attach to the
// browser target and the page target (so twice per worker) during
// the regular auto-attach and might hang if the CDP session on
// the browser level is not detached. The logic to detach the
// right session is handled in the switch below.
const targetKey = targetInfo.type === 'service_worker'
? `${parentSessionCdpClient.sessionId}_${targetInfo.targetId}`
: targetInfo.targetId;
// Mapper generally only needs one session per target. If we
// receive additional auto-attached sessions, that is very likely
// coming from custom CDP sessions.
if (this.#targetKeysToBeIgnoredByAutoAttach.has(targetKey)) {
// Return to leave the session untouched.
return;
}
this.#targetKeysToBeIgnoredByAutoAttach.add(targetKey);
const userContext = targetInfo.browserContextId &&
targetInfo.browserContextId !== this.#defaultUserContextId
? targetInfo.browserContextId
: 'default';
switch (targetInfo.type) {
case 'tab': {
// Tab targets are required only to handle page targets beneath them.
this.#setEventListeners(targetCdpClient);
// Auto-attach to the page target. No need in resuming tab target debugger, as it
// should preserve the page target debugger state, and will be resumed by the page
// target.
void (async () => {
await targetCdpClient.sendCommand('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
});
})();
return;
}
case 'page':
case 'iframe': {
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
const maybeContext = this.#browsingContextStorage.findContext(targetInfo.targetId);
if (maybeContext && targetInfo.type === 'iframe') {
// OOPiF.
maybeContext.updateCdpTarget(cdpTarget);
}
else {
// If attaching to existing browser instance, there could be OOPiF targets. This
// case is handled by the `findFrameParentId` method.
const parentId = this.#findFrameParentId(targetInfo, parentSessionCdpClient.sessionId);
// New context.
BrowsingContextImpl.create(targetInfo.targetId, parentId, userContext, cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.#configStorage,
// Hack: when a new target created, CDP emits targetInfoChanged with an empty
// url, and navigates it to about:blank later. When the event is emitted for
// an existing target (reconnect), the url is already known, and navigation
// events will not be emitted anymore. Replacing empty url with `about:blank`
// allows to handle both cases in the same way.
// "7.3.2.1 Creating browsing contexts".
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-browsing-contexts
// TODO: check who to deal with non-null creator and its `creatorOrigin`.
targetInfo.url === '' ? 'about:blank' : targetInfo.url, targetInfo.openerFrameId ?? targetInfo.openerId, this.#logger);
}
return;
}
case 'service_worker':
case 'worker': {
const realm = this.#realmStorage.findRealm({
cdpSessionId: parentSessionCdpClient.sessionId,
sandbox: null, // Non-sandboxed realms.
});
// If there is no browsing context, this worker is already terminated.
if (!realm) {
void detach();
return;
}
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget, realm);
return;
}
// In CDP, we only emit shared workers on the browser and not the set of
// frames that use the shared worker. If we change this in the future to
// behave like service workers (emits on both browser and frame targets),
// we can remove this block and merge service workers with the above one.
case 'shared_worker': {
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget);
return;
}
}
// DevTools or some other not supported by BiDi target. Just release
// debugger and ignore them.
void detach();
}
/** Try to find the parent browsing context ID for the given attached target. */
#findFrameParentId(targetInfo, parentSessionId) {
if (targetInfo.type !== 'iframe') {
return null;
}
const parentId = targetInfo.openerFrameId ?? targetInfo.openerId;
if (parentId !== undefined) {
return parentId;
}
if (parentSessionId !== undefined) {
return (this.#browsingContextStorage.findContextBySession(parentSessionId)
?.id ?? null);
}
return null;
}
#createCdpTarget(targetCdpClient, parentCdpClient, targetInfo, userContext) {
this.#setEventListeners(targetCdpClient);
this.#preloadScriptStorage.onCdpTargetCreated(targetInfo.targetId, userContext);
const target = CdpTarget.create(targetInfo.targetId, targetCdpClient, this.#browserCdpClient, parentCdpClient, this.#realmStorage, this.#eventManager, this.#preloadScriptStorage, this.#browsingContextStorage, this.#networkStorage, this.#configStorage, userContext,
// Pass the cached default User Agent to the new target.
this.#defaultUserAgent, this.#logger);
this.#networkStorage.onCdpTargetCreated(target);
this.#bluetoothProcessor.onCdpTargetCreated(target);
this.#speculationProcessor.onCdpTargetCreated(target);
return target;
}
#workers = new Map();
#handleWorkerTarget(realmType, cdpTarget, ownerRealm) {
cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
const { uniqueId, id, origin } = params.context;
const workerRealm = new WorkerRealm(cdpTarget.cdpClient, this.#eventManager, id, this.#logger, serializeOrigin(origin), ownerRealm ? [ownerRealm] : [], uniqueId, this.#realmStorage, realmType);
this.#workers.set(cdpTarget.cdpSessionId, workerRealm);
});
}
#handleDetachedFromTargetEvent({ sessionId, targetId, }) {
if (targetId) {
this.#preloadScriptStorage.find({ targetId }).map((preloadScript) => {
preloadScript.dispose(targetId);
});
}
const context = this.#browsingContextStorage.findContextBySession(sessionId);
if (context) {
context.dispose(true);
return;
}
const worker = this.#workers.get(sessionId);
if (worker) {
this.#realmStorage.deleteRealms({
cdpSessionId: worker.cdpClient.sessionId,
});
}
}
#handleTargetInfoChangedEvent(params) {
const context = this.#browsingContextStorage.findContext(params.targetInfo.targetId);
if (context) {
context.onTargetInfoChanged(params);
}
}
#handleTargetCrashedEvent(cdpClient) {
// This is primarily used for service and shared workers. CDP tends to not
// signal they closed gracefully and instead says they crashed to signal
// they are closed.
const realms = this.#realmStorage.findRealms({
cdpSessionId: cdpClient.sessionId,
});
for (const realm of realms) {
realm.dispose();
}
}
}
//# sourceMappingURL=CdpTargetManager.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,92 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Protocol } from 'devtools-protocol';
import { BrowsingContext, type Emulation, type UAClientHints } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { Realm } from '../script/Realm.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { BrowsingContextStorage } from './BrowsingContextStorage.js';
export declare class BrowsingContextImpl {
#private;
static readonly LOGGER_PREFIX: "debug:browsingContext";
readonly userContext: string;
private constructor();
static create(id: BrowsingContext.BrowsingContext, parentId: BrowsingContext.BrowsingContext | null, userContext: string, cdpTarget: CdpTarget, eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, configStorage: ContextConfigStorage, url: string, originalOpener?: string, logger?: LoggerFn): BrowsingContextImpl;
/**
* @see https://html.spec.whatwg.org/multipage/document-sequences.html#navigable
*/
get navigableId(): string | undefined;
get navigationId(): string;
dispose(emitContextDestroyed: boolean): void;
/** Returns the ID of this context. */
get id(): BrowsingContext.BrowsingContext;
/** Returns the parent context ID. */
get parentId(): BrowsingContext.BrowsingContext | null;
/** Sets the parent context ID and updates parent's children. */
set parentId(parentId: BrowsingContext.BrowsingContext | null);
/** Returns the parent context. */
get parent(): BrowsingContextImpl | null;
/** Returns all direct children contexts. */
get directChildren(): BrowsingContextImpl[];
/** Returns all children contexts, flattened. */
get allChildren(): BrowsingContextImpl[];
/**
* Returns true if this is a top-level context.
* This is the case whenever the parent context ID is null.
*/
isTopLevelContext(): boolean;
get top(): BrowsingContextImpl;
addChild(childId: BrowsingContext.BrowsingContext): void;
get cdpTarget(): CdpTarget;
updateCdpTarget(cdpTarget: CdpTarget): void;
get url(): string;
lifecycleLoaded(): Promise<void>;
targetUnblockedOrThrow(): Promise<void>;
/** Returns a sandbox for internal helper scripts which is not exposed to the user.*/
getOrCreateHiddenSandbox(): Promise<Realm>;
/** Returns a sandbox which is exposed to user. */
getOrCreateUserSandbox(sandbox: string | undefined): Promise<Realm>;
/**
* Implements https://w3c.github.io/webdriver-bidi/#get-the-navigable-info.
*/
serializeToBidiValue(maxDepth?: number | null, addParentField?: boolean): BrowsingContext.Info;
onTargetInfoChanged(params: Protocol.Target.TargetInfoChangedEvent): void;
navigate(url: string, wait: BrowsingContext.ReadinessState): Promise<BrowsingContext.NavigateResult>;
reload(ignoreCache: boolean, wait: BrowsingContext.ReadinessState): Promise<BrowsingContext.NavigateResult>;
setViewport(viewport: BrowsingContext.Viewport | null, devicePixelRatio: number | null, screenOrientation: Emulation.ScreenOrientation | null): Promise<void>;
handleUserPrompt(accept?: boolean, userText?: string): Promise<void>;
activate(): Promise<void>;
captureScreenshot(params: BrowsingContext.CaptureScreenshotParameters): Promise<BrowsingContext.CaptureScreenshotResult>;
print(params: BrowsingContext.PrintParameters): Promise<BrowsingContext.PrintResult>;
close(): Promise<void>;
traverseHistory(delta: number): Promise<void>;
toggleModulesIfNeeded(): Promise<void>;
locateNodes(params: BrowsingContext.LocateNodesParameters): Promise<BrowsingContext.LocateNodesResult>;
setTimezoneOverride(timezone: string | null): Promise<void>;
setLocaleOverride(locale: string | null): Promise<void>;
setGeolocationOverride(geolocation: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null): Promise<void>;
setScriptingEnabled(scriptingEnabled: false | null): Promise<void>;
setUserAgentAndAcceptLanguage(userAgent: string | null | undefined, acceptLanguage: string | null | undefined, clientHints: UAClientHints.UserAgentClientHints.ClientHintsMetadata | null | undefined): Promise<void>;
setEmulatedNetworkConditions(networkConditions: Emulation.NetworkConditions | null): Promise<void>;
setTouchOverride(maxTouchPoints: number | null): Promise<void>;
setExtraHeaders(cdpExtraHeaders: Protocol.Network.Headers): Promise<Promise<any>>;
setScrollbarTypeOverride(scrollbarType: 'classic' | 'overlay' | null): Promise<void>;
}
export declare function serializeOrigin(origin: string): string;
@@ -0,0 +1,1447 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var _a;
import { ChromiumBidi, InvalidArgumentException, InvalidSelectorException, NoSuchElementException, NoSuchFrameException, NoSuchHistoryEntryException, NoSuchNodeException, UnableToCaptureScreenException, UnknownErrorException, UnsupportedOperationException, } from '../../../protocol/protocol.js';
import { assert } from '../../../utils/assert.js';
import { Deferred } from '../../../utils/Deferred.js';
import { LogType } from '../../../utils/log.js';
import { getTimestamp } from '../../../utils/time.js';
import { inchesFromCm } from '../../../utils/unitConversions.js';
import { uuidv4 } from '../../../utils/uuid.js';
import { getSharedId, parseSharedId } from '../script/SharedId.js';
import { WindowRealm } from '../script/WindowRealm.js';
import { NavigationResult, NavigationTracker, } from './NavigationTracker.js';
export class BrowsingContextImpl {
static LOGGER_PREFIX = `${LogType.debug}:browsingContext`;
/** Direct children browsing contexts. */
#children = new Set();
/** The ID of this browsing context. */
#id;
userContext;
// Used for running helper scripts.
#hiddenSandbox = uuidv4();
#downloadIdToUrlMap = new Map();
/**
* The ID of the parent browsing context.
* If null, this is a top-level context.
*/
#loaderId;
#parentId = null;
#originalOpener;
#lifecycle = {
DOMContentLoaded: new Deferred(),
load: new Deferred(),
};
#cdpTarget;
#defaultRealmDeferred = new Deferred();
#browsingContextStorage;
#eventManager;
#logger;
#navigationTracker;
#realmStorage;
#configStorage;
// Set when the user prompt is opened. Required to provide the type in closing event.
#lastUserPromptType;
constructor(id, parentId, userContext, cdpTarget, eventManager, browsingContextStorage, realmStorage, configStorage, url, originalOpener, logger) {
this.#cdpTarget = cdpTarget;
this.#id = id;
this.#parentId = parentId;
this.userContext = userContext;
this.#eventManager = eventManager;
this.#browsingContextStorage = browsingContextStorage;
this.#realmStorage = realmStorage;
this.#configStorage = configStorage;
this.#logger = logger;
this.#originalOpener = originalOpener;
// Register helper realm as hidden, so that it will not be reported to the user.
this.#realmStorage.hiddenSandboxes.add(this.#hiddenSandbox);
this.#navigationTracker = new NavigationTracker(url, id, eventManager, logger);
}
static create(id, parentId, userContext, cdpTarget, eventManager, browsingContextStorage, realmStorage, configStorage, url, originalOpener, logger) {
const context = new _a(id, parentId, userContext, cdpTarget, eventManager, browsingContextStorage, realmStorage, configStorage, url, originalOpener, logger);
context.#initListeners();
browsingContextStorage.addContext(context);
if (!context.isTopLevelContext()) {
context.parent.addChild(context.id);
}
// Hold on the `contextCreated` event until the target is unblocked. This is required,
// as the parent of the context can be set later in case of reconnecting to an
// existing browser instance + OOPiF.
eventManager.registerPromiseEvent(context.targetUnblockedOrThrow().then(() => {
return {
kind: 'success',
value: {
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.ContextCreated,
params: {
...context.serializeToBidiValue(),
// Hack to provide the initial URL of the context, as it can be changed
// between the page target is attached and unblocked, as the page is not
// fully paused in MPArch session (https://crbug.com/372842894).
// TODO: remove once https://crbug.com/372842894 is addressed.
url,
},
},
};
}, (error) => {
return {
kind: 'error',
error,
};
}), context.id, ChromiumBidi.BrowsingContext.EventNames.ContextCreated);
return context;
}
/**
* @see https://html.spec.whatwg.org/multipage/document-sequences.html#navigable
*/
get navigableId() {
return this.#loaderId;
}
get navigationId() {
return this.#navigationTracker.currentNavigationId;
}
dispose(emitContextDestroyed) {
this.#navigationTracker.dispose();
this.#realmStorage.deleteRealms({
browsingContextId: this.id,
});
// Delete context from the parent.
if (!this.isTopLevelContext()) {
this.parent.#children.delete(this.id);
}
// Fail all ongoing navigations.
this.#failLifecycleIfNotFinished();
if (emitContextDestroyed) {
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.ContextDestroyed,
params: this.serializeToBidiValue(null),
}, this.id);
}
// Dispose children after the events are emitted.
this.#deleteAllChildren();
this.#eventManager.clearBufferedEvents(this.id);
this.#browsingContextStorage.deleteContextById(this.id);
}
/** Returns the ID of this context. */
get id() {
return this.#id;
}
/** Returns the parent context ID. */
get parentId() {
return this.#parentId;
}
/** Sets the parent context ID and updates parent's children. */
set parentId(parentId) {
if (this.#parentId !== null) {
this.#logger?.(LogType.debugError, 'Parent context already set');
// Cannot do anything except logging, as throwing will stop event processing. So
// just return,
return;
}
this.#parentId = parentId;
if (!this.isTopLevelContext()) {
this.parent.addChild(this.id);
}
}
/** Returns the parent context. */
get parent() {
if (this.parentId === null) {
return null;
}
return this.#browsingContextStorage.getContext(this.parentId);
}
/** Returns all direct children contexts. */
get directChildren() {
return [...this.#children].map((id) => this.#browsingContextStorage.getContext(id));
}
/** Returns all children contexts, flattened. */
get allChildren() {
const children = this.directChildren;
return children.concat(...children.map((child) => child.allChildren));
}
/**
* Returns true if this is a top-level context.
* This is the case whenever the parent context ID is null.
*/
isTopLevelContext() {
return this.#parentId === null;
}
get top() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let topContext = this;
let parent = topContext.parent;
while (parent) {
topContext = parent;
parent = topContext.parent;
}
return topContext;
}
addChild(childId) {
this.#children.add(childId);
}
#deleteAllChildren(emitContextDestroyed = false) {
this.directChildren.map((child) => child.dispose(emitContextDestroyed));
}
get cdpTarget() {
return this.#cdpTarget;
}
updateCdpTarget(cdpTarget) {
this.#cdpTarget = cdpTarget;
this.#initListeners();
}
get url() {
return this.#navigationTracker.url;
}
async lifecycleLoaded() {
await this.#lifecycle.load;
}
async targetUnblockedOrThrow() {
const result = await this.#cdpTarget.unblocked;
if (result.kind === 'error') {
throw result.error;
}
}
/** Returns a sandbox for internal helper scripts which is not exposed to the user.*/
async getOrCreateHiddenSandbox() {
return await this.#getOrCreateSandboxInternal(this.#hiddenSandbox);
}
/** Returns a sandbox which is exposed to user. */
async getOrCreateUserSandbox(sandbox) {
const realm = await this.#getOrCreateSandboxInternal(sandbox);
if (realm.isHidden()) {
throw new NoSuchFrameException(`Realm "${sandbox}" not found`);
}
return realm;
}
async #getOrCreateSandboxInternal(sandbox) {
if (sandbox === undefined || sandbox === '') {
// Default realm is not guaranteed to be created at this point, so return a deferred.
return await this.#defaultRealmDeferred;
}
let maybeSandboxes = this.#realmStorage.findRealms({
browsingContextId: this.id,
sandbox,
});
if (maybeSandboxes.length === 0) {
await this.#cdpTarget.cdpClient.sendCommand('Page.createIsolatedWorld', {
frameId: this.id,
worldName: sandbox,
});
// `Runtime.executionContextCreated` should be emitted by the time the
// previous command is done.
maybeSandboxes = this.#realmStorage.findRealms({
browsingContextId: this.id,
sandbox,
});
assert(maybeSandboxes.length !== 0);
}
// It's possible for more than one sandbox to be created due to provisional
// frames. In this case, it's always the first one (i.e. the oldest one)
// that is more relevant since the user may have set that one up already
// through evaluation.
return maybeSandboxes[0];
}
/**
* Implements https://w3c.github.io/webdriver-bidi/#get-the-navigable-info.
*/
serializeToBidiValue(maxDepth = 0, addParentField = true) {
return {
context: this.#id,
url: this.url,
userContext: this.userContext,
originalOpener: this.#originalOpener ?? null,
clientWindow: `${this.cdpTarget.windowId}`,
children: maxDepth === null || maxDepth > 0
? this.directChildren.map((c) => c.serializeToBidiValue(maxDepth === null ? maxDepth : maxDepth - 1, false))
: null,
...(addParentField ? { parent: this.#parentId } : {}),
};
}
onTargetInfoChanged(params) {
this.#navigationTracker.onTargetInfoChanged(params.targetInfo.url);
}
#initListeners() {
this.#cdpTarget.cdpClient.on('Network.loadingFailed', (params) => {
// Detect navigation errors like `net::ERR_BLOCKED_BY_RESPONSE`.
// Network related to navigation has request id equals to navigation's loader id.
this.#navigationTracker.networkLoadingFailed(params.requestId, params.errorText);
});
this.#cdpTarget.cdpClient.on('Page.fileChooserOpened', (params) => {
if (this.id !== params.frameId) {
return;
}
if (this.#loaderId === undefined) {
this.#logger?.(LogType.debugError, 'LoaderId should be defined when file upload is shown', params);
return;
}
const element = params.backendNodeId === undefined
? undefined
: {
sharedId: getSharedId(this.id, this.#loaderId, params.backendNodeId),
};
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.Input.EventNames.FileDialogOpened,
params: {
context: this.id,
multiple: params.mode === 'selectMultiple',
element,
},
}, this.id);
});
this.#cdpTarget.cdpClient.on('Page.frameNavigated', (params) => {
if (this.id !== params.frame.id) {
return;
}
this.#navigationTracker.frameNavigated(params.frame.url + (params.frame.urlFragment ?? ''), params.frame.loaderId,
// `unreachableUrl` indicates if the navigation failed.
params.frame.unreachableUrl);
// At the point the page is initialized, all the nested iframes from the
// previous page are detached and realms are destroyed.
// Delete children from context.
this.#deleteAllChildren();
this.#documentChanged(params.frame.loaderId);
});
this.#cdpTarget.cdpClient.on('Page.frameStartedNavigating', (params) => {
if (this.id !== params.frameId) {
return;
}
this.#navigationTracker.frameStartedNavigating(params.url, params.loaderId, params.navigationType);
});
this.#cdpTarget.cdpClient.on('Page.navigatedWithinDocument', (params) => {
if (this.id !== params.frameId) {
return;
}
this.#navigationTracker.navigatedWithinDocument(params.url, params.navigationType);
if (params.navigationType === 'historyApi') {
this.#eventManager.registerEvent({
type: 'event',
method: 'browsingContext.historyUpdated',
params: {
context: this.id,
timestamp: getTimestamp(),
url: this.#navigationTracker.url,
},
}, this.id);
return;
}
});
this.#cdpTarget.cdpClient.on('Page.lifecycleEvent', (params) => {
if (this.id !== params.frameId) {
return;
}
if (params.name === 'init') {
this.#documentChanged(params.loaderId);
return;
}
if (params.name === 'commit') {
this.#loaderId = params.loaderId;
return;
}
// If mapper attached to the page late, it might miss init and
// commit events. In that case, save the first loaderId for this
// frameId.
if (!this.#loaderId) {
this.#loaderId = params.loaderId;
}
// Ignore event from not current navigation.
if (params.loaderId !== this.#loaderId) {
return;
}
switch (params.name) {
case 'DOMContentLoaded':
if (!this.#navigationTracker.isInitialNavigation) {
// Do not emit for the initial navigation.
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.DomContentLoaded,
params: {
context: this.id,
navigation: this.#navigationTracker.currentNavigationId,
timestamp: getTimestamp(),
url: this.#navigationTracker.url,
},
}, this.id);
}
this.#lifecycle.DOMContentLoaded.resolve();
break;
case 'load':
if (!this.#navigationTracker.isInitialNavigation) {
// Do not emit for the initial navigation.
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.Load,
params: {
context: this.id,
navigation: this.#navigationTracker.currentNavigationId,
timestamp: getTimestamp(),
url: this.#navigationTracker.url,
},
}, this.id);
}
// The initial navigation is finished.
this.#navigationTracker.loadPageEvent(params.loaderId);
this.#lifecycle.load.resolve();
break;
}
});
this.#cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
const { auxData, name, uniqueId, id } = params.context;
if (!auxData || auxData.frameId !== this.id) {
return;
}
if (auxData.type === 'isolated' && name === '') {
// This is an internal isolated realm and it is not expected to be exposed to
// WebDriver BiDi users. Ignore it.
return;
}
let origin;
let sandbox;
// Only these execution contexts are supported for now.
switch (auxData.type) {
case 'isolated':
sandbox = name;
// Sandbox should have the same origin as the context itself, but in CDP
// it has an empty one.
if (!this.#defaultRealmDeferred.isFinished) {
this.#logger?.(LogType.debugError, 'Unexpectedly, isolated realm created before the default one');
}
origin = this.#defaultRealmDeferred.isFinished
? this.#defaultRealmDeferred.result.origin
: // This fallback is not expected to be ever reached.
'';
break;
case 'default':
origin = serializeOrigin(params.context.origin);
break;
default:
return;
}
const realm = new WindowRealm(this.id, this.#browsingContextStorage, this.#cdpTarget.cdpClient, this.#eventManager, id, this.#logger, origin, uniqueId, this.#realmStorage, sandbox);
if (auxData.isDefault) {
this.#defaultRealmDeferred.resolve(realm);
// Initialize ChannelProxy listeners for all the channels of all the
// preload scripts related to this BrowsingContext.
// TODO: extend for not default realms by the sandbox name.
void Promise.all(this.#cdpTarget
.getChannels()
.map((channel) => channel.startListenerFromWindow(realm, this.#eventManager)));
}
});
this.#cdpTarget.cdpClient.on('Runtime.executionContextDestroyed', (params) => {
if (this.#defaultRealmDeferred.isFinished &&
this.#defaultRealmDeferred.result.executionContextId ===
params.executionContextId) {
this.#defaultRealmDeferred = new Deferred();
}
this.#realmStorage.deleteRealms({
cdpSessionId: this.#cdpTarget.cdpSessionId,
executionContextId: params.executionContextId,
});
});
this.#cdpTarget.cdpClient.on('Runtime.executionContextsCleared', () => {
if (!this.#defaultRealmDeferred.isFinished) {
this.#defaultRealmDeferred.reject(new UnknownErrorException('execution contexts cleared'));
}
this.#defaultRealmDeferred = new Deferred();
this.#realmStorage.deleteRealms({
cdpSessionId: this.#cdpTarget.cdpSessionId,
});
});
this.#cdpTarget.cdpClient.on('Page.javascriptDialogClosed', (params) => {
// Checking for `params.frameId` for comptaibility with Chrome
// versions that do not have a frameId. TODO: remove once
// https://crrev.com/c/6487891 is in stable.
if (params.frameId && this.id !== params.frameId) {
return;
}
if (!params.frameId &&
this.#parentId &&
this.#cdpTarget.cdpClient !==
this.#browsingContextStorage.getContext(this.#parentId)?.cdpTarget
.cdpClient) {
// If CDP event `Page.javascriptDialogClosed` does not have a frameId, this
// heuristic emits the event only for top-level per-cdp target context, ignoring
// the event for same-process iframes. So the event will be emitted only once per
// CDP target. TODO: remove once https://crrev.com/c/6487891 is in stable.
return;
}
const accepted = params.result;
if (this.#lastUserPromptType === undefined) {
this.#logger?.(LogType.debugError, 'Unexpectedly no opening prompt event before closing one');
}
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.UserPromptClosed,
params: {
context: this.id,
accepted,
// `lastUserPromptType` should never be undefined here, so fallback to
// `UNKNOWN`. The fallback is required to prevent tests from hanging while
// waiting for the closing event. The cast is required, as the `UNKNOWN` value
// is not standard.
type: this.#lastUserPromptType ??
'UNKNOWN',
userText: accepted && params.userInput ? params.userInput : undefined,
},
}, this.id);
this.#lastUserPromptType = undefined;
});
this.#cdpTarget.cdpClient.on('Page.javascriptDialogOpening', (params) => {
// Checking for `params.frameId` for comptaibility with Chrome
// versions that do not have a frameId. TODO: remove once
// https://crrev.com/c/6487891 is in stable.
if (params.frameId && this.id !== params.frameId) {
return;
}
if (!params.frameId &&
this.#parentId &&
this.#cdpTarget.cdpClient !==
this.#browsingContextStorage.getContext(this.#parentId)?.cdpTarget
.cdpClient) {
// If CDP event `Page.javascriptDialogClosed` does not have a frameId, this
// heuristic emits the event only for top-level per-cdp target context, ignoring
// the event for same-process iframes. So the event will be emitted only once per
// CDP target. TODO: remove once https://crrev.com/c/6487891 is in stable.
return;
}
const promptType = _a.#getPromptType(params.type);
// Set the last prompt type to provide it in closing event.
this.#lastUserPromptType = promptType;
const promptHandler = this.#getPromptHandler(promptType);
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.UserPromptOpened,
params: {
context: this.id,
handler: promptHandler,
type: promptType,
message: params.message,
...(params.type === 'prompt'
? { defaultValue: params.defaultPrompt }
: {}),
},
}, this.id);
switch (promptHandler) {
// Based on `unhandledPromptBehavior`, check if the prompt should be handled
// automatically (`accept`, `dismiss`) or wait for the user to do it.
case "accept" /* Session.UserPromptHandlerType.Accept */:
void this.handleUserPrompt(true);
break;
case "dismiss" /* Session.UserPromptHandlerType.Dismiss */:
void this.handleUserPrompt(false);
break;
case "ignore" /* Session.UserPromptHandlerType.Ignore */:
break;
}
});
this.#cdpTarget.browserCdpClient.on('Browser.downloadWillBegin', (params) => {
if (this.id !== params.frameId) {
return;
}
this.#downloadIdToUrlMap.set(params.guid, params.url);
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.DownloadWillBegin,
params: {
context: this.id,
suggestedFilename: params.suggestedFilename,
navigation: params.guid,
timestamp: getTimestamp(),
url: params.url,
},
}, this.id);
});
this.#cdpTarget.browserCdpClient.on('Browser.downloadProgress', (params) => {
if (!this.#downloadIdToUrlMap.has(params.guid)) {
// The event is not related to this browsing context.
return;
}
if (params.state === 'inProgress') {
// No need in reporting progress.
return;
}
const url = this.#downloadIdToUrlMap.get(params.guid);
switch (params.state) {
case 'canceled':
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.DownloadEnd,
params: {
status: 'canceled',
context: this.id,
navigation: params.guid,
timestamp: getTimestamp(),
url,
},
}, this.id);
break;
case 'completed':
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.DownloadEnd,
params: {
filepath: params.filePath ?? null,
status: 'complete',
context: this.id,
navigation: params.guid,
timestamp: getTimestamp(),
url,
},
}, this.id);
break;
default:
// Unreachable.
throw new UnknownErrorException(`Unknown download state: ${params.state}`);
}
});
}
static #getPromptType(cdpType) {
switch (cdpType) {
case 'alert':
return "alert" /* BrowsingContext.UserPromptType.Alert */;
case 'beforeunload':
return "beforeunload" /* BrowsingContext.UserPromptType.Beforeunload */;
case 'confirm':
return "confirm" /* BrowsingContext.UserPromptType.Confirm */;
case 'prompt':
return "prompt" /* BrowsingContext.UserPromptType.Prompt */;
}
}
/**
* Returns either custom UserContext's prompt handler, global or default one.
*/
#getPromptHandler(promptType) {
const defaultPromptHandler = "dismiss" /* Session.UserPromptHandlerType.Dismiss */;
const contextConfig = this.#configStorage.getActiveConfig(this.top.id, this.userContext);
switch (promptType) {
case "alert" /* BrowsingContext.UserPromptType.Alert */:
return (contextConfig.userPromptHandler?.alert ??
contextConfig.userPromptHandler?.default ??
defaultPromptHandler);
case "beforeunload" /* BrowsingContext.UserPromptType.Beforeunload */:
return (contextConfig.userPromptHandler?.beforeUnload ??
contextConfig.userPromptHandler?.default ??
"accept" /* Session.UserPromptHandlerType.Accept */);
case "confirm" /* BrowsingContext.UserPromptType.Confirm */:
return (contextConfig.userPromptHandler?.confirm ??
contextConfig.userPromptHandler?.default ??
defaultPromptHandler);
case "prompt" /* BrowsingContext.UserPromptType.Prompt */:
return (contextConfig.userPromptHandler?.prompt ??
contextConfig.userPromptHandler?.default ??
defaultPromptHandler);
}
}
#documentChanged(loaderId) {
if (loaderId === undefined || this.#loaderId === loaderId) {
return;
}
// Document changed.
this.#resetLifecycleIfFinished();
this.#loaderId = loaderId;
// Delete all child iframes and notify about top level destruction.
this.#deleteAllChildren(true);
}
#resetLifecycleIfFinished() {
if (this.#lifecycle.DOMContentLoaded.isFinished) {
this.#lifecycle.DOMContentLoaded = new Deferred();
}
else {
this.#logger?.(_a.LOGGER_PREFIX, 'Document changed (DOMContentLoaded)');
}
if (this.#lifecycle.load.isFinished) {
this.#lifecycle.load = new Deferred();
}
else {
this.#logger?.(_a.LOGGER_PREFIX, 'Document changed (load)');
}
}
#failLifecycleIfNotFinished() {
if (!this.#lifecycle.DOMContentLoaded.isFinished) {
this.#lifecycle.DOMContentLoaded.reject(new UnknownErrorException('navigation canceled'));
}
if (!this.#lifecycle.load.isFinished) {
this.#lifecycle.load.reject(new UnknownErrorException('navigation canceled'));
}
}
async navigate(url, wait) {
try {
new URL(url);
}
catch {
throw new InvalidArgumentException(`Invalid URL: ${url}`);
}
const navigationState = this.#navigationTracker.createPendingNavigation(url);
// Navigate and wait for the result. If the navigation fails, the error event is
// emitted and the promise is rejected.
const cdpNavigatePromise = (async () => {
const cdpNavigateResult = await this.#cdpTarget.cdpClient.sendCommand('Page.navigate', {
url,
frameId: this.id,
});
if (cdpNavigateResult.errorText) {
// If navigation failed, no pending navigation is left.
this.#navigationTracker.failNavigation(navigationState, cdpNavigateResult.errorText);
throw new UnknownErrorException(cdpNavigateResult.errorText);
}
this.#navigationTracker.navigationCommandFinished(navigationState, cdpNavigateResult.loaderId);
this.#documentChanged(cdpNavigateResult.loaderId);
})();
// Wait for either the navigation is finished or canceled by another navigation.
const result = await Promise.race([
// No `loaderId` means same-document navigation.
this.#waitNavigation(wait, cdpNavigatePromise, navigationState),
// Throw an error if the navigation is canceled.
navigationState.finished,
]);
if (result instanceof NavigationResult) {
if (
// TODO: check after decision on the spec is done:
// https://github.com/w3c/webdriver-bidi/issues/799.
result.eventName === "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */ ||
result.eventName === "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */) {
throw new UnknownErrorException(result.message ?? 'unknown exception');
}
}
return {
navigation: navigationState.navigationId,
// Url can change due to redirects. Get the one from commandNavigation.
url: navigationState.url,
};
}
async #waitNavigation(wait, cdpCommandPromise, navigationState) {
await Promise.all([navigationState.committed, cdpCommandPromise]);
if (wait === "none" /* BrowsingContext.ReadinessState.None */) {
return;
}
if (navigationState.isFragmentNavigation === true) {
// After the cdp command is finished, the `fragmentNavigation` should be already
// settled. If it's the fragment navigation, wait for the `navigationStatus` to be
// finished, which happens after the fragment navigation happened. No need to wait for
// DOM events.
await navigationState.finished;
return;
}
if (wait === "interactive" /* BrowsingContext.ReadinessState.Interactive */) {
await this.#lifecycle.DOMContentLoaded;
return;
}
if (wait === "complete" /* BrowsingContext.ReadinessState.Complete */) {
await this.#lifecycle.load;
return;
}
throw new InvalidArgumentException(`Wait condition ${wait} is not supported`);
}
// TODO: support concurrent navigations analogous to `navigate`.
async reload(ignoreCache, wait) {
await this.targetUnblockedOrThrow();
this.#resetLifecycleIfFinished();
const navigationState = this.#navigationTracker.createPendingNavigation(this.#navigationTracker.url);
const cdpReloadPromise = this.#cdpTarget.cdpClient.sendCommand('Page.reload', {
ignoreCache,
});
// Wait for either the navigation is finished or canceled by another navigation.
const result = await Promise.race([
// No `loaderId` means same-document navigation.
this.#waitNavigation(wait, cdpReloadPromise, navigationState),
// Throw an error if the navigation is canceled.
navigationState.finished,
]);
if (result instanceof NavigationResult) {
if (result.eventName === "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */ ||
result.eventName === "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */) {
throw new UnknownErrorException(result.message ?? 'unknown exception');
}
}
return {
navigation: navigationState.navigationId,
// Url can change due to redirects. Get the one from commandNavigation.
url: navigationState.url,
};
}
async setViewport(viewport, devicePixelRatio, screenOrientation) {
// Set the target's viewport.
const config = this.#configStorage.getActiveConfig(this.id, this.userContext);
await this.cdpTarget.setDeviceMetricsOverride(viewport, devicePixelRatio, screenOrientation, config.screenArea ?? null, config.scrollbarType ?? null);
}
async handleUserPrompt(accept, userText) {
await this.top.#cdpTarget.cdpClient.sendCommand('Page.handleJavaScriptDialog', {
accept: accept ?? true,
promptText: userText,
});
}
async activate() {
await this.#cdpTarget.cdpClient.sendCommand('Page.bringToFront');
}
async captureScreenshot(params) {
if (!this.isTopLevelContext()) {
throw new UnsupportedOperationException(`Non-top-level 'context' (${params.context}) is currently not supported`);
}
const formatParameters = getImageFormatParameters(params);
let captureBeyondViewport = false;
let script;
params.origin ??= 'viewport';
switch (params.origin) {
case 'document': {
script = String(() => {
const element = document.documentElement;
return {
x: 0,
y: 0,
width: element.scrollWidth,
height: element.scrollHeight,
};
});
captureBeyondViewport = true;
break;
}
case 'viewport': {
script = String(() => {
const viewport = window.visualViewport;
return {
x: viewport.pageLeft,
y: viewport.pageTop,
width: viewport.width,
height: viewport.height,
};
});
break;
}
}
const hiddenSandboxRealm = await this.getOrCreateHiddenSandbox();
const originResult = await hiddenSandboxRealm.callFunction(script, false);
assert(originResult.type === 'success');
const origin = deserializeDOMRect(originResult.result);
assert(origin);
let rect = origin;
if (params.clip) {
const clip = params.clip;
if (params.origin === 'viewport' && clip.type === 'box') {
// For viewport origin, the clip is relative to the viewport, while the CDP
// screenshot is relative to the document. So correction for the viewport position
// is required.
clip.x += origin.x;
clip.y += origin.y;
}
rect = getIntersectionRect(await this.#parseRect(clip), origin);
}
if (rect.width === 0 || rect.height === 0) {
throw new UnableToCaptureScreenException(`Unable to capture screenshot with zero dimensions: width=${rect.width}, height=${rect.height}`);
}
return await this.#cdpTarget.cdpClient.sendCommand('Page.captureScreenshot', {
clip: { ...rect, scale: 1.0 },
...formatParameters,
captureBeyondViewport,
});
}
async print(params) {
if (!this.isTopLevelContext()) {
throw new UnsupportedOperationException('Printing of non-top level contexts is not supported');
}
const cdpParams = {};
if (params.background !== undefined) {
cdpParams.printBackground = params.background;
}
if (params.margin?.bottom !== undefined) {
cdpParams.marginBottom = inchesFromCm(params.margin.bottom);
}
if (params.margin?.left !== undefined) {
cdpParams.marginLeft = inchesFromCm(params.margin.left);
}
if (params.margin?.right !== undefined) {
cdpParams.marginRight = inchesFromCm(params.margin.right);
}
if (params.margin?.top !== undefined) {
cdpParams.marginTop = inchesFromCm(params.margin.top);
}
if (params.orientation !== undefined) {
cdpParams.landscape = params.orientation === 'landscape';
}
if (params.page?.height !== undefined) {
cdpParams.paperHeight = inchesFromCm(params.page.height);
}
if (params.page?.width !== undefined) {
cdpParams.paperWidth = inchesFromCm(params.page.width);
}
if (params.pageRanges !== undefined) {
for (const range of params.pageRanges) {
if (typeof range === 'number') {
continue;
}
const rangeParts = range.split('-');
if (rangeParts.length < 1 || rangeParts.length > 2) {
throw new InvalidArgumentException(`Invalid page range: ${range} is not a valid integer range.`);
}
if (rangeParts.length === 1) {
void parseInteger(rangeParts[0] ?? '');
continue;
}
let lowerBound;
let upperBound;
const [rangeLowerPart = '', rangeUpperPart = ''] = rangeParts;
if (rangeLowerPart === '') {
lowerBound = 1;
}
else {
lowerBound = parseInteger(rangeLowerPart);
}
if (rangeUpperPart === '') {
upperBound = Number.MAX_SAFE_INTEGER;
}
else {
upperBound = parseInteger(rangeUpperPart);
}
if (lowerBound > upperBound) {
throw new InvalidArgumentException(`Invalid page range: ${rangeLowerPart} > ${rangeUpperPart}`);
}
}
cdpParams.pageRanges = params.pageRanges.join(',');
}
if (params.scale !== undefined) {
cdpParams.scale = params.scale;
}
if (params.shrinkToFit !== undefined) {
cdpParams.preferCSSPageSize = !params.shrinkToFit;
}
try {
const result = await this.#cdpTarget.cdpClient.sendCommand('Page.printToPDF', cdpParams);
return {
data: result.data,
};
}
catch (error) {
// Effectively zero dimensions.
if (error.message ===
'invalid print parameters: content area is empty') {
throw new UnsupportedOperationException(error.message);
}
throw error;
}
}
/**
* See
* https://w3c.github.io/webdriver-bidi/#:~:text=If%20command%20parameters%20contains%20%22clip%22%3A
*/
async #parseRect(clip) {
switch (clip.type) {
case 'box':
return { x: clip.x, y: clip.y, width: clip.width, height: clip.height };
case 'element': {
const hiddenSandboxRealm = await this.getOrCreateHiddenSandbox();
const result = await hiddenSandboxRealm.callFunction(String((element) => {
return element instanceof Element;
}), false, { type: 'undefined' }, [clip.element]);
if (result.type === 'exception') {
throw new NoSuchElementException(`Element '${clip.element.sharedId}' was not found`);
}
assert(result.result.type === 'boolean');
if (!result.result.value) {
throw new NoSuchElementException(`Node '${clip.element.sharedId}' is not an Element`);
}
{
const result = await hiddenSandboxRealm.callFunction(String((element) => {
const rect = element.getBoundingClientRect();
return {
x: rect.x,
y: rect.y,
height: rect.height,
width: rect.width,
};
}), false, { type: 'undefined' }, [clip.element]);
assert(result.type === 'success');
const rect = deserializeDOMRect(result.result);
if (!rect) {
throw new UnableToCaptureScreenException(`Could not get bounding box for Element '${clip.element.sharedId}'`);
}
return rect;
}
}
}
}
async close() {
await this.#cdpTarget.cdpClient.sendCommand('Page.close');
}
async traverseHistory(delta) {
if (delta === 0) {
return;
}
const history = await this.#cdpTarget.cdpClient.sendCommand('Page.getNavigationHistory');
const entry = history.entries[history.currentIndex + delta];
if (!entry) {
throw new NoSuchHistoryEntryException(`No history entry at delta ${delta}`);
}
await this.#cdpTarget.cdpClient.sendCommand('Page.navigateToHistoryEntry', {
entryId: entry.id,
});
}
async toggleModulesIfNeeded() {
await Promise.all([
this.#cdpTarget.toggleNetworkIfNeeded(),
this.#cdpTarget.toggleDeviceAccessIfNeeded(),
this.#cdpTarget.togglePreloadIfNeeded(),
]);
}
async locateNodes(params) {
// TODO: create a dedicated sandbox instead of `#defaultRealm`.
return await this.#locateNodesByLocator(await this.#defaultRealmDeferred, params.locator, params.startNodes ?? [], params.maxNodeCount, params.serializationOptions);
}
#getLocatorDelegate(locator, maxNodeCount, startNodes) {
switch (locator.type) {
case 'context':
case 'accessibility':
throw new Error('Unreachable');
case 'css':
return {
functionDeclaration: String((cssSelector, maxNodeCount, ...startNodes) => {
const locateNodesUsingCss = (element) => {
if (!(element instanceof HTMLElement ||
element instanceof Document ||
element instanceof DocumentFragment ||
element instanceof SVGElement)) {
throw new Error('startNodes in css selector should be HTMLElement, SVGElement or Document or DocumentFragment');
}
return [...element.querySelectorAll(cssSelector)];
};
startNodes = startNodes.length > 0 ? startNodes : [document];
const returnedNodes = startNodes
.map((startNode) =>
// TODO: stop search early if `maxNodeCount` is reached.
locateNodesUsingCss(startNode))
.flat(1);
return maxNodeCount === 0
? returnedNodes
: returnedNodes.slice(0, maxNodeCount);
}),
argumentsLocalValues: [
// `cssSelector`
{ type: 'string', value: locator.value },
// `maxNodeCount` with `0` means no limit.
{ type: 'number', value: maxNodeCount ?? 0 },
// `startNodes`
...startNodes,
],
};
case 'xpath':
return {
functionDeclaration: String((xPathSelector, maxNodeCount, ...startNodes) => {
// https://w3c.github.io/webdriver-bidi/#locate-nodes-using-xpath
const evaluator = new XPathEvaluator();
const expression = evaluator.createExpression(xPathSelector);
const locateNodesUsingXpath = (element) => {
const xPathResult = expression.evaluate(element, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
const returnedNodes = [];
for (let i = 0; i < xPathResult.snapshotLength; i++) {
returnedNodes.push(xPathResult.snapshotItem(i));
}
return returnedNodes;
};
startNodes = startNodes.length > 0 ? startNodes : [document];
const returnedNodes = startNodes
.map((startNode) =>
// TODO: stop search early if `maxNodeCount` is reached.
locateNodesUsingXpath(startNode))
.flat(1);
return maxNodeCount === 0
? returnedNodes
: returnedNodes.slice(0, maxNodeCount);
}),
argumentsLocalValues: [
// `xPathSelector`
{ type: 'string', value: locator.value },
// `maxNodeCount` with `0` means no limit.
{ type: 'number', value: maxNodeCount ?? 0 },
// `startNodes`
...startNodes,
],
};
case 'innerText':
// https://w3c.github.io/webdriver-bidi/#locate-nodes-using-inner-text
if (locator.value === '') {
throw new InvalidSelectorException('innerText locator cannot be empty');
}
return {
functionDeclaration: String((innerTextSelector, fullMatch, ignoreCase, maxNodeCount, maxDepth, ...startNodes) => {
const searchText = ignoreCase
? innerTextSelector.toUpperCase()
: innerTextSelector;
const locateNodesUsingInnerText = (node, currentMaxDepth) => {
const returnedNodes = [];
if (node instanceof DocumentFragment ||
node instanceof Document) {
const children = [...node.children];
children.forEach((child) =>
// `currentMaxDepth` is not decremented intentionally according to
// https://github.com/w3c/webdriver-bidi/pull/713.
returnedNodes.push(...locateNodesUsingInnerText(child, currentMaxDepth)));
return returnedNodes;
}
if (!(node instanceof HTMLElement)) {
return [];
}
const element = node;
const nodeInnerText = ignoreCase
? element.innerText?.toUpperCase()
: element.innerText;
if (!nodeInnerText.includes(searchText)) {
return [];
}
const childNodes = [];
for (const child of element.children) {
if (child instanceof HTMLElement) {
childNodes.push(child);
}
}
if (childNodes.length === 0) {
if (fullMatch && nodeInnerText === searchText) {
returnedNodes.push(element);
}
else {
if (!fullMatch) {
// Note: `nodeInnerText.includes(searchText)` is already checked
returnedNodes.push(element);
}
}
}
else {
const childNodeMatches =
// Don't search deeper if `maxDepth` is reached.
currentMaxDepth <= 0
? []
: childNodes
.map((child) => locateNodesUsingInnerText(child, currentMaxDepth - 1))
.flat(1);
if (childNodeMatches.length === 0) {
// Note: `nodeInnerText.includes(searchText)` is already checked
if (!fullMatch || nodeInnerText === searchText) {
returnedNodes.push(element);
}
}
else {
returnedNodes.push(...childNodeMatches);
}
}
// TODO: stop search early if `maxNodeCount` is reached.
return returnedNodes;
};
// TODO: stop search early if `maxNodeCount` is reached.
startNodes = startNodes.length > 0 ? startNodes : [document];
const returnedNodes = startNodes
.map((startNode) =>
// TODO: stop search early if `maxNodeCount` is reached.
locateNodesUsingInnerText(startNode, maxDepth))
.flat(1);
return maxNodeCount === 0
? returnedNodes
: returnedNodes.slice(0, maxNodeCount);
}),
argumentsLocalValues: [
// `innerTextSelector`
{ type: 'string', value: locator.value },
// `fullMatch` with default `true`.
{ type: 'boolean', value: locator.matchType !== 'partial' },
// `ignoreCase` with default `false`.
{ type: 'boolean', value: locator.ignoreCase === true },
// `maxNodeCount` with `0` means no limit.
{ type: 'number', value: maxNodeCount ?? 0 },
// `maxDepth` with default `1000` (same as default full serialization depth).
{ type: 'number', value: locator.maxDepth ?? 1000 },
// `startNodes`
...startNodes,
],
};
}
}
async #locateNodesByLocator(realm, locator, startNodes, maxNodeCount, serializationOptions) {
if (locator.type === 'context') {
return await this.#locateNodesByContextLocator(locator, startNodes, realm, serializationOptions);
}
if (locator.type === 'accessibility') {
return await this.#locateNodesByAccessibility(locator, startNodes, maxNodeCount, realm);
}
// Select by injecting a script into the realm.
const locatorDelegate = this.#getLocatorDelegate(locator, maxNodeCount, startNodes);
serializationOptions = {
...serializationOptions,
// The returned object is an array of nodes, so no need in deeper JS serialization.
maxObjectDepth: 1,
};
const locatorResult = await realm.callFunction(locatorDelegate.functionDeclaration, false, { type: 'undefined' }, locatorDelegate.argumentsLocalValues, "none" /* Script.ResultOwnership.None */, serializationOptions);
if (locatorResult.type !== 'success') {
this.#logger?.(_a.LOGGER_PREFIX, 'Failed locateNodesByLocator', locatorResult);
// Heuristic to detect invalid selector for different types of selectors.
if (
// CSS selector.
locatorResult.exceptionDetails.text?.endsWith('is not a valid selector.') ||
// XPath selector.
locatorResult.exceptionDetails.text?.endsWith('is not a valid XPath expression.')) {
throw new InvalidSelectorException(`Not valid selector ${typeof locator.value === 'string' ? locator.value : JSON.stringify(locator.value)}`);
}
// Heuristic to detect if the `startNode` is not an `HTMLElement` in css selector.
if (locatorResult.exceptionDetails.text ===
'Error: startNodes in css selector should be HTMLElement, SVGElement or Document or DocumentFragment') {
throw new InvalidArgumentException('startNodes in css selector should be HTMLElement, SVGElement or Document or DocumentFragment');
}
throw new UnknownErrorException(`Unexpected error in selector script: ${locatorResult.exceptionDetails.text}`);
}
if (locatorResult.result.type !== 'array') {
throw new UnknownErrorException(`Unexpected selector script result type: ${locatorResult.result.type}`);
}
// Check there are no non-node elements in the result.
const nodes = locatorResult.result.value.map((value) => {
if (value.type !== 'node') {
throw new UnknownErrorException(`Unexpected selector script result element: ${value.type}`);
}
return value;
});
return { nodes };
}
async #locateNodesByContextLocator(locator, startNodes, realm, serializationOptions) {
if (startNodes.length !== 0) {
throw new InvalidArgumentException('Start nodes are not supported');
}
const contextId = locator.value.context;
if (!contextId) {
throw new InvalidSelectorException('Invalid context');
}
const context = this.#browsingContextStorage.getContext(contextId);
const parent = context.parent;
if (!parent) {
throw new InvalidArgumentException('This context has no container');
}
try {
const { backendNodeId } = await parent.#cdpTarget.cdpClient.sendCommand('DOM.getFrameOwner', {
frameId: contextId,
});
const { object } = await parent.#cdpTarget.cdpClient.sendCommand('DOM.resolveNode', {
backendNodeId,
});
const locatorResult = await realm.callFunction(`function () { return this; }`, false, { handle: object.objectId }, [], "none" /* Script.ResultOwnership.None */, serializationOptions);
if (locatorResult.type === 'exception') {
throw new Error('Unknown exception');
}
return { nodes: [locatorResult.result] };
}
catch {
throw new InvalidArgumentException('Context does not exist');
}
}
async #locateNodesByAccessibility(locator, startNodes, maxNodeCount, realm) {
if (!locator.value.name && !locator.value.role) {
throw new InvalidSelectorException('Either name or role has to be specified');
}
await this.#cdpTarget.cdpClient.sendCommand('Accessibility.enable');
const startBackendNodeIds = [];
if (startNodes.length === 0) {
const { root: documentRoot } = await this.#cdpTarget.cdpClient.sendCommand('DOM.getDocument');
startBackendNodeIds.push(documentRoot.backendNodeId);
}
else {
for (const node of startNodes) {
if (node.sharedId) {
const parsed = parseSharedId(node.sharedId);
if (!parsed) {
throw new NoSuchNodeException(`Invalid sharedId: ${node.sharedId}`);
}
startBackendNodeIds.push(parsed.backendNodeId);
}
else {
if (node.handle) {
const { nodeId } = await this.#cdpTarget.cdpClient.sendCommand('DOM.requestNode', {
objectId: node.handle,
});
const { node: describedNode } = await this.#cdpTarget.cdpClient.sendCommand('DOM.describeNode', {
nodeId,
});
startBackendNodeIds.push(describedNode.backendNodeId);
}
else {
throw new NoSuchNodeException('Start node must have sharedId or handle');
}
}
}
}
const matchedBackendNodeIds = new Set();
for (const backendNodeId of startBackendNodeIds) {
const { nodes } = await this.#cdpTarget.cdpClient.sendCommand('Accessibility.queryAXTree', {
backendNodeId,
accessibleName: locator.value.name,
role: locator.value.role,
});
for (const node of nodes) {
if (node.backendDOMNodeId && node.role?.type === 'role') {
matchedBackendNodeIds.add(node.backendDOMNodeId);
if (maxNodeCount !== undefined &&
maxNodeCount > 0 &&
matchedBackendNodeIds.size >= maxNodeCount) {
break;
}
}
}
}
const resultNodes = await Promise.all(Array.from(matchedBackendNodeIds).map(async (backendNodeId) => {
const { object } = await this.#cdpTarget.cdpClient.sendCommand('DOM.resolveNode', {
backendNodeId,
});
// We need to use `serializeCdpObject` to convert it to BiDi format.
// We use `Script.ResultOwnership.None` as `locateNodes` returns weak references (nodes).
return await realm.serializeCdpObject(object, "none" /* Script.ResultOwnership.None */);
}));
return {
nodes: resultNodes.filter((result) => result.type === 'node'),
};
}
#getAllRelatedCdpTargets() {
const targets = new Set();
targets.add(this.cdpTarget);
this.allChildren.forEach((c) => targets.add(c.cdpTarget));
return Array.from(targets);
}
async setTimezoneOverride(timezone) {
await Promise.all(this.#getAllRelatedCdpTargets().map(async (cdpTarget) => await cdpTarget.setTimezoneOverride(timezone)));
}
async setLocaleOverride(locale) {
await Promise.all(this.#getAllRelatedCdpTargets().map(async (cdpTarget) => await cdpTarget.setLocaleOverride(locale)));
}
async setGeolocationOverride(geolocation) {
await Promise.all(this.#getAllRelatedCdpTargets().map(async (cdpTarget) => await cdpTarget.setGeolocationOverride(geolocation)));
}
async setScriptingEnabled(scriptingEnabled) {
await Promise.all(this.#getAllRelatedCdpTargets().map(async (cdpTarget) => await cdpTarget.setScriptingEnabled(scriptingEnabled)));
}
async setUserAgentAndAcceptLanguage(userAgent, acceptLanguage, clientHints) {
await Promise.all(this.#getAllRelatedCdpTargets().map(async (cdpTarget) => await cdpTarget.setUserAgentAndAcceptLanguage(userAgent, acceptLanguage, clientHints)));
}
async setEmulatedNetworkConditions(networkConditions) {
await Promise.all(this.#getAllRelatedCdpTargets().map(async (cdpTarget) => await cdpTarget.setEmulatedNetworkConditions(networkConditions)));
}
async setTouchOverride(maxTouchPoints) {
await Promise.allSettled(this.#getAllRelatedCdpTargets().map(async (cdpTarget) => await cdpTarget.setTouchOverride(maxTouchPoints)));
}
async setExtraHeaders(cdpExtraHeaders) {
await Promise.all(this.#getAllRelatedCdpTargets().map(async (cdpTarget) => await cdpTarget.setExtraHeaders(cdpExtraHeaders)));
}
async setScrollbarTypeOverride(scrollbarType) {
const config = this.#configStorage.getActiveConfig(this.id, this.userContext);
await this.cdpTarget.setDeviceMetricsOverride(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null, config.screenArea ?? null, scrollbarType);
}
}
_a = BrowsingContextImpl;
export function serializeOrigin(origin) {
// https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin
if (['://', ''].includes(origin)) {
origin = 'null';
}
return origin;
}
function getImageFormatParameters(params) {
const { quality, type } = params.format ?? {
type: 'image/png',
};
switch (type) {
case 'image/png': {
return { format: 'png' };
}
case 'image/jpeg': {
return {
format: 'jpeg',
...(quality === undefined ? {} : { quality: Math.round(quality * 100) }),
};
}
case 'image/webp': {
return {
format: 'webp',
...(quality === undefined ? {} : { quality: Math.round(quality * 100) }),
};
}
}
throw new InvalidArgumentException(`Image format '${type}' is not a supported format`);
}
function deserializeDOMRect(result) {
if (result.type !== 'object' || result.value === undefined) {
return;
}
const x = result.value.find(([key]) => {
return key === 'x';
})?.[1];
const y = result.value.find(([key]) => {
return key === 'y';
})?.[1];
const height = result.value.find(([key]) => {
return key === 'height';
})?.[1];
const width = result.value.find(([key]) => {
return key === 'width';
})?.[1];
if (x?.type !== 'number' ||
y?.type !== 'number' ||
height?.type !== 'number' ||
width?.type !== 'number') {
return;
}
return {
x: x.value,
y: y.value,
width: width.value,
height: height.value,
};
}
/** @see https://w3c.github.io/webdriver-bidi/#normalize-rect */
function normalizeRect(box) {
return {
...(box.width < 0
? {
x: box.x + box.width,
width: -box.width,
}
: {
x: box.x,
width: box.width,
}),
...(box.height < 0
? {
y: box.y + box.height,
height: -box.height,
}
: {
y: box.y,
height: box.height,
}),
};
}
/** @see https://w3c.github.io/webdriver-bidi/#rectangle-intersection */
function getIntersectionRect(first, second) {
first = normalizeRect(first);
second = normalizeRect(second);
const x = Math.max(first.x, second.x);
const y = Math.max(first.y, second.y);
return {
x,
y,
width: Math.max(Math.min(first.x + first.width, second.x + second.width) - x, 0),
height: Math.max(Math.min(first.y + first.height, second.y + second.height) - y, 0),
};
}
function parseInteger(value) {
value = value.trim();
if (!/^[0-9]+$/.test(value)) {
throw new InvalidArgumentException(`Invalid integer: ${value}`);
}
return parseInt(value);
}
//# sourceMappingURL=BrowsingContextImpl.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { BrowsingContext, type EmptyResult } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { BrowsingContextStorage } from './BrowsingContextStorage.js';
export declare class BrowsingContextProcessor {
#private;
constructor(browserCdpClient: CdpClient, browsingContextStorage: BrowsingContextStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage, eventManager: EventManager);
getTree(params: BrowsingContext.GetTreeParameters): BrowsingContext.GetTreeResult;
create(params: BrowsingContext.CreateParameters): Promise<BrowsingContext.CreateResult>;
navigate(params: BrowsingContext.NavigateParameters): Promise<BrowsingContext.NavigateResult>;
reload(params: BrowsingContext.ReloadParameters): Promise<EmptyResult>;
activate(params: BrowsingContext.ActivateParameters): Promise<EmptyResult>;
captureScreenshot(params: BrowsingContext.CaptureScreenshotParameters): Promise<BrowsingContext.CaptureScreenshotResult>;
print(params: BrowsingContext.PrintParameters): Promise<BrowsingContext.PrintResult>;
setViewport(params: BrowsingContext.SetViewportParameters): Promise<EmptyResult>;
traverseHistory(params: BrowsingContext.TraverseHistoryParameters): Promise<BrowsingContext.TraverseHistoryResult>;
handleUserPrompt(params: BrowsingContext.HandleUserPromptParameters): Promise<EmptyResult>;
close(params: BrowsingContext.CloseParameters): Promise<EmptyResult>;
locateNodes(params: BrowsingContext.LocateNodesParameters): Promise<BrowsingContext.LocateNodesResult>;
}
@@ -0,0 +1,263 @@
import { ChromiumBidi, InvalidArgumentException, NoSuchUserContextException, NoSuchAlertException, UnsupportedOperationException, } from '../../../protocol/protocol.js';
export class BrowsingContextProcessor {
#browserCdpClient;
#browsingContextStorage;
#contextConfigStorage;
#eventManager;
#userContextStorage;
constructor(browserCdpClient, browsingContextStorage, userContextStorage, contextConfigStorage, eventManager) {
this.#contextConfigStorage = contextConfigStorage;
this.#userContextStorage = userContextStorage;
this.#browserCdpClient = browserCdpClient;
this.#browsingContextStorage = browsingContextStorage;
this.#eventManager = eventManager;
this.#eventManager.addSubscribeHook(ChromiumBidi.BrowsingContext.EventNames.ContextCreated, this.#onContextCreatedSubscribeHook.bind(this));
}
getTree(params) {
const resultContexts = params.root === undefined
? this.#browsingContextStorage.getTopLevelContexts()
: [this.#browsingContextStorage.getContext(params.root)];
return {
contexts: resultContexts.map((c) => c.serializeToBidiValue(params.maxDepth ?? Number.MAX_VALUE)),
};
}
async create(params) {
let referenceContext;
let userContext = 'default';
if (params.referenceContext !== undefined) {
referenceContext = this.#browsingContextStorage.getContext(params.referenceContext);
if (!referenceContext.isTopLevelContext()) {
throw new InvalidArgumentException(`referenceContext should be a top-level context`);
}
userContext = referenceContext.userContext;
}
if (params.userContext !== undefined) {
userContext = params.userContext;
}
const existingContexts = this.#browsingContextStorage
.getAllContexts()
.filter((context) => context.userContext === userContext);
let newWindow = false;
switch (params.type) {
case "tab" /* BrowsingContext.CreateType.Tab */:
newWindow = false;
break;
case "window" /* BrowsingContext.CreateType.Window */:
newWindow = true;
break;
}
if (!existingContexts.length) {
// If there are no contexts in the given user context, we need to set
// newWindow to true as newWindow=false will be rejected.
newWindow = true;
}
let result;
try {
result = await this.#browserCdpClient.sendCommand('Target.createTarget', {
url: 'about:blank',
newWindow,
browserContextId: userContext === 'default' ? undefined : userContext,
background: params.background === true,
});
}
catch (err) {
if (
// See https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/devtools/protocol/target_handler.cc;l=90;drc=e80392ac11e48a691f4309964cab83a3a59e01c8
err.message.startsWith('Failed to find browser context with id') ||
// See https://source.chromium.org/chromium/chromium/src/+/main:headless/lib/browser/protocol/target_handler.cc;l=49;drc=e80392ac11e48a691f4309964cab83a3a59e01c8
err.message === 'browserContextId') {
throw new NoSuchUserContextException(`The context ${userContext} was not found`);
}
throw err;
}
// Wait for the new target to be attached and to be added to the browsing context
// storage.
const context = await this.#browsingContextStorage.waitForContext(result.targetId);
// Wait for the new tab to be loaded to avoid race conditions in the
// `browsingContext` events, when the `browsingContext.domContentLoaded` and
// `browsingContext.load` events from the initial `about:blank` navigation
// are emitted after the next navigation is started.
// Details: https://github.com/web-platform-tests/wpt/issues/35846
await context.lifecycleLoaded();
return { context: context.id };
}
navigate(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return context.navigate(params.url, params.wait ?? "none" /* BrowsingContext.ReadinessState.None */);
}
reload(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return context.reload(params.ignoreCache ?? false, params.wait ?? "none" /* BrowsingContext.ReadinessState.None */);
}
async activate(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context.isTopLevelContext()) {
throw new InvalidArgumentException('Activation is only supported on the top-level context');
}
await context.activate();
return {};
}
async captureScreenshot(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return await context.captureScreenshot(params);
}
async print(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return await context.print(params);
}
async setViewport(params) {
// Check the The viewport size limits is not checked by protocol parser, so we need to validate
// it manually:
// https://crsrc.org/c/content/browser/devtools/protocol/emulation_handler.cc;drc=f49e23d8e2bd190b42ec62284b8be10dcccd0446;l=660
const maxDimensionSize = 10_000_000;
if ((params.viewport?.height ?? 0) > maxDimensionSize ||
(params.viewport?.width ?? 0) > maxDimensionSize) {
throw new UnsupportedOperationException(`Viewport dimension over ${maxDimensionSize} are not supported`);
}
const config = {};
// `undefined` means no changes should be done to the config.
if (params.devicePixelRatio !== undefined) {
config.devicePixelRatio = params.devicePixelRatio;
}
if (params.viewport !== undefined) {
config.viewport = params.viewport;
}
const impactedTopLevelContexts = await this.#getRelatedTopLevelBrowsingContexts(params.context, params.userContexts);
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, config);
}
if (params.context !== undefined) {
this.#contextConfigStorage.updateBrowsingContextConfig(params.context, config);
}
await Promise.all(impactedTopLevelContexts.map(async (context) => {
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
/**
* Returns a list of top-level browsing context ids.
*/
async #getRelatedTopLevelBrowsingContexts(browsingContextId, userContextIds) {
if (browsingContextId === undefined && userContextIds === undefined) {
throw new InvalidArgumentException('Either userContexts or context must be provided');
}
if (browsingContextId !== undefined && userContextIds !== undefined) {
throw new InvalidArgumentException('userContexts and context are mutually exclusive');
}
if (browsingContextId !== undefined) {
const context = this.#browsingContextStorage.getContext(browsingContextId);
if (!context.isTopLevelContext()) {
throw new InvalidArgumentException('Emulating viewport is only supported on the top-level context');
}
return [context];
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
const result = [];
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async traverseHistory(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context) {
throw new InvalidArgumentException(`No browsing context with id ${params.context}`);
}
if (!context.isTopLevelContext()) {
throw new InvalidArgumentException('Traversing history is only supported on the top-level context');
}
await context.traverseHistory(params.delta);
return {};
}
async handleUserPrompt(params) {
const context = this.#browsingContextStorage.getContext(params.context);
try {
await context.handleUserPrompt(params.accept, params.userText);
}
catch (error) {
// Heuristically determine the error
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/page_handler.cc;l=1085?q=%22No%20dialog%20is%20showing%22&ss=chromium
if (error.message?.includes('No dialog is showing')) {
throw new NoSuchAlertException('No dialog is showing');
}
throw error;
}
return {};
}
async close(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context.isTopLevelContext()) {
throw new InvalidArgumentException(`Non top-level browsing context ${context.id} cannot be closed.`);
}
// Parent session of a page target session can be a `browser` or a `tab` session.
const parentCdpClient = context.cdpTarget.parentCdpClient;
try {
const detachedFromTargetPromise = new Promise((resolve) => {
const onContextDestroyed = (event) => {
if (event.targetId === params.context) {
parentCdpClient.off('Target.detachedFromTarget', onContextDestroyed);
resolve();
}
};
parentCdpClient.on('Target.detachedFromTarget', onContextDestroyed);
});
try {
if (params.promptUnload) {
await context.close();
}
else {
await parentCdpClient.sendCommand('Target.closeTarget', {
targetId: params.context,
});
}
}
catch (error) {
// Swallow error that arise from the session being destroyed. Rely on the
// `detachedFromTargetPromise` event to be resolved.
if (!parentCdpClient.isCloseError(error)) {
throw error;
}
}
// Sometimes CDP command finishes before `detachedFromTarget` event,
// sometimes after. Wait for the CDP command to be finished, and then wait
// for `detachedFromTarget` if it hasn't emitted.
await detachedFromTargetPromise;
}
catch (error) {
// Swallow error that arise from the page being destroyed
// Example is navigating to faulty SSL certificate
if (!(error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
error.message === 'Not attached to an active page')) {
throw error;
}
}
return {};
}
async locateNodes(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return await context.locateNodes(params);
}
#onContextCreatedSubscribeHook(contextId) {
const context = this.#browsingContextStorage.getContext(contextId);
const contextsToReport = [
context,
...this.#browsingContextStorage.getContext(contextId).allChildren,
];
contextsToReport.forEach((context) => {
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.ContextCreated,
params: context.serializeToBidiValue(),
}, context.id);
});
return Promise.resolve();
}
}
//# sourceMappingURL=BrowsingContextProcessor.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type BrowsingContext } from '../../../protocol/protocol.js';
import type { BrowsingContextImpl } from './BrowsingContextImpl.js';
/** Container class for browsing contexts. */
export declare class BrowsingContextStorage {
#private;
/** Gets all top-level contexts, i.e. those with no parent. */
getTopLevelContexts(): BrowsingContextImpl[];
/** Gets all contexts. */
getAllContexts(): BrowsingContextImpl[];
/** Deletes the context with the given ID. */
deleteContextById(id: BrowsingContext.BrowsingContext): void;
/** Deletes the given context. */
deleteContext(context: BrowsingContextImpl): void;
/** Tracks the given context. */
addContext(context: BrowsingContextImpl): void;
/**
* Waits for a context with the given ID to be added and returns it.
*/
waitForContext(browsingContextId: BrowsingContext.BrowsingContext): Promise<BrowsingContextImpl>;
/** Returns true whether there is an existing context with the given ID. */
hasContext(id: BrowsingContext.BrowsingContext): boolean;
/** Gets the context with the given ID, if any. */
findContext(id: BrowsingContext.BrowsingContext): BrowsingContextImpl | undefined;
/** Returns the top-level context ID of the given context, if any. */
findTopLevelContextId(id: BrowsingContext.BrowsingContext | null): BrowsingContext.BrowsingContext | null;
findContextBySession(sessionId: string): BrowsingContextImpl | undefined;
/** Gets the context with the given ID, if any, otherwise throws. */
getContext(id: BrowsingContext.BrowsingContext): BrowsingContextImpl;
verifyTopLevelContextsList(contexts: BrowsingContext.BrowsingContext[] | undefined): Set<BrowsingContextImpl>;
verifyContextsList(contexts: BrowsingContext.BrowsingContext[]): void;
}
@@ -0,0 +1,130 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NoSuchFrameException, InvalidArgumentException, } from '../../../protocol/protocol.js';
import { EventEmitter } from '../../../utils/EventEmitter.js';
/** Container class for browsing contexts. */
export class BrowsingContextStorage {
/** Map from context ID to context implementation. */
#contexts = new Map();
/** Event emitter for browsing context storage eventsis not expected to be exposed to
* the outside world. */
#eventEmitter = new EventEmitter();
/** Gets all top-level contexts, i.e. those with no parent. */
getTopLevelContexts() {
return this.getAllContexts().filter((context) => context.isTopLevelContext());
}
/** Gets all contexts. */
getAllContexts() {
return Array.from(this.#contexts.values());
}
/** Deletes the context with the given ID. */
deleteContextById(id) {
this.#contexts.delete(id);
}
/** Deletes the given context. */
deleteContext(context) {
this.#contexts.delete(context.id);
}
/** Tracks the given context. */
addContext(context) {
this.#contexts.set(context.id, context);
this.#eventEmitter.emit("added" /* BrowsingContextStorageEvents.Added */, {
browsingContext: context,
});
}
/**
* Waits for a context with the given ID to be added and returns it.
*/
waitForContext(browsingContextId) {
if (this.#contexts.has(browsingContextId)) {
return Promise.resolve(this.getContext(browsingContextId));
}
return new Promise((resolve) => {
const listener = (event) => {
if (event.browsingContext.id === browsingContextId) {
this.#eventEmitter.off("added" /* BrowsingContextStorageEvents.Added */, listener);
resolve(event.browsingContext);
}
};
this.#eventEmitter.on("added" /* BrowsingContextStorageEvents.Added */, listener);
});
}
/** Returns true whether there is an existing context with the given ID. */
hasContext(id) {
return this.#contexts.has(id);
}
/** Gets the context with the given ID, if any. */
findContext(id) {
return this.#contexts.get(id);
}
/** Returns the top-level context ID of the given context, if any. */
findTopLevelContextId(id) {
if (id === null) {
return null;
}
const maybeContext = this.findContext(id);
if (!maybeContext) {
return null;
}
const parentId = maybeContext.parentId ?? null;
if (parentId === null) {
return id;
}
return this.findTopLevelContextId(parentId);
}
findContextBySession(sessionId) {
for (const context of this.#contexts.values()) {
if (context.cdpTarget.cdpSessionId === sessionId) {
return context;
}
}
return;
}
/** Gets the context with the given ID, if any, otherwise throws. */
getContext(id) {
const result = this.findContext(id);
if (result === undefined) {
throw new NoSuchFrameException(`Context ${id} not found`);
}
return result;
}
verifyTopLevelContextsList(contexts) {
const foundContexts = new Set();
if (!contexts) {
return foundContexts;
}
for (const contextId of contexts) {
const context = this.getContext(contextId);
if (context.isTopLevelContext()) {
foundContexts.add(context);
}
else {
throw new InvalidArgumentException(`Non top-level context '${contextId}' given.`);
}
}
return foundContexts;
}
verifyContextsList(contexts) {
if (!contexts.length) {
return;
}
for (const contextId of contexts) {
this.getContext(contextId);
}
}
}
//# sourceMappingURL=BrowsingContextStorage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"BrowsingContextStorage.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/context/BrowsingContextStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
import type { Protocol } from 'devtools-protocol';
import { type BrowsingContext } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { EventManager } from '../session/EventManager.js';
export declare const enum NavigationEventName {
FragmentNavigated = "browsingContext.fragmentNavigated",
NavigationAborted = "browsingContext.navigationAborted",
NavigationFailed = "browsingContext.navigationFailed",
Load = "browsingContext.load"
}
export declare class NavigationResult {
readonly eventName: NavigationEventName;
readonly message?: string;
constructor(eventName: NavigationEventName, message?: string);
}
export declare class NavigationState {
#private;
readonly navigationId: `${string}-${string}-${string}-${string}-${string}`;
url: string;
loaderId?: string;
committed: Deferred<void>;
isFragmentNavigation?: boolean;
get finished(): Promise<NavigationResult>;
constructor(url: string, browsingContextId: string, isInitial: boolean, eventManager: EventManager);
navigationInfo(): BrowsingContext.NavigationInfo;
start(): void;
frameNavigated(): void;
fragmentNavigated(): void;
load(): void;
fail(message: string): void;
}
/**
* Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
*/
export declare class NavigationTracker {
#private;
constructor(url: string, browsingContextId: string, eventManager: EventManager, logger?: LoggerFn);
/**
* Returns current started ongoing navigation. It can be either a started pending
* navigation, or one is already navigated.
*/
get currentNavigationId(): `${string}-${string}-${string}-${string}-${string}`;
/**
* Flags if the current navigation relates to the initial to `about:blank` navigation.
*/
get isInitialNavigation(): boolean;
/**
* Url of the last navigated navigation.
*/
get url(): string;
/**
* Creates a pending navigation e.g. when navigation command is called. Required to
* provide navigation id before the actual navigation is started. It will be used when
* navigation started. Can be aborted, failed, fragment navigated, or became a current
* navigation.
*/
createPendingNavigation(url: string, canBeInitialNavigation?: boolean): NavigationState;
dispose(): void;
onTargetInfoChanged(url: string): void;
/**
* @param {string} unreachableUrl indicated the navigation is actually failed.
*/
frameNavigated(url: string, loaderId: string, unreachableUrl?: string): void;
navigatedWithinDocument(url: string, navigationType: Protocol.Page.NavigatedWithinDocumentEvent['navigationType']): void;
/**
* Required to mark navigation as fully complete.
* TODO: navigation should be complete when it became the current one on
* `Page.frameNavigated` or on navigating command finished with a new loader Id.
*/
loadPageEvent(loaderId: string): void;
/**
* Fail navigation due to navigation command failed.
*/
failNavigation(navigation: NavigationState, errorText: string): void;
/**
* Updates the navigation's `loaderId` and sets it as current one, if it is a
* cross-document navigation.
*/
navigationCommandFinished(navigation: NavigationState, loaderId?: string): void;
frameStartedNavigating(url: string, loaderId: string, navigationType: string): void;
/**
* If there is a navigation with the loaderId equals to the network request id, it means
* that the navigation failed.
*/
networkLoadingFailed(loaderId: string, errorText: string): void;
}
@@ -0,0 +1,325 @@
/*
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { ChromiumBidi, } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { LogType } from '../../../utils/log.js';
import { getTimestamp } from '../../../utils/time.js';
import { urlMatchesAboutBlank } from '../../../utils/urlHelpers.js';
import { uuidv4 } from '../../../utils/uuid.js';
export class NavigationResult {
eventName;
message;
constructor(eventName, message) {
this.eventName = eventName;
this.message = message;
}
}
export class NavigationState {
navigationId = uuidv4();
#browsingContextId;
#started = false;
#finished = new Deferred();
url;
loaderId;
#isInitial;
#eventManager;
committed = new Deferred();
isFragmentNavigation;
get finished() {
return this.#finished;
}
constructor(url, browsingContextId, isInitial, eventManager) {
this.#browsingContextId = browsingContextId;
this.url = url;
this.#isInitial = isInitial;
this.#eventManager = eventManager;
}
navigationInfo() {
return {
context: this.#browsingContextId,
navigation: this.navigationId,
timestamp: getTimestamp(),
url: this.url,
};
}
start() {
if (
// Initial navigation should not be reported.
!this.#isInitial &&
// No need in reporting started navigation twice.
!this.#started &&
// No need for reporting fragment navigations. Step 13 vs step 16 of the spec:
// https://html.spec.whatwg.org/#beginning-navigation:webdriver-bidi-navigation-started
!this.isFragmentNavigation) {
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.NavigationStarted,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
this.#started = true;
}
#finish(navigationResult) {
this.#started = true;
if (!this.#isInitial &&
!this.#finished.isFinished &&
navigationResult.eventName !== "browsingContext.load" /* NavigationEventName.Load */) {
this.#eventManager.registerEvent({
type: 'event',
method: navigationResult.eventName,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
this.#finished.resolve(navigationResult);
}
frameNavigated() {
this.committed.resolve();
if (!this.#isInitial) {
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.BrowsingContext.EventNames.NavigationCommitted,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
}
fragmentNavigated() {
this.committed.resolve();
this.#finish(new NavigationResult("browsingContext.fragmentNavigated" /* NavigationEventName.FragmentNavigated */));
}
load() {
this.#finish(new NavigationResult("browsingContext.load" /* NavigationEventName.Load */));
}
fail(message) {
this.#finish(new NavigationResult(this.committed.isFinished
? "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */
: "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */, message));
}
}
/**
* Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
*/
export class NavigationTracker {
#eventManager;
#logger;
#loaderIdToNavigationsMap = new Map();
#browsingContextId;
/**
* Last committed navigation is committed, but is not guaranteed to be finished, as it
* can still wait for `load` or `DOMContentLoaded` events.
*/
#lastCommittedNavigation;
/**
* Pending navigation is a navigation that is started but not yet committed.
*/
#pendingNavigation;
// Flags if the initial navigation to `about:blank` is in progress.
#isInitialNavigation = true;
constructor(url, browsingContextId, eventManager, logger) {
this.#browsingContextId = browsingContextId;
this.#eventManager = eventManager;
this.#logger = logger;
this.#isInitialNavigation = true;
// The initial navigation is always committed.
this.#lastCommittedNavigation = new NavigationState(url, browsingContextId, urlMatchesAboutBlank(url), this.#eventManager);
}
/**
* Returns current started ongoing navigation. It can be either a started pending
* navigation, or one is already navigated.
*/
get currentNavigationId() {
if (this.#pendingNavigation?.isFragmentNavigation === false) {
// Use pending navigation if it is started and it is not a fragment navigation.
return this.#pendingNavigation.navigationId;
}
// If the pending navigation is a fragment one, or if it is not exists, the last
// committed navigation should be used.
return this.#lastCommittedNavigation.navigationId;
}
/**
* Flags if the current navigation relates to the initial to `about:blank` navigation.
*/
get isInitialNavigation() {
return this.#isInitialNavigation;
}
/**
* Url of the last navigated navigation.
*/
get url() {
return this.#lastCommittedNavigation.url;
}
/**
* Creates a pending navigation e.g. when navigation command is called. Required to
* provide navigation id before the actual navigation is started. It will be used when
* navigation started. Can be aborted, failed, fragment navigated, or became a current
* navigation.
*/
createPendingNavigation(url, canBeInitialNavigation = false) {
this.#logger?.(LogType.debug, 'createCommandNavigation');
this.#isInitialNavigation =
canBeInitialNavigation &&
this.#isInitialNavigation &&
urlMatchesAboutBlank(url);
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
const navigation = new NavigationState(url, this.#browsingContextId, this.#isInitialNavigation, this.#eventManager);
this.#pendingNavigation = navigation;
return navigation;
}
dispose() {
this.#pendingNavigation?.fail('navigation canceled by context disposal');
this.#lastCommittedNavigation.fail('navigation canceled by context disposal');
}
// Update the current url.
onTargetInfoChanged(url) {
this.#logger?.(LogType.debug, `onTargetInfoChanged ${url}`);
this.#lastCommittedNavigation.url = url;
}
#getNavigationForFrameNavigated(url, loaderId) {
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
return this.#loaderIdToNavigationsMap.get(loaderId);
}
if (this.#pendingNavigation !== undefined &&
this.#pendingNavigation.loaderId === undefined) {
// This can be a pending navigation to `about:blank` created by a command. Use the
// pending navigation in this case.
return this.#pendingNavigation;
}
// Create a new pending navigation.
return this.createPendingNavigation(url, true);
}
/**
* @param {string} unreachableUrl indicated the navigation is actually failed.
*/
frameNavigated(url, loaderId, unreachableUrl) {
this.#logger?.(LogType.debug, `frameNavigated ${url}`);
if (unreachableUrl !== undefined) {
// The navigation failed.
const navigation = this.#loaderIdToNavigationsMap.get(loaderId) ??
this.#pendingNavigation ??
this.createPendingNavigation(unreachableUrl, true);
navigation.url = unreachableUrl;
navigation.start();
navigation.fail('the requested url is unreachable');
return;
}
const navigation = this.#getNavigationForFrameNavigated(url, loaderId);
if (navigation !== this.#lastCommittedNavigation) {
// Even though the `lastCommittedNavigation` is navigated, it still can be waiting
// for `load` or `DOMContentLoaded` events.
this.#lastCommittedNavigation.fail('navigation canceled by concurrent navigation');
}
navigation.url = url;
navigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
navigation.start();
navigation.frameNavigated();
this.#lastCommittedNavigation = navigation;
if (this.#pendingNavigation === navigation) {
this.#pendingNavigation = undefined;
}
}
navigatedWithinDocument(url, navigationType) {
this.#logger?.(LogType.debug, `navigatedWithinDocument ${url}, ${navigationType}`);
// Current navigation URL should be updated.
this.#lastCommittedNavigation.url = url;
if (navigationType !== 'fragment') {
// TODO: check for other navigation types, like `javascript`.
return;
}
// There is no way to map `navigatedWithinDocument` to a specific navigation. Consider
// it is the pending navigation, if it is a fragment one.
const fragmentNavigation = this.#pendingNavigation?.isFragmentNavigation === true
? this.#pendingNavigation
: new NavigationState(url, this.#browsingContextId, false, this.#eventManager);
// Finish ongoing navigation.
fragmentNavigation.fragmentNavigated();
if (fragmentNavigation === this.#pendingNavigation) {
this.#pendingNavigation = undefined;
}
}
/**
* Required to mark navigation as fully complete.
* TODO: navigation should be complete when it became the current one on
* `Page.frameNavigated` or on navigating command finished with a new loader Id.
*/
loadPageEvent(loaderId) {
this.#logger?.(LogType.debug, 'loadPageEvent');
// Even if it was an initial navigation, it is finished.
this.#isInitialNavigation = false;
this.#loaderIdToNavigationsMap.get(loaderId)?.load();
}
/**
* Fail navigation due to navigation command failed.
*/
failNavigation(navigation, errorText) {
this.#logger?.(LogType.debug, 'failCommandNavigation');
navigation.fail(errorText);
}
/**
* Updates the navigation's `loaderId` and sets it as current one, if it is a
* cross-document navigation.
*/
navigationCommandFinished(navigation, loaderId) {
this.#logger?.(LogType.debug, `finishCommandNavigation ${navigation.navigationId}, ${loaderId}`);
if (loaderId !== undefined) {
navigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
}
navigation.isFragmentNavigation = loaderId === undefined;
}
frameStartedNavigating(url, loaderId, navigationType) {
this.#logger?.(LogType.debug, `frameStartedNavigating ${url}, ${loaderId}`);
if (this.#pendingNavigation &&
this.#pendingNavigation?.loaderId !== undefined &&
this.#pendingNavigation?.loaderId !== loaderId) {
// If there is a pending navigation with loader id set, but not equal to the new
// loader id, cancel pending navigation.
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
this.#pendingNavigation = undefined;
}
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
const existingNavigation = this.#loaderIdToNavigationsMap.get(loaderId);
// Navigation can be changed from `sameDocument` to `differentDocument`.
existingNavigation.isFragmentNavigation =
NavigationTracker.#isFragmentNavigation(navigationType);
this.#pendingNavigation = existingNavigation;
return;
}
const pendingNavigation = this.#pendingNavigation ?? this.createPendingNavigation(url, true);
this.#loaderIdToNavigationsMap.set(loaderId, pendingNavigation);
pendingNavigation.isFragmentNavigation =
NavigationTracker.#isFragmentNavigation(navigationType);
pendingNavigation.url = url;
pendingNavigation.loaderId = loaderId;
pendingNavigation.start();
}
static #isFragmentNavigation(navigationType) {
// Page.frameStartedNavigating.navigationType can be one of the following values:
// reload, reloadBypassingCache, restore, restoreWithPost, historySameDocument,
// historyDifferentDocument, sameDocument, differentDocument.
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-frameStartedNavigating
return ['historySameDocument', 'sameDocument'].includes(navigationType);
}
/**
* If there is a navigation with the loaderId equals to the network request id, it means
* that the navigation failed.
*/
networkLoadingFailed(loaderId, errorText) {
this.#loaderIdToNavigationsMap.get(loaderId)?.fail(errorText);
}
}
//# sourceMappingURL=NavigationTracker.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { EmptyResult, Emulation, UAClientHints } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
export declare class EmulationProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage);
setGeolocationOverride(params: Emulation.SetGeolocationOverrideParameters): Promise<EmptyResult>;
setLocaleOverride(params: Emulation.SetLocaleOverrideParameters): Promise<EmptyResult>;
setScriptingEnabled(params: Emulation.SetScriptingEnabledParameters): Promise<EmptyResult>;
setScrollbarTypeOverride(params: Emulation.SetScrollbarTypeOverrideParameters): Promise<EmptyResult>;
setScreenOrientationOverride(params: Emulation.SetScreenOrientationOverrideParameters): Promise<EmptyResult>;
setScreenSettingsOverride(params: Emulation.SetScreenSettingsOverrideParameters): Promise<EmptyResult>;
setTimezoneOverride(params: Emulation.SetTimezoneOverrideParameters): Promise<EmptyResult>;
setTouchOverride(params: Emulation.SetTouchOverrideParameters): Promise<EmptyResult>;
setUserAgentOverrideParams(params: Emulation.SetUserAgentOverrideParameters): Promise<EmptyResult>;
setClientHintsOverride(params: UAClientHints.UserAgentClientHints.SetClientHintsOverrideCommand['params']): Promise<EmptyResult>;
setNetworkConditions(params: Emulation.SetNetworkConditionsParameters): Promise<EmptyResult>;
}
export declare function isValidLocale(locale: string): boolean;
export declare function isValidTimezone(timezone: string): boolean;
export declare function isTimeZoneOffsetString(timezone: string): boolean;
@@ -0,0 +1,397 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, UnsupportedOperationException, } from '../../../protocol/protocol.js';
export class EmulationProcessor {
#userContextStorage;
#browsingContextStorage;
#contextConfigStorage;
constructor(browsingContextStorage, userContextStorage, contextConfigStorage) {
this.#userContextStorage = userContextStorage;
this.#browsingContextStorage = browsingContextStorage;
this.#contextConfigStorage = contextConfigStorage;
}
async setGeolocationOverride(params) {
if ('coordinates' in params && 'error' in params) {
// Unreachable. Handled by params parser.
throw new InvalidArgumentException('Coordinates and error cannot be set at the same time');
}
let geolocation = null;
if ('coordinates' in params) {
if ((params.coordinates?.altitude ?? null) === null &&
(params.coordinates?.altitudeAccuracy ?? null) !== null) {
throw new InvalidArgumentException('Geolocation altitudeAccuracy can be set only with altitude');
}
geolocation = params.coordinates;
}
else if ('error' in params) {
if (params.error.type !== 'positionUnavailable') {
// Unreachable. Handled by params parser.
throw new InvalidArgumentException(`Unknown geolocation error ${params.error.type}`);
}
geolocation = params.error;
}
else {
// Unreachable. Handled by params parser.
throw new InvalidArgumentException(`Coordinates or error should be set`);
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
geolocation,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
geolocation,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setGeolocationOverride(config.geolocation ?? null);
}));
return {};
}
async setLocaleOverride(params) {
const locale = params.locale ?? null;
if (locale !== null && !isValidLocale(locale)) {
throw new InvalidArgumentException(`Invalid locale "${locale}"`);
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
locale,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
locale,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await Promise.all([
context.setLocaleOverride(config.locale ?? null),
// Set `AcceptLanguage` to locale.
context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints),
]);
}));
return {};
}
async setScriptingEnabled(params) {
const scriptingEnabled = params.enabled;
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
scriptingEnabled,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
scriptingEnabled,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setScriptingEnabled(config.scriptingEnabled ?? null);
}));
return {};
}
async setScrollbarTypeOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
scrollbarType: params.scrollbarType,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
scrollbarType: params.scrollbarType,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setScrollbarTypeOverride(config.scrollbarType ?? null);
}));
return {};
}
async setScreenOrientationOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
screenOrientation: params.screenOrientation,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
screenOrientation: params.screenOrientation,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
async setScreenSettingsOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
screenArea: params.screenArea,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
screenArea: params.screenArea,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
/**
* Returns a list of top-level browsing contexts.
*/
async #getRelatedTopLevelBrowsingContexts(browsingContextIds, userContextIds, allowGlobal = false) {
if (browsingContextIds === undefined && userContextIds === undefined) {
if (allowGlobal) {
return this.#browsingContextStorage.getTopLevelContexts();
}
throw new InvalidArgumentException('Either user contexts or browsing contexts must be provided');
}
if (browsingContextIds !== undefined && userContextIds !== undefined) {
throw new InvalidArgumentException('User contexts and browsing contexts are mutually exclusive');
}
const result = [];
if (browsingContextIds === undefined) {
// userContextIds !== undefined
if (userContextIds.length === 0) {
throw new InvalidArgumentException('user context should be provided');
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
}
else {
if (browsingContextIds.length === 0) {
throw new InvalidArgumentException('browsing context should be provided');
}
for (const browsingContextId of browsingContextIds) {
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new InvalidArgumentException('The command is only supported on the top-level context');
}
result.push(browsingContext);
}
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async setTimezoneOverride(params) {
let timezone = params.timezone ?? null;
if (timezone !== null && !isValidTimezone(timezone)) {
throw new InvalidArgumentException(`Invalid timezone "${timezone}"`);
}
if (timezone !== null && isTimeZoneOffsetString(timezone)) {
// CDP supports offset timezone with `GMT` prefix.
timezone = `GMT${timezone}`;
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
timezone,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
timezone,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setTimezoneOverride(config.timezone ?? null);
}));
return {};
}
async setTouchOverride(params) {
const maxTouchPoints = params.maxTouchPoints;
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
maxTouchPoints,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
maxTouchPoints,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
maxTouchPoints,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setTouchOverride(config.maxTouchPoints ?? null);
}));
return {};
}
async setUserAgentOverrideParams(params) {
if (params.userAgent === '') {
throw new UnsupportedOperationException('empty user agent string is not supported');
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
userAgent: params.userAgent,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
userAgent: params.userAgent,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
userAgent: params.userAgent,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
}));
return {};
}
async setClientHintsOverride(params) {
const clientHints = params.clientHints ?? null;
// Get all relevant contexts to update:
// 1. Specific browsing contexts (if provided).
// 2. All contexts for specific user contexts (if provided).
// 3. All top-level contexts (if global).
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
clientHints,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
clientHints,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
clientHints,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
}));
return {};
}
async setNetworkConditions(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
emulatedNetworkConditions: params.networkConditions,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
emulatedNetworkConditions: params.networkConditions,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
emulatedNetworkConditions: params.networkConditions,
});
}
if (params.networkConditions !== null &&
params.networkConditions.type !== 'offline') {
throw new UnsupportedOperationException(`Unsupported network conditions ${params.networkConditions.type}`);
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setEmulatedNetworkConditions(config.emulatedNetworkConditions ?? null);
}));
return {};
}
}
// Export for testing.
export function isValidLocale(locale) {
try {
new Intl.Locale(locale);
return true;
}
catch (e) {
if (e instanceof RangeError) {
return false;
}
// Re-throw other errors
throw e;
}
}
// Export for testing.
export function isValidTimezone(timezone) {
try {
Intl.DateTimeFormat(undefined, { timeZone: timezone });
return true;
}
catch (e) {
if (e instanceof RangeError) {
return false;
}
// Re-throw other errors
throw e;
}
}
// Export for testing.
export function isTimeZoneOffsetString(timezone) {
return /^[+-](?:2[0-3]|[01]\d)(?::[0-5]\d)?$/.test(timezone);
}
//# sourceMappingURL=EmulationProcessor.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { ActionOption } from './ActionOption.js';
import type { InputState } from './InputState.js';
export declare class ActionDispatcher {
#private;
static isMacOS: (context: BrowsingContextImpl) => Promise<boolean>;
constructor(inputState: InputState, browsingContextStorage: BrowsingContextStorage, contextId: string, isMacOS: boolean);
dispatchActions(optionsByTick: readonly (readonly Readonly<ActionOption>[])[]): Promise<void>;
dispatchTickActions(options: readonly Readonly<ActionOption>[]): Promise<void>;
}
@@ -0,0 +1,740 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, MoveTargetOutOfBoundsException, NoSuchElementException, } from '../../../protocol/protocol.js';
import { assert } from '../../../utils/assert.js';
import { isSingleComplexGrapheme, isSingleGrapheme, } from '../../../utils/graphemeTools.js';
import { PointerSource, } from './InputSource.js';
import { getKeyCode, getKeyLocation, getNormalizedKey } from './keyUtils.js';
import { KeyToKeyCode } from './USKeyboardLayout.js';
/** https://w3c.github.io/webdriver/#dfn-center-point */
const CALCULATE_IN_VIEW_CENTER_PT_DECL = ((i) => {
const t = i.getClientRects()[0], e = Math.max(0, Math.min(t.x, t.x + t.width)), n = Math.min(window.innerWidth, Math.max(t.x, t.x + t.width)), h = Math.max(0, Math.min(t.y, t.y + t.height)), m = Math.min(window.innerHeight, Math.max(t.y, t.y + t.height));
return [e + ((n - e) >> 1), h + ((m - h) >> 1)];
}).toString();
const IS_MAC_DECL = (() => {
return navigator.platform.toLowerCase().includes('mac');
}).toString();
async function getElementCenter(context, element) {
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
const result = await hiddenSandboxRealm.callFunction(CALCULATE_IN_VIEW_CENTER_PT_DECL, false, { type: 'undefined' }, [element]);
if (result.type === 'exception') {
throw new NoSuchElementException(`Origin element ${element.sharedId} was not found`);
}
assert(result.result.type === 'array');
assert(result.result.value?.[0]?.type === 'number');
assert(result.result.value?.[1]?.type === 'number');
const { result: { value: [{ value: x }, { value: y }], }, } = result;
return { x: x, y: y };
}
export class ActionDispatcher {
static isMacOS = async (context) => {
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
const result = await hiddenSandboxRealm.callFunction(IS_MAC_DECL, false);
assert(result.type !== 'exception');
assert(result.result.type === 'boolean');
return result.result.value;
};
#browsingContextStorage;
#tickStart = 0;
#tickDuration = 0;
#inputState;
#contextId;
#isMacOS;
constructor(inputState, browsingContextStorage, contextId, isMacOS) {
this.#browsingContextStorage = browsingContextStorage;
this.#inputState = inputState;
this.#contextId = contextId;
this.#isMacOS = isMacOS;
}
/**
* The context can be disposed between action ticks, so need to get it each time.
*/
get #context() {
return this.#browsingContextStorage.getContext(this.#contextId);
}
async dispatchActions(optionsByTick) {
await this.#inputState.queue.run(async () => {
for (const options of optionsByTick) {
await this.dispatchTickActions(options);
}
});
}
async dispatchTickActions(options) {
this.#tickStart = performance.now();
this.#tickDuration = 0;
for (const { action } of options) {
if ('duration' in action && action.duration !== undefined) {
this.#tickDuration = Math.max(this.#tickDuration, action.duration);
}
}
const promises = [
new Promise((resolve) => setTimeout(resolve, this.#tickDuration)),
];
for (const option of options) {
// In theory we have to wait for each action to happen, but CDP is serial,
// so as an optimization, we queue all CDP commands at once and await all
// of them.
promises.push(this.#dispatchAction(option));
}
await Promise.all(promises);
}
async #dispatchAction({ id, action }) {
const source = this.#inputState.get(id);
const keyState = this.#inputState.getGlobalKeyState();
switch (action.type) {
case 'keyDown': {
// SAFETY: The source is validated before.
await this.#dispatchKeyDownAction(source, action);
this.#inputState.cancelList.push({
id,
action: {
...action,
type: 'keyUp',
},
});
break;
}
case 'keyUp': {
// SAFETY: The source is validated before.
await this.#dispatchKeyUpAction(source, action);
break;
}
case 'pause': {
// TODO: Implement waiting on the input source.
break;
}
case 'pointerDown': {
// SAFETY: The source is validated before.
await this.#dispatchPointerDownAction(source, keyState, action);
this.#inputState.cancelList.push({
id,
action: {
...action,
type: 'pointerUp',
},
});
break;
}
case 'pointerMove': {
// SAFETY: The source is validated before.
await this.#dispatchPointerMoveAction(source, keyState, action);
break;
}
case 'pointerUp': {
// SAFETY: The source is validated before.
await this.#dispatchPointerUpAction(source, keyState, action);
break;
}
case 'scroll': {
// SAFETY: The source is validated before.
await this.#dispatchScrollAction(source, keyState, action);
break;
}
}
}
async #dispatchPointerDownAction(source, keyState, action) {
const { button } = action;
if (source.pressed.has(button)) {
return;
}
source.pressed.add(button);
const { x, y, subtype: pointerType } = source;
const { width, height, pressure, twist, tangentialPressure } = action;
const { tiltX, tiltY } = getTilt(action);
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
const { radiusX, radiusY } = getRadii(width ?? 1, height ?? 1);
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
case "pen" /* Input.PointerType.Pen */:
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mousePressed',
x,
y,
modifiers,
button: getCdpButton(button),
buttons: source.buttons,
clickCount: source.setClickCount(button, new PointerSource.ClickContext(x, y, performance.now())),
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
});
break;
case "touch" /* Input.PointerType.Touch */:
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [
{
x,
y,
radiusX,
radiusY,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
id: source.pointerId,
},
],
modifiers,
});
break;
}
source.radiusX = radiusX;
source.radiusY = radiusY;
source.force = pressure;
// --- Platform-specific code ends here ---
}
#dispatchPointerUpAction(source, keyState, action) {
const { button } = action;
if (!source.pressed.has(button)) {
return;
}
source.pressed.delete(button);
const { x, y, force, radiusX, radiusY, subtype: pointerType } = source;
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
case "pen" /* Input.PointerType.Pen */:
// TODO: Implement width and height when available.
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseReleased',
x,
y,
modifiers,
button: getCdpButton(button),
buttons: source.buttons,
clickCount: source.getClickCount(button),
pointerType,
});
case "touch" /* Input.PointerType.Touch */:
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [
{
x,
y,
id: source.pointerId,
force,
radiusX,
radiusY,
},
],
modifiers,
});
}
// --- Platform-specific code ends here ---
}
async #dispatchPointerMoveAction(source, keyState, action) {
const { x: startX, y: startY, subtype: pointerType } = source;
const { width, height, pressure, twist, tangentialPressure, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
const { tiltX, tiltY } = getTilt(action);
const { radiusX, radiusY } = getRadii(width ?? 1, height ?? 1);
const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY);
if (targetX < 0 || targetY < 0) {
throw new MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
}
let last;
do {
const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
last = ratio >= 1;
let x;
let y;
if (last) {
x = targetX;
y = targetY;
}
else {
x = Math.round(ratio * (targetX - startX) + startX);
y = Math.round(ratio * (targetY - startY) + startY);
}
if (source.x !== x || source.y !== y) {
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x,
y,
modifiers,
clickCount: 0,
button: getCdpButton(source.pressed.values().next().value ?? 5),
buttons: source.buttons,
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
});
break;
case "pen" /* Input.PointerType.Pen */:
if (source.pressed.size !== 0) {
// Empty `source.pressed.size` means the pen is not detected by digitizer.
// Dispatch a mouse event for the pen only if either:
// 1. the pen is hovering over the digitizer (0);
// 2. the pen is in contact with the digitizer (1);
// 3. the pen has at least one button pressed (2, 4, etc).
// https://www.w3.org/TR/pointerevents/#the-buttons-property
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x,
y,
modifiers,
clickCount: 0,
button: getCdpButton(source.pressed.values().next().value ?? 5),
buttons: source.buttons,
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure ?? 0.5,
});
}
break;
case "touch" /* Input.PointerType.Touch */:
if (source.pressed.size !== 0) {
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [
{
x,
y,
radiusX,
radiusY,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
id: source.pointerId,
},
],
modifiers,
});
}
break;
}
// --- Platform-specific code ends here ---
source.x = x;
source.y = y;
source.radiusX = radiusX;
source.radiusY = radiusY;
source.force = pressure;
}
} while (!last);
}
async #getFrameOffset() {
if (this.#context.id === this.#context.cdpTarget.id) {
return { x: 0, y: 0 };
}
// https://github.com/w3c/webdriver/pull/1847 proposes dispatching events from
// the top-level browsing context. This implementation dispatches it on the top-most
// same-target frame, which is not top-level one in case of OOPiF.
// TODO: switch to the top-level browsing context.
const { backendNodeId } = await this.#context.cdpTarget.cdpClient.sendCommand('DOM.getFrameOwner', { frameId: this.#context.id });
const { model: frameBoxModel } = await this.#context.cdpTarget.cdpClient.sendCommand('DOM.getBoxModel', {
backendNodeId,
});
return { x: frameBoxModel.content[0], y: frameBoxModel.content[1] };
}
async #getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY) {
let targetX;
let targetY;
const frameOffset = await this.#getFrameOffset();
switch (origin) {
case 'viewport':
targetX = offsetX + frameOffset.x;
targetY = offsetY + frameOffset.y;
break;
case 'pointer':
targetX = startX + offsetX + frameOffset.x;
targetY = startY + offsetY + frameOffset.y;
break;
default: {
const { x: posX, y: posY } = await getElementCenter(this.#context, origin.element);
// SAFETY: These can never be special numbers.
targetX = posX + offsetX + frameOffset.x;
targetY = posY + offsetY + frameOffset.y;
break;
}
}
return { targetX, targetY };
}
async #dispatchScrollAction(_source, keyState, action) {
const { deltaX: targetDeltaX, deltaY: targetDeltaY, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
if (origin === 'pointer') {
throw new InvalidArgumentException('"pointer" origin is invalid for scrolling.');
}
const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, 0, 0);
if (targetX < 0 || targetY < 0) {
throw new MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
}
let currentDeltaX = 0;
let currentDeltaY = 0;
let last;
do {
const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
last = ratio >= 1;
let deltaX;
let deltaY;
if (last) {
deltaX = targetDeltaX - currentDeltaX;
deltaY = targetDeltaY - currentDeltaY;
}
else {
deltaX = Math.round(ratio * targetDeltaX - currentDeltaX);
deltaY = Math.round(ratio * targetDeltaY - currentDeltaY);
}
if (deltaX !== 0 || deltaY !== 0) {
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseWheel',
deltaX,
deltaY,
x: targetX,
y: targetY,
modifiers,
});
// --- Platform-specific code ends here ---
currentDeltaX += deltaX;
currentDeltaY += deltaY;
}
} while (!last);
}
async #dispatchKeyDownAction(source, action) {
const rawKey = action.value;
if (!isSingleGrapheme(rawKey)) {
// https://w3c.github.io/webdriver/#dfn-process-a-key-action
// WebDriver spec allows a grapheme to be used.
throw new InvalidArgumentException(`Invalid key value: ${rawKey}`);
}
const isGrapheme = isSingleComplexGrapheme(rawKey);
const key = getNormalizedKey(rawKey);
const repeat = source.pressed.has(key);
const code = getKeyCode(rawKey);
const location = getKeyLocation(rawKey);
switch (key) {
case 'Alt':
source.alt = true;
break;
case 'Shift':
source.shift = true;
break;
case 'Control':
source.ctrl = true;
break;
case 'Meta':
source.meta = true;
break;
}
source.pressed.add(key);
const { modifiers } = source;
// --- Platform-specific code begins here ---
// The spread is a little hack so JS gives us an array of unicode characters
// to measure.
const unmodifiedText = getKeyEventUnmodifiedText(key, source, isGrapheme);
const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
let command;
// The following commands need to be declared because Chromium doesn't
// handle them. See
// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/blink/renderer/core/editing/editing_behavior.cc;l=169;drc=b8143cf1dfd24842890fcd831c4f5d909bef4fc4;bpv=0;bpt=1.
if (this.#isMacOS && source.meta) {
switch (code) {
case 'KeyA':
command = 'SelectAll';
break;
case 'KeyC':
command = 'Copy';
break;
case 'KeyV':
command = source.shift ? 'PasteAndMatchStyle' : 'Paste';
break;
case 'KeyX':
command = 'Cut';
break;
case 'KeyZ':
command = source.shift ? 'Redo' : 'Undo';
break;
default:
// Intentionally empty.
}
}
const promises = [
this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
windowsVirtualKeyCode: KeyToKeyCode[key],
key,
code,
text,
unmodifiedText,
autoRepeat: repeat,
isSystemKey: source.alt || undefined,
location: location < 3 ? location : undefined,
isKeypad: location === 3,
modifiers,
commands: command ? [command] : undefined,
}),
];
// Drag cancelling happens on escape.
if (key === 'Escape') {
if (!source.alt &&
((this.#isMacOS && !source.ctrl && !source.meta) || !this.#isMacOS)) {
promises.push(this.#context.cdpTarget.cdpClient.sendCommand('Input.cancelDragging'));
}
}
await Promise.all(promises);
// --- Platform-specific code ends here ---
}
#dispatchKeyUpAction(source, action) {
const rawKey = action.value;
if (!isSingleGrapheme(rawKey)) {
// https://w3c.github.io/webdriver/#dfn-process-a-key-action
// WebDriver spec allows a grapheme to be used.
throw new InvalidArgumentException(`Invalid key value: ${rawKey}`);
}
const isGrapheme = isSingleComplexGrapheme(rawKey);
const key = getNormalizedKey(rawKey);
if (!source.pressed.has(key)) {
return;
}
const code = getKeyCode(rawKey);
const location = getKeyLocation(rawKey);
switch (key) {
case 'Alt':
source.alt = false;
break;
case 'Shift':
source.shift = false;
break;
case 'Control':
source.ctrl = false;
break;
case 'Meta':
source.meta = false;
break;
}
source.pressed.delete(key);
const { modifiers } = source;
// --- Platform-specific code begins here ---
// The spread is a little hack so JS gives us an array of unicode characters
// to measure.
const unmodifiedText = getKeyEventUnmodifiedText(key, source, isGrapheme);
const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
type: 'keyUp',
windowsVirtualKeyCode: KeyToKeyCode[key],
key,
code,
text,
unmodifiedText,
location: location < 3 ? location : undefined,
isSystemKey: source.alt || undefined,
isKeypad: location === 3,
modifiers,
});
// --- Platform-specific code ends here ---
}
}
/**
* Translates a non-grapheme key to either an `undefined` for a special keys, or a single
* character modified by shift if needed.
*/
const getKeyEventUnmodifiedText = (key, source, isGrapheme) => {
if (isGrapheme) {
// Graphemes should be presented as text in the CDP command.
return key;
}
if (key === 'Enter') {
return '\r';
}
// If key is not a single character, it is a normalized key value, and should be
// presented as key, not text in the CDP command.
return [...key].length === 1
? source.shift
? key.toLocaleUpperCase('en-US')
: key
: undefined;
};
const getKeyEventText = (code, source) => {
if (source.ctrl) {
switch (code) {
case 'Digit2':
if (source.shift) {
return '\x00';
}
break;
case 'KeyA':
return '\x01';
case 'KeyB':
return '\x02';
case 'KeyC':
return '\x03';
case 'KeyD':
return '\x04';
case 'KeyE':
return '\x05';
case 'KeyF':
return '\x06';
case 'KeyG':
return '\x07';
case 'KeyH':
return '\x08';
case 'KeyI':
return '\x09';
case 'KeyJ':
return '\x0A';
case 'KeyK':
return '\x0B';
case 'KeyL':
return '\x0C';
case 'KeyM':
return '\x0D';
case 'KeyN':
return '\x0E';
case 'KeyO':
return '\x0F';
case 'KeyP':
return '\x10';
case 'KeyQ':
return '\x11';
case 'KeyR':
return '\x12';
case 'KeyS':
return '\x13';
case 'KeyT':
return '\x14';
case 'KeyU':
return '\x15';
case 'KeyV':
return '\x16';
case 'KeyW':
return '\x17';
case 'KeyX':
return '\x18';
case 'KeyY':
return '\x19';
case 'KeyZ':
return '\x1A';
case 'BracketLeft':
return '\x1B';
case 'Backslash':
return '\x1C';
case 'BracketRight':
return '\x1D';
case 'Digit6':
if (source.shift) {
return '\x1E';
}
break;
case 'Minus':
return '\x1F';
}
return '';
}
if (source.alt) {
return '';
}
return;
};
function getCdpButton(button) {
// https://www.w3.org/TR/pointerevents/#the-button-property
switch (button) {
case 0:
return 'left';
case 1:
return 'middle';
case 2:
return 'right';
case 3:
return 'back';
case 4:
return 'forward';
default:
return 'none';
}
}
function getTilt(action) {
// https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
const altitudeAngle = action.altitudeAngle ?? Math.PI / 2;
const azimuthAngle = action.azimuthAngle ?? 0;
let tiltXRadians = 0;
let tiltYRadians = 0;
if (altitudeAngle === 0) {
// the pen is in the X-Y plane
if (azimuthAngle === 0 || azimuthAngle === 2 * Math.PI) {
// pen is on positive X axis
tiltXRadians = Math.PI / 2;
}
if (azimuthAngle === Math.PI / 2) {
// pen is on positive Y axis
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle === Math.PI) {
// pen is on negative X axis
tiltXRadians = -Math.PI / 2;
}
if (azimuthAngle === (3 * Math.PI) / 2) {
// pen is on negative Y axis
tiltYRadians = -Math.PI / 2;
}
if (azimuthAngle > 0 && azimuthAngle < Math.PI / 2) {
tiltXRadians = Math.PI / 2;
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle > Math.PI / 2 && azimuthAngle < Math.PI) {
tiltXRadians = -Math.PI / 2;
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle > Math.PI && azimuthAngle < (3 * Math.PI) / 2) {
tiltXRadians = -Math.PI / 2;
tiltYRadians = -Math.PI / 2;
}
if (azimuthAngle > (3 * Math.PI) / 2 && azimuthAngle < 2 * Math.PI) {
tiltXRadians = Math.PI / 2;
tiltYRadians = -Math.PI / 2;
}
}
if (altitudeAngle !== 0) {
const tanAlt = Math.tan(altitudeAngle);
tiltXRadians = Math.atan(Math.cos(azimuthAngle) / tanAlt);
tiltYRadians = Math.atan(Math.sin(azimuthAngle) / tanAlt);
}
const factor = 180 / Math.PI;
return {
tiltX: Math.round(tiltXRadians * factor),
tiltY: Math.round(tiltYRadians * factor),
};
}
function getRadii(width, height) {
return {
radiusX: width ? width / 2 : 0.5,
radiusY: height ? height / 2 : 0.5,
};
}
//# sourceMappingURL=ActionDispatcher.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Input } from '../../../protocol/protocol.js';
export type ActionOption = ActionOptionFor<Input.NoneSourceAction | Input.KeySourceAction | Input.PointerSourceAction | Input.WheelSourceAction>;
export interface ActionOptionFor<A> {
id: string;
action: A;
}
@@ -0,0 +1,18 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
//# sourceMappingURL=ActionOption.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ActionOption.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/input/ActionOption.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}
@@ -0,0 +1,9 @@
import { Input, type EmptyResult } from '../../../protocol/protocol.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
export declare class InputProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage);
performActions(params: Input.PerformActionsParameters): Promise<EmptyResult>;
releaseActions(params: Input.ReleaseActionsParameters): Promise<EmptyResult>;
setFiles(params: Input.SetFilesParameters): Promise<EmptyResult>;
}
@@ -0,0 +1,190 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, NoSuchElementException, UnableToSetFileInputException, NoSuchNodeException, } from '../../../protocol/protocol.js';
import { assert } from '../../../utils/assert.js';
import { ActionDispatcher } from '../input/ActionDispatcher.js';
import { InputStateManager } from '../input/InputStateManager.js';
export class InputProcessor {
#browsingContextStorage;
#inputStateManager = new InputStateManager();
constructor(browsingContextStorage) {
this.#browsingContextStorage = browsingContextStorage;
}
async performActions(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const inputState = this.#inputStateManager.get(context.top);
const actionsByTick = this.#getActionsByTick(params, inputState);
const dispatcher = new ActionDispatcher(inputState, this.#browsingContextStorage, params.context, await ActionDispatcher.isMacOS(context).catch(() => false));
await dispatcher.dispatchActions(actionsByTick);
return {};
}
async releaseActions(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const topContext = context.top;
const inputState = this.#inputStateManager.get(topContext);
const dispatcher = new ActionDispatcher(inputState, this.#browsingContextStorage, params.context, await ActionDispatcher.isMacOS(context).catch(() => false));
await dispatcher.dispatchTickActions(inputState.cancelList.reverse());
this.#inputStateManager.delete(topContext);
return {};
}
async setFiles(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
let result;
try {
result = await hiddenSandboxRealm.callFunction(String(function getFiles(fileListLength) {
if (!(this instanceof HTMLInputElement)) {
if (this instanceof Element) {
return 1 /* ErrorCode.Element */;
}
return 0 /* ErrorCode.Node */;
}
if (this.type !== 'file') {
return 2 /* ErrorCode.Type */;
}
if (this.disabled) {
return 3 /* ErrorCode.Disabled */;
}
if (fileListLength > 1 && !this.multiple) {
return 4 /* ErrorCode.Multiple */;
}
return;
}), false, params.element, [{ type: 'number', value: params.files.length }]);
}
catch {
throw new NoSuchNodeException(`Could not find element ${params.element.sharedId}`);
}
assert(result.type === 'success');
if (result.result.type === 'number') {
switch (result.result.value) {
case 0 /* ErrorCode.Node */: {
throw new NoSuchElementException(`Could not find element ${params.element.sharedId}`);
}
case 1 /* ErrorCode.Element */: {
throw new UnableToSetFileInputException(`Element ${params.element.sharedId} is not a input`);
}
case 2 /* ErrorCode.Type */: {
throw new UnableToSetFileInputException(`Input element ${params.element.sharedId} is not a file type`);
}
case 3 /* ErrorCode.Disabled */: {
throw new UnableToSetFileInputException(`Input element ${params.element.sharedId} is disabled`);
}
case 4 /* ErrorCode.Multiple */: {
throw new UnableToSetFileInputException(`Cannot set multiple files on a non-multiple input element`);
}
}
}
/**
* The zero-length array is a special case, it seems that
* DOM.setFileInputFiles does not actually update the files in that case, so
* the solution is to eval the element value to a new FileList directly.
*/
if (params.files.length === 0) {
// XXX: These events should converted to trusted events. Perhaps do this
// in `DOM.setFileInputFiles`?
await hiddenSandboxRealm.callFunction(String(function dispatchEvent() {
if (this.files?.length === 0) {
this.dispatchEvent(new Event('cancel', {
bubbles: true,
}));
return;
}
this.files = new DataTransfer().files;
// Dispatch events for this case because it should behave akin to a user action.
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
}), false, params.element);
return {};
}
// Our goal here is to iterate over the input element files and get their
// file paths.
const paths = [];
for (let i = 0; i < params.files.length; ++i) {
const result = await hiddenSandboxRealm.callFunction(String(function getFiles(index) {
return this.files?.item(index);
}), false, params.element, [{ type: 'number', value: 0 }], "root" /* Script.ResultOwnership.Root */);
assert(result.type === 'success');
if (result.result.type !== 'object') {
break;
}
const { handle } = result.result;
assert(handle !== undefined);
const { path } = await hiddenSandboxRealm.cdpClient.sendCommand('DOM.getFileInfo', {
objectId: handle,
});
paths.push(path);
// Cleanup the handle.
void hiddenSandboxRealm.disown(handle).catch(undefined);
}
paths.sort();
// We create a new array so we preserve the order of the original files.
const sortedFiles = [...params.files].sort();
if (paths.length !== params.files.length ||
sortedFiles.some((path, index) => {
return paths[index] !== path;
})) {
const { objectId } = await hiddenSandboxRealm.deserializeForCdp(params.element);
// This cannot throw since this was just used in `callFunction` above.
assert(objectId !== undefined);
await hiddenSandboxRealm.cdpClient.sendCommand('DOM.setFileInputFiles', {
files: params.files,
objectId,
});
}
else {
// XXX: We should dispatch a trusted event.
await hiddenSandboxRealm.callFunction(String(function dispatchEvent() {
this.dispatchEvent(new Event('cancel', {
bubbles: true,
}));
}), false, params.element);
}
return {};
}
#getActionsByTick(params, inputState) {
const actionsByTick = [];
for (const action of params.actions) {
switch (action.type) {
case "pointer" /* SourceType.Pointer */: {
action.parameters ??= { pointerType: "mouse" /* Input.PointerType.Mouse */ };
action.parameters.pointerType ??= "mouse" /* Input.PointerType.Mouse */;
const source = inputState.getOrCreate(action.id, "pointer" /* SourceType.Pointer */, action.parameters.pointerType);
if (source.subtype !== action.parameters.pointerType) {
throw new InvalidArgumentException(`Expected input source ${action.id} to be ${source.subtype}; got ${action.parameters.pointerType}.`);
}
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043
source.resetClickCount();
break;
}
default:
inputState.getOrCreate(action.id, action.type);
}
const actions = action.actions.map((item) => ({
id: action.id,
action: item,
}));
for (let i = 0; i < actions.length; i++) {
if (actionsByTick.length === i) {
actionsByTick.push([]);
}
actionsByTick[i].push(actions[i]);
}
}
return actionsByTick;
}
}
//# sourceMappingURL=InputProcessor.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,78 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Input } from '../../../protocol/protocol.js';
export declare const enum SourceType {
Key = "key",
Pointer = "pointer",
Wheel = "wheel",
None = "none"
}
export declare class NoneSource {
type: SourceType.None;
}
export declare class KeySource {
#private;
type: SourceType.Key;
pressed: Set<string>;
get modifiers(): number;
get alt(): boolean;
set alt(value: boolean);
get ctrl(): boolean;
set ctrl(value: boolean);
get meta(): boolean;
set meta(value: boolean);
get shift(): boolean;
set shift(value: boolean);
}
export declare class PointerSource {
#private;
type: SourceType.Pointer;
subtype: Input.PointerType;
pointerId: number;
pressed: Set<number>;
x: number;
y: number;
radiusX?: number;
radiusY?: number;
force?: number;
constructor(id: number, subtype: Input.PointerType);
get buttons(): number;
static ClickContext: {
new (x: number, y: number, time: number): {
count: number;
"__#private@#x": number;
"__#private@#y": number;
"__#private@#time": number;
compare(context: /*elided*/ any): boolean;
};
"__#private@#DOUBLE_CLICK_TIME_MS": number;
"__#private@#MAX_DOUBLE_CLICK_RADIUS": number;
};
setClickCount(button: number, context: InstanceType<typeof PointerSource.ClickContext>): number;
getClickCount(button: number): number;
/**
* Resets click count. Resets consequent click counter. Prevents grouping clicks in
* different `performActions` calls, so that they are not grouped as double, triple etc
* clicks. Required for https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043.
*/
resetClickCount(): void;
}
export declare class WheelSource {
type: SourceType.Wheel;
}
export type InputSource = NoneSource | KeySource | PointerSource | WheelSource;
export type InputSourceFor<Type extends SourceType> = Type extends SourceType.Key ? KeySource : Type extends SourceType.Pointer ? PointerSource : Type extends SourceType.Wheel ? WheelSource : NoneSource;
@@ -0,0 +1,154 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var _a;
export class NoneSource {
type = "none" /* SourceType.None */;
}
export class KeySource {
type = "key" /* SourceType.Key */;
pressed = new Set();
// This is a bitfield that matches the modifiers parameter of
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
#modifiers = 0;
get modifiers() {
return this.#modifiers;
}
get alt() {
return (this.#modifiers & 1) === 1;
}
set alt(value) {
this.#setModifier(value, 1);
}
get ctrl() {
return (this.#modifiers & 2) === 2;
}
set ctrl(value) {
this.#setModifier(value, 2);
}
get meta() {
return (this.#modifiers & 4) === 4;
}
set meta(value) {
this.#setModifier(value, 4);
}
get shift() {
return (this.#modifiers & 8) === 8;
}
set shift(value) {
this.#setModifier(value, 8);
}
#setModifier(value, bit) {
if (value) {
this.#modifiers |= bit;
}
else {
this.#modifiers &= ~bit;
}
}
}
export class PointerSource {
type = "pointer" /* SourceType.Pointer */;
subtype;
pointerId;
pressed = new Set();
x = 0;
y = 0;
radiusX;
radiusY;
force;
constructor(id, subtype) {
this.pointerId = id;
this.subtype = subtype;
}
// This is a bitfield that matches the buttons parameter of
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
get buttons() {
let buttons = 0;
for (const button of this.pressed) {
switch (button) {
case 0:
buttons |= 1;
break;
case 1:
buttons |= 4;
break;
case 2:
buttons |= 2;
break;
case 3:
buttons |= 8;
break;
case 4:
buttons |= 16;
break;
}
}
return buttons;
}
// --- Platform-specific code starts here ---
// Input.dispatchMouseEvent doesn't know the concept of double click, so we
// need to create the logic, similar to how it's done for OSes:
// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:ui/events/event.cc;l=479
static ClickContext = class ClickContext {
static #DOUBLE_CLICK_TIME_MS = 500;
static #MAX_DOUBLE_CLICK_RADIUS = 2;
count = 0;
#x;
#y;
#time;
constructor(x, y, time) {
this.#x = x;
this.#y = y;
this.#time = time;
}
compare(context) {
return (
// The click needs to be within a certain amount of ms.
context.#time - this.#time > ClickContext.#DOUBLE_CLICK_TIME_MS ||
// The click needs to be within a certain square radius.
Math.abs(context.#x - this.#x) >
ClickContext.#MAX_DOUBLE_CLICK_RADIUS ||
Math.abs(context.#y - this.#y) > ClickContext.#MAX_DOUBLE_CLICK_RADIUS);
}
};
#clickContexts = new Map();
setClickCount(button, context) {
let storedContext = this.#clickContexts.get(button);
if (!storedContext || storedContext.compare(context)) {
storedContext = context;
}
++storedContext.count;
this.#clickContexts.set(button, storedContext);
return storedContext.count;
}
getClickCount(button) {
return this.#clickContexts.get(button)?.count ?? 0;
}
/**
* Resets click count. Resets consequent click counter. Prevents grouping clicks in
* different `performActions` calls, so that they are not grouped as double, triple etc
* clicks. Required for https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043.
*/
resetClickCount() {
this.#clickContexts = new Map();
}
}
_a = PointerSource;
export class WheelSource {
type = "wheel" /* SourceType.Wheel */;
}
//# sourceMappingURL=InputSource.js.map
@@ -0,0 +1 @@
{"version":3,"file":"InputSource.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/input/InputSource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Input } from '../../../protocol/protocol.js';
import { Mutex } from '../../../utils/Mutex.js';
import type { ActionOption } from './ActionOption.js';
import { KeySource, PointerSource, SourceType, type InputSource, type InputSourceFor } from './InputSource.js';
export declare class InputState {
#private;
cancelList: ActionOption[];
getOrCreate(id: string, type: SourceType.Pointer, subtype: Input.PointerType): PointerSource;
getOrCreate<Type extends SourceType>(id: string, type: Type): InputSourceFor<Type>;
get(id: string): InputSource;
getGlobalKeyState(): KeySource;
get queue(): Mutex;
}
@@ -0,0 +1,89 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, UnknownErrorException, } from '../../../protocol/protocol.js';
import { Mutex } from '../../../utils/Mutex.js';
import { KeySource, NoneSource, PointerSource, WheelSource, } from './InputSource.js';
export class InputState {
cancelList = [];
#sources = new Map();
#mutex = new Mutex();
getOrCreate(id, type, subtype) {
let source = this.#sources.get(id);
if (!source) {
switch (type) {
case "none" /* SourceType.None */:
source = new NoneSource();
break;
case "key" /* SourceType.Key */:
source = new KeySource();
break;
case "pointer" /* SourceType.Pointer */: {
let pointerId = subtype === "mouse" /* Input.PointerType.Mouse */ ? 0 : 2;
const pointerIds = new Set();
for (const [, source] of this.#sources) {
if (source.type === "pointer" /* SourceType.Pointer */) {
pointerIds.add(source.pointerId);
}
}
while (pointerIds.has(pointerId)) {
++pointerId;
}
source = new PointerSource(pointerId, subtype);
break;
}
case "wheel" /* SourceType.Wheel */:
source = new WheelSource();
break;
default:
throw new InvalidArgumentException(`Expected "${"none" /* SourceType.None */}", "${"key" /* SourceType.Key */}", "${"pointer" /* SourceType.Pointer */}", or "${"wheel" /* SourceType.Wheel */}". Found unknown source type ${type}.`);
}
this.#sources.set(id, source);
return source;
}
if (source.type !== type) {
throw new InvalidArgumentException(`Input source type of ${id} is ${source.type}, but received ${type}.`);
}
return source;
}
get(id) {
const source = this.#sources.get(id);
if (!source) {
throw new UnknownErrorException(`Internal error.`);
}
return source;
}
getGlobalKeyState() {
const state = new KeySource();
for (const [, source] of this.#sources) {
if (source.type !== "key" /* SourceType.Key */) {
continue;
}
for (const pressed of source.pressed) {
state.pressed.add(pressed);
}
state.alt ||= source.alt;
state.ctrl ||= source.ctrl;
state.meta ||= source.meta;
state.shift ||= source.shift;
}
return state;
}
get queue() {
return this.#mutex;
}
}
//# sourceMappingURL=InputState.js.map
@@ -0,0 +1 @@
{"version":3,"file":"InputState.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/input/InputState.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import { InputState } from './InputState.js';
export declare class InputStateManager extends WeakMap<BrowsingContextImpl, InputState> {
get(context: BrowsingContextImpl): InputState;
}
@@ -0,0 +1,30 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from '../../../utils/assert.js';
import { InputState } from './InputState.js';
// We use a weak map here as specified here:
// https://www.w3.org/TR/webdriver/#dfn-browsing-context-input-state-map
export class InputStateManager extends WeakMap {
get(context) {
assert(context.isTopLevelContext());
if (!this.has(context)) {
this.set(context, new InputState());
}
return super.get(context);
}
}
//# sourceMappingURL=InputStateManager.js.map
@@ -0,0 +1 @@
{"version":3,"file":"InputStateManager.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/input/InputStateManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,0BAA0B,CAAC;AAGhD,OAAO,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAE3C,4CAA4C;AAC5C,wEAAwE;AACxE,MAAM,OAAO,iBAAkB,SAAQ,OAGtC;IACU,GAAG,CAAC,OAA4B;QACvC,MAAM,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAEpC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,UAAU,EAAE,CAAC,CAAC;QACtC,CAAC;QAED,OAAO,KAAK,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;IAC7B,CAAC;CACF"}
@@ -0,0 +1,17 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export declare const KeyToKeyCode: Record<string, number | undefined>;
@@ -0,0 +1,271 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO: Remove this once https://crrev.com/c/4548290 is stably in Chromium.
// `Input.dispatchKeyboardEvent` will automatically handle these conversions.
export const KeyToKeyCode = {
'0': 48,
'1': 49,
'2': 50,
'3': 51,
'4': 52,
'5': 53,
'6': 54,
'7': 55,
'8': 56,
'9': 57,
Abort: 3,
Help: 6,
Backspace: 8,
Tab: 9,
Numpad5: 12,
NumpadEnter: 13,
Enter: 13,
'\\r': 13,
'\\n': 13,
ShiftLeft: 16,
ShiftRight: 16,
ControlLeft: 17,
ControlRight: 17,
AltLeft: 18,
AltRight: 18,
Pause: 19,
CapsLock: 20,
Escape: 27,
Convert: 28,
NonConvert: 29,
Space: 32,
Numpad9: 33,
PageUp: 33,
Numpad3: 34,
PageDown: 34,
End: 35,
Numpad1: 35,
Home: 36,
Numpad7: 36,
ArrowLeft: 37,
Numpad4: 37,
Numpad8: 38,
ArrowUp: 38,
ArrowRight: 39,
Numpad6: 39,
Numpad2: 40,
ArrowDown: 40,
Select: 41,
Open: 43,
PrintScreen: 44,
Insert: 45,
Numpad0: 45,
Delete: 46,
NumpadDecimal: 46,
Digit0: 48,
Digit1: 49,
Digit2: 50,
Digit3: 51,
Digit4: 52,
Digit5: 53,
Digit6: 54,
Digit7: 55,
Digit8: 56,
Digit9: 57,
KeyA: 65,
KeyB: 66,
KeyC: 67,
KeyD: 68,
KeyE: 69,
KeyF: 70,
KeyG: 71,
KeyH: 72,
KeyI: 73,
KeyJ: 74,
KeyK: 75,
KeyL: 76,
KeyM: 77,
KeyN: 78,
KeyO: 79,
KeyP: 80,
KeyQ: 81,
KeyR: 82,
KeyS: 83,
KeyT: 84,
KeyU: 85,
KeyV: 86,
KeyW: 87,
KeyX: 88,
KeyY: 89,
KeyZ: 90,
MetaLeft: 91,
MetaRight: 92,
ContextMenu: 93,
NumpadMultiply: 106,
NumpadAdd: 107,
NumpadSubtract: 109,
NumpadDivide: 111,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
F13: 124,
F14: 125,
F15: 126,
F16: 127,
F17: 128,
F18: 129,
F19: 130,
F20: 131,
F21: 132,
F22: 133,
F23: 134,
F24: 135,
NumLock: 144,
ScrollLock: 145,
AudioVolumeMute: 173,
AudioVolumeDown: 174,
AudioVolumeUp: 175,
MediaTrackNext: 176,
MediaTrackPrevious: 177,
MediaStop: 178,
MediaPlayPause: 179,
Semicolon: 186,
Equal: 187,
NumpadEqual: 187,
Comma: 188,
Minus: 189,
Period: 190,
Slash: 191,
Backquote: 192,
BracketLeft: 219,
Backslash: 220,
BracketRight: 221,
Quote: 222,
AltGraph: 225,
Props: 247,
Cancel: 3,
Clear: 12,
Shift: 16,
Control: 17,
Alt: 18,
Accept: 30,
ModeChange: 31,
' ': 32,
Print: 42,
Execute: 43,
'\\u0000': 46,
a: 65,
b: 66,
c: 67,
d: 68,
e: 69,
f: 70,
g: 71,
h: 72,
i: 73,
j: 74,
k: 75,
l: 76,
m: 77,
n: 78,
o: 79,
p: 80,
q: 81,
r: 82,
s: 83,
t: 84,
u: 85,
v: 86,
w: 87,
x: 88,
y: 89,
z: 90,
Meta: 91,
'*': 106,
'+': 107,
'-': 109,
'/': 111,
';': 186,
'=': 187,
',': 188,
'.': 190,
'`': 192,
'[': 219,
'\\\\': 220,
']': 221,
"'": 222,
Attn: 246,
CrSel: 247,
ExSel: 248,
EraseEof: 249,
Play: 250,
ZoomOut: 251,
')': 48,
'!': 49,
'@': 50,
'#': 51,
$: 52,
'%': 53,
'^': 54,
'&': 55,
'(': 57,
A: 65,
B: 66,
C: 67,
D: 68,
E: 69,
F: 70,
G: 71,
H: 72,
I: 73,
J: 74,
K: 75,
L: 76,
M: 77,
N: 78,
O: 79,
P: 80,
Q: 81,
R: 82,
S: 83,
T: 84,
U: 85,
V: 86,
W: 87,
X: 88,
Y: 89,
Z: 90,
':': 186,
'<': 188,
_: 189,
'>': 190,
'?': 191,
'~': 192,
'{': 219,
'|': 220,
'}': 221,
'"': 222,
Camera: 44,
EndCall: 95,
VolumeDown: 182,
VolumeUp: 183,
};
//# sourceMappingURL=USKeyboardLayout.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,31 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Returns the normalized key value for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-normalized-key-value
*/
export declare function getNormalizedKey(value: string): string;
/**
* Returns the key code for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-shifted-character
*/
export declare function getKeyCode(key: string): string | undefined;
/**
* Returns the location of the key according to the table:
* https://w3c.github.io/webdriver/#dfn-key-location
*/
export declare function getKeyLocation(key: string): 0 | 1 | 2 | 3;
@@ -0,0 +1,492 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Returns the normalized key value for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-normalized-key-value
*/
export function getNormalizedKey(value) {
switch (value) {
case '\uE000':
return 'Unidentified';
case '\uE001':
return 'Cancel';
case '\uE002':
return 'Help';
case '\uE003':
return 'Backspace';
case '\uE004':
return 'Tab';
case '\uE005':
return 'Clear';
// Specification declares the '\uE006' to be `Return`, but it is not supported by
// Chrome, so fall back to `Enter`, which aligns with WPT.
case '\uE006':
case '\uE007':
return 'Enter';
case '\uE008':
return 'Shift';
case '\uE009':
return 'Control';
case '\uE00A':
return 'Alt';
case '\uE00B':
return 'Pause';
case '\uE00C':
return 'Escape';
case '\uE00D':
return ' ';
case '\uE00E':
return 'PageUp';
case '\uE00F':
return 'PageDown';
case '\uE010':
return 'End';
case '\uE011':
return 'Home';
case '\uE012':
return 'ArrowLeft';
case '\uE013':
return 'ArrowUp';
case '\uE014':
return 'ArrowRight';
case '\uE015':
return 'ArrowDown';
case '\uE016':
return 'Insert';
case '\uE017':
return 'Delete';
case '\uE018':
return ';';
case '\uE019':
return '=';
case '\uE01A':
return '0';
case '\uE01B':
return '1';
case '\uE01C':
return '2';
case '\uE01D':
return '3';
case '\uE01E':
return '4';
case '\uE01F':
return '5';
case '\uE020':
return '6';
case '\uE021':
return '7';
case '\uE022':
return '8';
case '\uE023':
return '9';
case '\uE024':
return '*';
case '\uE025':
return '+';
case '\uE026':
return ',';
case '\uE027':
return '-';
case '\uE028':
return '.';
case '\uE029':
return '/';
case '\uE031':
return 'F1';
case '\uE032':
return 'F2';
case '\uE033':
return 'F3';
case '\uE034':
return 'F4';
case '\uE035':
return 'F5';
case '\uE036':
return 'F6';
case '\uE037':
return 'F7';
case '\uE038':
return 'F8';
case '\uE039':
return 'F9';
case '\uE03A':
return 'F10';
case '\uE03B':
return 'F11';
case '\uE03C':
return 'F12';
case '\uE03D':
return 'Meta';
case '\uE040':
return 'ZenkakuHankaku';
case '\uE050':
return 'Shift';
case '\uE051':
return 'Control';
case '\uE052':
return 'Alt';
case '\uE053':
return 'Meta';
case '\uE054':
return 'PageUp';
case '\uE055':
return 'PageDown';
case '\uE056':
return 'End';
case '\uE057':
return 'Home';
case '\uE058':
return 'ArrowLeft';
case '\uE059':
return 'ArrowUp';
case '\uE05A':
return 'ArrowRight';
case '\uE05B':
return 'ArrowDown';
case '\uE05C':
return 'Insert';
case '\uE05D':
return 'Delete';
default:
return value;
}
}
/**
* Returns the key code for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-shifted-character
*/
export function getKeyCode(key) {
switch (key) {
case '`':
case '~':
return 'Backquote';
case '\\':
case '|':
return 'Backslash';
case '\uE003':
return 'Backspace';
case '[':
case '{':
return 'BracketLeft';
case ']':
case '}':
return 'BracketRight';
case ',':
case '<':
return 'Comma';
case '0':
case ')':
return 'Digit0';
case '1':
case '!':
return 'Digit1';
case '2':
case '@':
return 'Digit2';
case '3':
case '#':
return 'Digit3';
case '4':
case '$':
return 'Digit4';
case '5':
case '%':
return 'Digit5';
case '6':
case '^':
return 'Digit6';
case '7':
case '&':
return 'Digit7';
case '8':
case '*':
return 'Digit8';
case '9':
case '(':
return 'Digit9';
case '=':
case '+':
return 'Equal';
// The spec declares the '<' to be `IntlBackslash` as well, but it is already covered
// in the `Comma` above.
case '>':
return 'IntlBackslash';
case 'a':
case 'A':
return 'KeyA';
case 'b':
case 'B':
return 'KeyB';
case 'c':
case 'C':
return 'KeyC';
case 'd':
case 'D':
return 'KeyD';
case 'e':
case 'E':
return 'KeyE';
case 'f':
case 'F':
return 'KeyF';
case 'g':
case 'G':
return 'KeyG';
case 'h':
case 'H':
return 'KeyH';
case 'i':
case 'I':
return 'KeyI';
case 'j':
case 'J':
return 'KeyJ';
case 'k':
case 'K':
return 'KeyK';
case 'l':
case 'L':
return 'KeyL';
case 'm':
case 'M':
return 'KeyM';
case 'n':
case 'N':
return 'KeyN';
case 'o':
case 'O':
return 'KeyO';
case 'p':
case 'P':
return 'KeyP';
case 'q':
case 'Q':
return 'KeyQ';
case 'r':
case 'R':
return 'KeyR';
case 's':
case 'S':
return 'KeyS';
case 't':
case 'T':
return 'KeyT';
case 'u':
case 'U':
return 'KeyU';
case 'v':
case 'V':
return 'KeyV';
case 'w':
case 'W':
return 'KeyW';
case 'x':
case 'X':
return 'KeyX';
case 'y':
case 'Y':
return 'KeyY';
case 'z':
case 'Z':
return 'KeyZ';
case '-':
case '_':
return 'Minus';
case '.':
return 'Period';
case "'":
case '"':
return 'Quote';
case ';':
case ':':
return 'Semicolon';
case '/':
case '?':
return 'Slash';
case '\uE00A':
return 'AltLeft';
case '\uE052':
return 'AltRight';
case '\uE009':
return 'ControlLeft';
case '\uE051':
return 'ControlRight';
case '\uE006':
return 'Enter';
case '\uE00B':
return 'Pause';
case '\uE03D':
return 'MetaLeft';
case '\uE053':
return 'MetaRight';
case '\uE008':
return 'ShiftLeft';
case '\uE050':
return 'ShiftRight';
case ' ':
case '\uE00D':
return 'Space';
case '\uE004':
return 'Tab';
case '\uE017':
return 'Delete';
case '\uE010':
return 'End';
case '\uE002':
return 'Help';
case '\uE011':
return 'Home';
case '\uE016':
return 'Insert';
case '\uE00F':
return 'PageDown';
case '\uE00E':
return 'PageUp';
case '\uE015':
return 'ArrowDown';
case '\uE012':
return 'ArrowLeft';
case '\uE014':
return 'ArrowRight';
case '\uE013':
return 'ArrowUp';
case '\uE00C':
return 'Escape';
case '\uE031':
return 'F1';
case '\uE032':
return 'F2';
case '\uE033':
return 'F3';
case '\uE034':
return 'F4';
case '\uE035':
return 'F5';
case '\uE036':
return 'F6';
case '\uE037':
return 'F7';
case '\uE038':
return 'F8';
case '\uE039':
return 'F9';
case '\uE03A':
return 'F10';
case '\uE03B':
return 'F11';
case '\uE03C':
return 'F12';
case '\uE019':
return 'NumpadEqual';
case '\uE01A':
case '\uE05C':
return 'Numpad0';
case '\uE01B':
case '\uE056':
return 'Numpad1';
case '\uE01C':
case '\uE05B':
return 'Numpad2';
case '\uE01D':
case '\uE055':
return 'Numpad3';
case '\uE01E':
case '\uE058':
return 'Numpad4';
case '\uE01F':
return 'Numpad5';
case '\uE020':
case '\uE05A':
return 'Numpad6';
case '\uE021':
case '\uE057':
return 'Numpad7';
case '\uE022':
case '\uE059':
return 'Numpad8';
case '\uE023':
case '\uE054':
return 'Numpad9';
case '\uE025':
return 'NumpadAdd';
case '\uE026':
return 'NumpadComma';
case '\uE028':
case '\uE05D':
return 'NumpadDecimal';
case '\uE029':
return 'NumpadDivide';
case '\uE007':
return 'NumpadEnter';
case '\uE024':
return 'NumpadMultiply';
case '\uE027':
return 'NumpadSubtract';
default:
return;
}
}
/**
* Returns the location of the key according to the table:
* https://w3c.github.io/webdriver/#dfn-key-location
*/
export function getKeyLocation(key) {
switch (key) {
case '\uE007':
case '\uE008':
case '\uE009':
case '\uE00A':
case '\uE03D':
return 1;
case '\uE019':
case '\uE01A':
case '\uE01B':
case '\uE01C':
case '\uE01D':
case '\uE01E':
case '\uE01F':
case '\uE020':
case '\uE021':
case '\uE022':
case '\uE023':
case '\uE024':
case '\uE025':
case '\uE026':
case '\uE027':
case '\uE028':
case '\uE029':
case '\uE054':
case '\uE055':
case '\uE056':
case '\uE057':
case '\uE058':
case '\uE059':
case '\uE05A':
case '\uE05B':
case '\uE05C':
case '\uE05D':
return 3;
case '\uE050':
case '\uE051':
case '\uE052':
case '\uE053':
return 2;
default:
return 0;
}
}
//# sourceMappingURL=keyUtils.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
import { type LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class LogManager {
#private;
private constructor();
static create(cdpTarget: CdpTarget, realmStorage: RealmStorage, eventManager: EventManager, logger?: LoggerFn): LogManager;
}
@@ -0,0 +1,183 @@
var _a;
import { ChromiumBidi } from '../../../protocol/protocol.js';
import { LogType } from '../../../utils/log.js';
import { getRemoteValuesText } from './logHelper.js';
/** Converts CDP StackTrace object to BiDi StackTrace object. */
function getBidiStackTrace(cdpStackTrace) {
const stackFrames = cdpStackTrace?.callFrames.map((callFrame) => {
return {
columnNumber: callFrame.columnNumber,
functionName: callFrame.functionName,
lineNumber: callFrame.lineNumber,
url: callFrame.url,
};
});
return stackFrames ? { callFrames: stackFrames } : undefined;
}
function getLogLevel(consoleApiType) {
if (["error" /* Log.Level.Error */, 'assert'].includes(consoleApiType)) {
return "error" /* Log.Level.Error */;
}
if (["debug" /* Log.Level.Debug */, 'trace'].includes(consoleApiType)) {
return "debug" /* Log.Level.Debug */;
}
if (["warn" /* Log.Level.Warn */, 'warning'].includes(consoleApiType)) {
return "warn" /* Log.Level.Warn */;
}
return "info" /* Log.Level.Info */;
}
function getLogMethod(consoleApiType) {
switch (consoleApiType) {
case 'warning':
return 'warn';
case 'startGroup':
return 'group';
case 'startGroupCollapsed':
return 'groupCollapsed';
case 'endGroup':
return 'groupEnd';
}
return consoleApiType;
}
export class LogManager {
#eventManager;
#realmStorage;
#cdpTarget;
#logger;
constructor(cdpTarget, realmStorage, eventManager, logger) {
this.#cdpTarget = cdpTarget;
this.#realmStorage = realmStorage;
this.#eventManager = eventManager;
this.#logger = logger;
}
static create(cdpTarget, realmStorage, eventManager, logger) {
const logManager = new _a(cdpTarget, realmStorage, eventManager, logger);
logManager.#initializeEntryAddedEventListener();
return logManager;
}
/**
* Heuristic serialization of CDP remote object. If possible, return the BiDi value
* without deep serialization.
*/
async #heuristicSerializeArg(arg, realm) {
switch (arg.type) {
// TODO: Implement regexp, array, object, map and set heuristics base on
// preview.
case 'undefined':
return { type: 'undefined' };
case 'boolean':
return { type: 'boolean', value: arg.value };
case 'string':
return { type: 'string', value: arg.value };
case 'number':
// The value can be either a number or a string like `Infinity` or `-0`.
return { type: 'number', value: arg.unserializableValue ?? arg.value };
case 'bigint':
if (arg.unserializableValue !== undefined &&
arg.unserializableValue[arg.unserializableValue.length - 1] === 'n') {
return {
type: arg.type,
value: arg.unserializableValue.slice(0, -1),
};
}
// Unexpected bigint value, fall back to CDP deep serialization.
break;
case 'object':
if (arg.subtype === 'null') {
return { type: 'null' };
}
// Fall back to CDP deep serialization.
break;
default:
// Fall back to CDP deep serialization.
break;
}
// Fall back to CDP deep serialization.
return await realm.serializeCdpObject(arg, "none" /* Script.ResultOwnership.None */);
}
#initializeEntryAddedEventListener() {
this.#cdpTarget.cdpClient.on('Runtime.consoleAPICalled', (params) => {
// Try to find realm by `cdpSessionId` and `executionContextId`,
// if provided.
const realm = this.#realmStorage.findRealm({
cdpSessionId: this.#cdpTarget.cdpSessionId,
executionContextId: params.executionContextId,
});
if (realm === undefined) {
// Ignore exceptions not attached to any realm.
this.#logger?.(LogType.cdp, params);
return;
}
const argsPromise = Promise.all(params.args.map((arg) => this.#heuristicSerializeArg(arg, realm)));
for (const browsingContext of realm.associatedBrowsingContexts) {
this.#eventManager.registerPromiseEvent(argsPromise.then((args) => ({
kind: 'success',
value: {
type: 'event',
method: ChromiumBidi.Log.EventNames.LogEntryAdded,
params: {
level: getLogLevel(params.type),
source: realm.source,
text: getRemoteValuesText(args, true),
timestamp: Math.round(params.timestamp),
stackTrace: getBidiStackTrace(params.stackTrace),
type: 'console',
method: getLogMethod(params.type),
args,
},
},
}), (error) => ({
kind: 'error',
error,
})), browsingContext.id, ChromiumBidi.Log.EventNames.LogEntryAdded);
}
});
this.#cdpTarget.cdpClient.on('Runtime.exceptionThrown', (params) => {
// Try to find realm by `cdpSessionId` and `executionContextId`,
// if provided.
const realm = this.#realmStorage.findRealm({
cdpSessionId: this.#cdpTarget.cdpSessionId,
executionContextId: params.exceptionDetails.executionContextId,
});
if (realm === undefined) {
// Ignore exceptions not attached to any realm.
this.#logger?.(LogType.cdp, params);
return;
}
for (const browsingContext of realm.associatedBrowsingContexts) {
this.#eventManager.registerPromiseEvent(_a.#getExceptionText(params, realm).then((text) => ({
kind: 'success',
value: {
type: 'event',
method: ChromiumBidi.Log.EventNames.LogEntryAdded,
params: {
level: "error" /* Log.Level.Error */,
source: realm.source,
text,
timestamp: Math.round(params.timestamp),
stackTrace: getBidiStackTrace(params.exceptionDetails.stackTrace),
type: 'javascript',
},
},
}), (error) => ({
kind: 'error',
error,
})), browsingContext.id, ChromiumBidi.Log.EventNames.LogEntryAdded);
}
});
}
/**
* Try the best to get the exception text.
*/
static async #getExceptionText(params, realm) {
if (!params.exceptionDetails.exception) {
return params.exceptionDetails.text;
}
if (realm === undefined) {
return JSON.stringify(params.exceptionDetails.exception);
}
return await realm.stringifyObject(params.exceptionDetails.exception);
}
}
_a = LogManager;
//# sourceMappingURL=LogManager.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,23 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Script } from '../../../protocol/protocol.js';
/**
* @param args input remote values to be format printed
* @return parsed text of the remote values in specific format
*/
export declare function logMessageFormatter(args: Script.RemoteValue[]): string;
export declare function getRemoteValuesText(args: Script.RemoteValue[], formatText: boolean): string;
@@ -0,0 +1,168 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from '../../../utils/assert.js';
const specifiers = ['%s', '%d', '%i', '%f', '%o', '%O', '%c'];
function isFormatSpecifier(str) {
return specifiers.some((spec) => str.includes(spec));
}
/**
* @param args input remote values to be format printed
* @return parsed text of the remote values in specific format
*/
export function logMessageFormatter(args) {
let output = '';
const argFormat = args[0].value.toString();
const argValues = args.slice(1, undefined);
const tokens = argFormat.split(new RegExp(specifiers.map((spec) => `(${spec})`).join('|'), 'g'));
for (const token of tokens) {
if (token === undefined || token === '') {
continue;
}
if (isFormatSpecifier(token)) {
const arg = argValues.shift();
// raise an exception when less value is provided
assert(arg, `Less value is provided: "${getRemoteValuesText(args, false)}"`);
if (token === '%s') {
output += stringFromArg(arg);
}
else if (token === '%d' || token === '%i') {
if (arg.type === 'bigint' ||
arg.type === 'number' ||
arg.type === 'string') {
output += parseInt(arg.value.toString(), 10);
}
else {
output += 'NaN';
}
}
else if (token === '%f') {
if (arg.type === 'bigint' ||
arg.type === 'number' ||
arg.type === 'string') {
output += parseFloat(arg.value.toString());
}
else {
output += 'NaN';
}
}
else {
// %o, %O, %c
output += toJson(arg);
}
}
else {
output += token;
}
}
// raise an exception when more value is provided
if (argValues.length > 0) {
throw new Error(`More value is provided: "${getRemoteValuesText(args, false)}"`);
}
return output;
}
/**
* @param arg input remote value to be parsed
* @return parsed text of the remote value
*
* input: {"type": "number", "value": 1}
* output: 1
*
* input: {"type": "string", "value": "abc"}
* output: "abc"
*
* input: {"type": "object", "value": [["id", {"type": "number", "value": 1}]]}
* output: '{"id": 1}'
*
* input: {"type": "object", "value": [["font-size", {"type": "string", "value": "20px"}]]}
* output: '{"font-size": "20px"}'
*/
function toJson(arg) {
// arg type validation
if (arg.type !== 'array' &&
arg.type !== 'bigint' &&
arg.type !== 'date' &&
arg.type !== 'number' &&
arg.type !== 'object' &&
arg.type !== 'string') {
return stringFromArg(arg);
}
if (arg.type === 'bigint') {
return `${arg.value.toString()}n`;
}
if (arg.type === 'number') {
return arg.value.toString();
}
if (['date', 'string'].includes(arg.type)) {
return JSON.stringify(arg.value);
}
if (arg.type === 'object') {
return `{${arg.value
.map((pair) => {
return `${JSON.stringify(pair[0])}:${toJson(pair[1])}`;
})
.join(',')}}`;
}
if (arg.type === 'array') {
return `[${arg.value?.map((val) => toJson(val)).join(',') ?? ''}]`;
}
throw Error(`Invalid value type: ${arg}`);
}
function stringFromArg(arg) {
if (!Object.hasOwn(arg, 'value')) {
return arg.type;
}
switch (arg.type) {
case 'string':
case 'number':
case 'boolean':
case 'bigint':
return String(arg.value);
case 'regexp':
return `/${arg.value.pattern}/${arg.value.flags ?? ''}`;
case 'date':
return new Date(arg.value).toString();
case 'object':
return `Object(${arg.value?.length ?? ''})`;
case 'array':
return `Array(${arg.value?.length ?? ''})`;
case 'map':
return `Map(${arg.value?.length})`;
case 'set':
return `Set(${arg.value?.length})`;
default:
return arg.type;
}
}
export function getRemoteValuesText(args, formatText) {
const arg = args[0];
if (!arg) {
return '';
}
// if args[0] is a format specifier, format the args as output
if (arg.type === 'string' &&
isFormatSpecifier(arg.value.toString()) &&
formatText) {
return logMessageFormatter(args);
}
// if args[0] is not a format specifier, just join the args with \u0020 (unicode 'SPACE')
return args
.map((arg) => {
return stringFromArg(arg);
})
.join('\u0020');
}
//# sourceMappingURL=logHelper.js.map
@@ -0,0 +1 @@
{"version":3,"file":"logHelper.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/log/logHelper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAC,MAAM,EAAC,MAAM,0BAA0B,CAAC;AAEhD,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAE9D,SAAS,iBAAiB,CAAC,GAAW;IACpC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAA0B;IAC5D,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,MAAM,SAAS,GAAI,IAAI,CAAC,CAAC,CAAmC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC9E,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAC5B,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CACjE,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YACxC,SAAS;QACX,CAAC;QACD,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC;YAC9B,iDAAiD;YACjD,MAAM,CACJ,GAAG,EACH,4BAA4B,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAChE,CAAC;YACF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC;YAC/B,CAAC;iBAAM,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC5C,IACE,GAAG,CAAC,IAAI,KAAK,QAAQ;oBACrB,GAAG,CAAC,IAAI,KAAK,QAAQ;oBACrB,GAAG,CAAC,IAAI,KAAK,QAAQ,EACrB,CAAC;oBACD,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;gBAC/C,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,KAAK,CAAC;gBAClB,CAAC;YACH,CAAC;iBAAM,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC1B,IACE,GAAG,CAAC,IAAI,KAAK,QAAQ;oBACrB,GAAG,CAAC,IAAI,KAAK,QAAQ;oBACrB,GAAG,CAAC,IAAI,KAAK,QAAQ,EACrB,CAAC;oBACD,MAAM,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;gBAC7C,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,KAAK,CAAC;gBAClB,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,aAAa;gBACb,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC;QAClB,CAAC;IACH,CAAC;IAED,iDAAiD;IACjD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,4BAA4B,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAChE,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,MAAM,CAAC,GAAuB;IACrC,sBAAsB;IACtB,IACE,GAAG,CAAC,IAAI,KAAK,OAAO;QACpB,GAAG,CAAC,IAAI,KAAK,QAAQ;QACrB,GAAG,CAAC,IAAI,KAAK,MAAM;QACnB,GAAG,CAAC,IAAI,KAAK,QAAQ;QACrB,GAAG,CAAC,IAAI,KAAK,QAAQ;QACrB,GAAG,CAAC,IAAI,KAAK,QAAQ,EACrB,CAAC;QACD,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC;IACpC,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,IAAK,GAAG,CAAC,KAAiB;aAC9B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACZ,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,CAAC,CAAC;aACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAClB,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,IAAI,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC;IACrE,CAAC;IAED,MAAM,KAAK,CAAC,uBAAuB,GAAG,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,aAAa,CAAC,GAAuB;IAC5C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;QACjC,OAAO,GAAG,CAAC,IAAI,CAAC;IAClB,CAAC;IAED,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,QAAQ,CAAC;QACd,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS,CAAC;QACf,KAAK,QAAQ;YACX,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,KAAK,QAAQ;YACX,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;QAC1D,KAAK,MAAM;YACT,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;QACxC,KAAK,QAAQ;YACX,OAAO,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,GAAG,CAAC;QAC9C,KAAK,OAAO;YACV,OAAO,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,GAAG,CAAC;QAC7C,KAAK,KAAK;YACR,OAAO,OAAO,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC;QACrC,KAAK,KAAK;YACR,OAAO,OAAO,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC;QAErC;YACE,OAAO,GAAG,CAAC,IAAI,CAAC;IACpB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,IAA0B,EAC1B,UAAmB;IAEnB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAEpB,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,8DAA8D;IAC9D,IACE,GAAG,CAAC,IAAI,KAAK,QAAQ;QACrB,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACvC,UAAU,EACV,CAAC;QACD,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,yFAAyF;IACzF,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACX,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC,CAAC;SACD,IAAI,CAAC,QAAQ,CAAC,CAAC;AACpB,CAAC"}
@@ -0,0 +1,13 @@
import type { Browser, BrowsingContext } from '../../../protocol/generated/webdriver-bidi.js';
import { Network } from '../../../protocol/generated/webdriver-bidi.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { NetworkRequest } from './NetworkRequest.js';
export declare class CollectorsStorage {
#private;
constructor(maxEncodedDataSize: number, logger?: LoggerFn);
addDataCollector(params: Network.AddDataCollectorParameters): `${string}-${string}-${string}-${string}-${string}`;
isCollected(requestId: Network.Request, dataType?: Network.DataType, collectorId?: string): boolean;
disownData(requestId: Network.Request, dataType: Network.DataType, collectorId?: string): void;
collectIfNeeded(request: NetworkRequest, dataType: Network.DataType, topLevelBrowsingContext: BrowsingContext.BrowsingContext, userContext: Browser.UserContext): void;
removeDataCollector(collectorId: Network.Collector): Network.Request[];
}
@@ -0,0 +1,149 @@
/*
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, NoSuchNetworkCollectorException, UnsupportedOperationException, } from '../../../protocol/ErrorResponse.js';
import { LogType } from '../../../utils/log.js';
import { uuidv4 } from '../../../utils/uuid.js';
export class CollectorsStorage {
#collectors = new Map();
#responseCollectors = new Map();
#requestBodyCollectors = new Map();
#maxEncodedDataSize;
#logger;
constructor(maxEncodedDataSize, logger) {
this.#maxEncodedDataSize = maxEncodedDataSize;
this.#logger = logger;
}
addDataCollector(params) {
if (params.maxEncodedDataSize < 1 ||
params.maxEncodedDataSize > this.#maxEncodedDataSize) {
// 200 MB is the default limit in CDP:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_network_agent.cc;drc=da1f749634c9a401cc756f36c2e6ce233e1c9b4d;l=133
throw new InvalidArgumentException(`Max encoded data size should be between 1 and ${this.#maxEncodedDataSize}`);
}
const collectorId = uuidv4();
this.#collectors.set(collectorId, params);
return collectorId;
}
isCollected(requestId, dataType, collectorId) {
if (collectorId !== undefined && !this.#collectors.has(collectorId)) {
throw new NoSuchNetworkCollectorException(`Unknown collector ${collectorId}`);
}
if (dataType === undefined) {
return (this.isCollected(requestId, "response" /* Network.DataType.Response */, collectorId) ||
this.isCollected(requestId, "request" /* Network.DataType.Request */, collectorId));
}
const requestToCollectorsMap = this.#getRequestToCollectorMap(dataType).get(requestId);
if (requestToCollectorsMap === undefined ||
requestToCollectorsMap.size === 0) {
return false;
}
if (collectorId === undefined) {
// There is at least 1 collector for the data.
return true;
}
if (!requestToCollectorsMap.has(collectorId)) {
return false;
}
return true;
}
#getRequestToCollectorMap(dataType) {
switch (dataType) {
case "response" /* Network.DataType.Response */:
return this.#responseCollectors;
case "request" /* Network.DataType.Request */:
return this.#requestBodyCollectors;
default:
throw new UnsupportedOperationException(`Unsupported data type ${dataType}`);
}
}
disownData(requestId, dataType, collectorId) {
const requestToCollectorsMap = this.#getRequestToCollectorMap(dataType);
if (collectorId !== undefined) {
requestToCollectorsMap.get(requestId)?.delete(collectorId);
}
if (collectorId === undefined ||
requestToCollectorsMap.get(requestId)?.size === 0) {
requestToCollectorsMap.delete(requestId);
}
}
#shouldCollectRequest(collectorId, request, dataType, topLevelBrowsingContext, userContext) {
const collector = this.#collectors.get(collectorId);
if (collector === undefined) {
throw new NoSuchNetworkCollectorException(`Unknown collector ${collectorId}`);
}
if (collector.userContexts &&
!collector.userContexts.includes(userContext)) {
// Collector is aimed for a different user context.
return false;
}
if (collector.contexts &&
!collector.contexts.includes(topLevelBrowsingContext)) {
// Collector is aimed for a different top-level browsing context.
return false;
}
if (!collector.dataTypes.includes(dataType)) {
// Collector is aimed for a different data type.
return false;
}
if (dataType === "request" /* Network.DataType.Request */ &&
request.bodySize > collector.maxEncodedDataSize) {
this.#logger?.(LogType.debug, `Request's ${request.id} body size is too big for the collector ${collectorId}`);
return false;
}
if (dataType === "response" /* Network.DataType.Response */ &&
request.encodedResponseBodySize > collector.maxEncodedDataSize) {
this.#logger?.(LogType.debug, `Request's ${request.id} response is too big for the collector ${collectorId}`);
return false;
}
this.#logger?.(LogType.debug, `Collector ${collectorId} collected ${dataType} of ${request.id}`);
return true;
}
collectIfNeeded(request, dataType, topLevelBrowsingContext, userContext) {
const collectorIds = [...this.#collectors.keys()].filter((collectorId) => this.#shouldCollectRequest(collectorId, request, dataType, topLevelBrowsingContext, userContext));
if (collectorIds.length > 0) {
this.#getRequestToCollectorMap(dataType).set(request.id, new Set(collectorIds));
}
}
removeDataCollector(collectorId) {
if (!this.#collectors.has(collectorId)) {
throw new NoSuchNetworkCollectorException(`Collector ${collectorId} does not exist`);
}
this.#collectors.delete(collectorId);
const affectedRequests = [];
// Clean up collected responses.
for (const [requestId, collectorIds] of this.#responseCollectors) {
if (collectorIds.has(collectorId)) {
collectorIds.delete(collectorId);
if (collectorIds.size === 0) {
this.#responseCollectors.delete(requestId);
affectedRequests.push(requestId);
}
}
}
for (const [requestId, collectorIds] of this.#requestBodyCollectors) {
if (collectorIds.has(collectorId)) {
collectorIds.delete(collectorId);
if (collectorIds.size === 0) {
this.#requestBodyCollectors.delete(requestId);
affectedRequests.push(requestId);
}
}
}
return affectedRequests;
}
}
//# sourceMappingURL=CollectorsStorage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"CollectorsStorage.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/network/CollectorsStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Protocol } from 'devtools-protocol';
import { Network, type EmptyResult } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { NetworkStorage } from './NetworkStorage.js';
import { type ParsedUrlPattern } from './NetworkUtils.js';
/** Dispatches Network module commands. */
export declare class NetworkProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, networkStorage: NetworkStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage);
addIntercept(params: Network.AddInterceptParameters): Promise<Network.AddInterceptResult>;
continueRequest(params: Network.ContinueRequestParameters): Promise<EmptyResult>;
continueResponse(params: Network.ContinueResponseParameters): Promise<EmptyResult>;
continueWithAuth(params: Network.ContinueWithAuthParameters): Promise<EmptyResult>;
failRequest({ request: networkId, }: Network.FailRequestParameters): Promise<EmptyResult>;
provideResponse(params: Network.ProvideResponseParameters): Promise<EmptyResult>;
removeIntercept(params: Network.RemoveInterceptParameters): Promise<EmptyResult>;
setCacheBehavior(params: Network.SetCacheBehaviorParameters): Promise<EmptyResult>;
/**
* Validate https://fetch.spec.whatwg.org/#header-value
*/
static validateHeaders(headers: Network.Header[]): void;
static isMethodValid(method: string): boolean;
/**
* Attempts to parse the given url.
* Throws an InvalidArgumentException if the url is invalid.
*/
static parseUrlString(url: string): URL;
static parseUrlPatterns(urlPatterns: Network.UrlPattern[]): ParsedUrlPattern[];
static wrapInterceptionError(error: any): any;
addDataCollector(params: Network.AddDataCollectorParameters): Promise<Network.AddDataCollectorResult>;
getData(params: Network.GetDataParameters): Promise<Network.GetDataResult>;
removeDataCollector(params: Network.RemoveDataCollectorParameters): Promise<EmptyResult>;
disownData(params: Network.DisownDataParameters): EmptyResult;
setExtraHeaders(params: Network.SetExtraHeadersParameters): Promise<EmptyResult>;
}
export declare function parseBiDiHeaders(headers: Network.Header[]): Protocol.Network.Headers;
@@ -0,0 +1,541 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NoSuchRequestException, InvalidArgumentException, UnsupportedOperationException, } from '../../../protocol/protocol.js';
import { isSpecialScheme } from './NetworkUtils.js';
/** Dispatches Network module commands. */
export class NetworkProcessor {
#browsingContextStorage;
#networkStorage;
#userContextStorage;
#contextConfigStorage;
constructor(browsingContextStorage, networkStorage, userContextStorage, contextConfigStorage) {
this.#userContextStorage = userContextStorage;
this.#browsingContextStorage = browsingContextStorage;
this.#networkStorage = networkStorage;
this.#contextConfigStorage = contextConfigStorage;
}
async addIntercept(params) {
this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
const urlPatterns = params.urlPatterns ?? [];
const parsedUrlPatterns = NetworkProcessor.parseUrlPatterns(urlPatterns);
const intercept = this.#networkStorage.addIntercept({
urlPatterns: parsedUrlPatterns,
phases: params.phases,
contexts: params.contexts,
});
// Adding interception may require enabling CDP Network domains.
await this.#toggleNetwork();
return {
intercept,
};
}
async continueRequest(params) {
if (params.url !== undefined) {
NetworkProcessor.parseUrlString(params.url);
}
if (params.method !== undefined) {
if (!NetworkProcessor.isMethodValid(params.method)) {
throw new InvalidArgumentException(`Method '${params.method}' is invalid.`);
}
}
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
]);
try {
await request.continueRequest(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async continueResponse(params) {
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"authRequired" /* Network.InterceptPhase.AuthRequired */,
"responseStarted" /* Network.InterceptPhase.ResponseStarted */,
]);
try {
await request.continueResponse(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async continueWithAuth(params) {
const networkId = params.request;
const request = this.#getBlockedRequestOrFail(networkId, [
"authRequired" /* Network.InterceptPhase.AuthRequired */,
]);
await request.continueWithAuth(params);
return {};
}
async failRequest({ request: networkId, }) {
const request = this.#getRequestOrFail(networkId);
if (request.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
throw new InvalidArgumentException(`Request '${networkId}' in 'authRequired' phase cannot be failed`);
}
if (!request.interceptPhase) {
throw new NoSuchRequestException(`No blocked request found for network id '${networkId}'`);
}
await request.failRequest('Failed');
return {};
}
async provideResponse(params) {
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
"responseStarted" /* Network.InterceptPhase.ResponseStarted */,
"authRequired" /* Network.InterceptPhase.AuthRequired */,
]);
try {
await request.provideResponse(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
/**
* In some states CDP Network and Fetch domains are not required, but in some they have
* to be updated. Whenever potential change in these kinds of states is introduced,
* update the states of all the CDP targets.
*/
async #toggleNetwork() {
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleNetwork();
}));
}
async removeIntercept(params) {
this.#networkStorage.removeIntercept(params.intercept);
// Removing interception may allow for disabling CDP Network domains.
await this.#toggleNetwork();
return {};
}
async setCacheBehavior(params) {
const contexts = this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
// Change all targets
if (contexts.size === 0) {
this.#networkStorage.defaultCacheBehavior = params.cacheBehavior;
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleSetCacheDisabled();
}));
return {};
}
const cacheDisabled = params.cacheBehavior === 'bypass';
await Promise.all([...contexts.values()].map((context) => {
return context.cdpTarget.toggleSetCacheDisabled(cacheDisabled);
}));
return {};
}
#getRequestOrFail(id) {
const request = this.#networkStorage.getRequestById(id);
if (!request) {
throw new NoSuchRequestException(`Network request with ID '${id}' doesn't exist`);
}
return request;
}
#getBlockedRequestOrFail(id, phases) {
const request = this.#getRequestOrFail(id);
if (!request.interceptPhase) {
throw new NoSuchRequestException(`No blocked request found for network id '${id}'`);
}
if (request.interceptPhase && !phases.includes(request.interceptPhase)) {
throw new InvalidArgumentException(`Blocked request for network id '${id}' is in '${request.interceptPhase}' phase`);
}
return request;
}
/**
* Validate https://fetch.spec.whatwg.org/#header-value
*/
static validateHeaders(headers) {
for (const header of headers) {
let headerValue;
if (header.value.type === 'string') {
headerValue = header.value.value;
}
else {
headerValue = atob(header.value.value);
}
if (headerValue !== headerValue.trim() ||
headerValue.includes('\n') ||
headerValue.includes('\0')) {
throw new InvalidArgumentException(`Header value '${headerValue}' is not acceptable value`);
}
}
}
static isMethodValid(method) {
// https://httpwg.org/specs/rfc9110.html#method.overview
return /^[!#$%&'*+\-.^_`|~a-zA-Z\d]+$/.test(method);
}
/**
* Attempts to parse the given url.
* Throws an InvalidArgumentException if the url is invalid.
*/
static parseUrlString(url) {
try {
return new URL(url);
}
catch (error) {
throw new InvalidArgumentException(`Invalid URL '${url}': ${error}`);
}
}
static parseUrlPatterns(urlPatterns) {
return urlPatterns.map((urlPattern) => {
let patternUrl = '';
let hasProtocol = true;
let hasHostname = true;
let hasPort = true;
let hasPathname = true;
let hasSearch = true;
switch (urlPattern.type) {
case 'string': {
patternUrl = unescapeURLPattern(urlPattern.pattern);
break;
}
case 'pattern': {
if (urlPattern.protocol === undefined) {
hasProtocol = false;
patternUrl += 'http';
}
else {
if (urlPattern.protocol === '') {
throw new InvalidArgumentException('URL pattern must specify a protocol');
}
urlPattern.protocol = unescapeURLPattern(urlPattern.protocol);
if (!urlPattern.protocol.match(/^[a-zA-Z+-.]+$/)) {
throw new InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.protocol;
}
const scheme = patternUrl.toLocaleLowerCase();
patternUrl += ':';
if (isSpecialScheme(scheme)) {
patternUrl += '//';
}
if (urlPattern.hostname === undefined) {
if (scheme !== 'file') {
patternUrl += 'placeholder';
}
hasHostname = false;
}
else {
if (urlPattern.hostname === '') {
throw new InvalidArgumentException('URL pattern must specify a hostname');
}
if (urlPattern.protocol === 'file') {
throw new InvalidArgumentException(`URL pattern protocol cannot be 'file'`);
}
urlPattern.hostname = unescapeURLPattern(urlPattern.hostname);
let insideBrackets = false;
for (const c of urlPattern.hostname) {
if (c === '/' || c === '?' || c === '#') {
throw new InvalidArgumentException(`'/', '?', '#' are forbidden in hostname`);
}
if (!insideBrackets && c === ':') {
throw new InvalidArgumentException(`':' is only allowed inside brackets in hostname`);
}
if (c === '[') {
insideBrackets = true;
}
if (c === ']') {
insideBrackets = false;
}
}
patternUrl += urlPattern.hostname;
}
if (urlPattern.port === undefined) {
hasPort = false;
}
else {
if (urlPattern.port === '') {
throw new InvalidArgumentException(`URL pattern must specify a port`);
}
urlPattern.port = unescapeURLPattern(urlPattern.port);
patternUrl += ':';
if (!urlPattern.port.match(/^\d+$/)) {
throw new InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.port;
}
if (urlPattern.pathname === undefined) {
hasPathname = false;
}
else {
urlPattern.pathname = unescapeURLPattern(urlPattern.pathname);
if (urlPattern.pathname[0] !== '/') {
patternUrl += '/';
}
if (urlPattern.pathname.includes('#') ||
urlPattern.pathname.includes('?')) {
throw new InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.pathname;
}
if (urlPattern.search === undefined) {
hasSearch = false;
}
else {
urlPattern.search = unescapeURLPattern(urlPattern.search);
if (urlPattern.search[0] !== '?') {
patternUrl += '?';
}
if (urlPattern.search.includes('#')) {
throw new InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.search;
}
break;
}
}
const serializePort = (url) => {
const defaultPorts = {
'ftp:': 21,
'file:': null,
'http:': 80,
'https:': 443,
'ws:': 80,
'wss:': 443,
};
if (isSpecialScheme(url.protocol) &&
defaultPorts[url.protocol] !== null &&
(!url.port || String(defaultPorts[url.protocol]) === url.port)) {
return '';
}
else if (url.port) {
return url.port;
}
return undefined;
};
try {
const url = new URL(patternUrl);
return {
protocol: hasProtocol ? url.protocol.replace(/:$/, '') : undefined,
hostname: hasHostname ? url.hostname : undefined,
port: hasPort ? serializePort(url) : undefined,
pathname: hasPathname && url.pathname ? url.pathname : undefined,
search: hasSearch ? url.search : undefined,
};
}
catch (err) {
throw new InvalidArgumentException(`${err.message} '${patternUrl}'`);
}
});
}
static wrapInterceptionError(error) {
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/fetch_handler.cc;l=169
if (error?.message.includes('Invalid header') ||
error?.message.includes('Unsafe header')) {
return new InvalidArgumentException(error.message);
}
return error;
}
async addDataCollector(params) {
if (params.userContexts !== undefined && params.contexts !== undefined) {
throw new InvalidArgumentException("'contexts' and 'userContexts' are mutually exclusive");
}
if (params.userContexts !== undefined) {
// Assert the user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(params.userContexts);
}
if (params.contexts !== undefined) {
for (const browsingContextId of params.contexts) {
// Assert the browsing context exists and are top-level.
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new InvalidArgumentException(`Data collectors are available only on top-level browsing contexts`);
}
}
}
const collectorId = this.#networkStorage.addDataCollector(params);
// Adding data collectors may require enabling CDP Network domains.
await this.#toggleNetwork();
return { collector: collectorId };
}
async getData(params) {
return await this.#networkStorage.getCollectedData(params);
}
async removeDataCollector(params) {
this.#networkStorage.removeDataCollector(params);
// Removing data collectors may allow disabling CDP Network domains.
await this.#toggleNetwork();
return {};
}
disownData(params) {
this.#networkStorage.disownData(params);
return {};
}
async #getRelatedTopLevelBrowsingContexts(browsingContextIds, userContextIds) {
// Duplicated with EmulationProcessor logic. Consider moving to ConfigStorage.
if (browsingContextIds === undefined && userContextIds === undefined) {
return this.#browsingContextStorage.getTopLevelContexts();
}
if (browsingContextIds !== undefined && userContextIds !== undefined) {
throw new InvalidArgumentException('User contexts and browsing contexts are mutually exclusive');
}
const result = [];
if (userContextIds !== undefined) {
if (userContextIds.length === 0) {
throw new InvalidArgumentException('user context should be provided');
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
}
if (browsingContextIds !== undefined) {
if (browsingContextIds.length === 0) {
throw new InvalidArgumentException('browsing context should be provided');
}
for (const browsingContextId of browsingContextIds) {
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new InvalidArgumentException('The command is only supported on the top-level context');
}
result.push(browsingContext);
}
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async setExtraHeaders(params) {
const affectedBrowsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
const cdpExtraHeaders = parseBiDiHeaders(params.headers);
if (params.userContexts === undefined && params.contexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
extraHeaders: cdpExtraHeaders,
});
}
if (params.userContexts !== undefined) {
params.userContexts.forEach((userContext) => {
this.#contextConfigStorage.updateUserContextConfig(userContext, {
extraHeaders: cdpExtraHeaders,
});
});
}
if (params.contexts !== undefined) {
params.contexts.forEach((browsingContextId) => {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, { extraHeaders: cdpExtraHeaders });
});
}
await Promise.all(affectedBrowsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing setting.
const extraHeaders = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext).extraHeaders ?? {};
await context.setExtraHeaders(extraHeaders);
}));
return {};
}
}
/**
* See https://w3c.github.io/webdriver-bidi/#unescape-url-pattern
*/
function unescapeURLPattern(pattern) {
const forbidden = new Set(['(', ')', '*', '{', '}']);
let result = '';
let isEscaped = false;
for (const c of pattern) {
if (!isEscaped) {
if (forbidden.has(c)) {
throw new InvalidArgumentException('Forbidden characters');
}
if (c === '\\') {
isEscaped = true;
continue;
}
}
result += c;
isEscaped = false;
}
return result;
}
// https://fetch.spec.whatwg.org/#header-name
const FORBIDDEN_HEADER_NAME_SYMBOLS = new Set([
' ',
'\t',
'\n',
'"',
'(',
')',
',',
'/',
':',
';',
'<',
'=',
'>',
'?',
'@',
'[',
'\\',
']',
'{',
'}',
]);
// https://fetch.spec.whatwg.org/#header-value
const FORBIDDEN_HEADER_VALUE_SYMBOLS = new Set(['\0', '\n', '\r']);
function includesChar(str, chars) {
for (const char of str) {
if (chars.has(char)) {
return true;
}
}
return false;
}
// Export for testing.
export function parseBiDiHeaders(headers) {
const parsedHeaders = {};
for (const bidiHeader of headers) {
if (bidiHeader.value.type === 'string') {
const name = bidiHeader.name;
const value = bidiHeader.value.value;
if (name.length === 0) {
throw new InvalidArgumentException(`Empty header name is not allowed`);
}
if (includesChar(name, FORBIDDEN_HEADER_NAME_SYMBOLS)) {
throw new InvalidArgumentException(`Header name '${name}' contains forbidden symbols`);
}
if (includesChar(value, FORBIDDEN_HEADER_VALUE_SYMBOLS)) {
throw new InvalidArgumentException(`Header value '${value}' contains forbidden symbols`);
}
if (value.trim() !== value) {
throw new InvalidArgumentException(`Header value should not contain trailing or ending whitespaces`);
}
// BiDi spec does not combine but overrides the headers with the same names.
// https://www.w3.org/TR/webdriver-bidi/#update-headers
parsedHeaders[bidiHeader.name] = bidiHeader.value.value;
}
else {
throw new UnsupportedOperationException('Only string headers values are supported');
}
}
return parsedHeaders;
}
//# sourceMappingURL=NetworkProcessor.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,55 @@
/**
* @fileoverview `NetworkRequest` represents a single network request and keeps
* track of all the related CDP events.
*/
import type { Protocol } from 'devtools-protocol';
import { Network } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { EventManager } from '../session/EventManager.js';
import type { NetworkStorage } from './NetworkStorage.js';
/** Abstracts one individual network request. */
export declare class NetworkRequest {
#private;
static unknownParameter: string;
waitNextPhase: Deferred<void>;
constructor(id: Network.Request, eventManager: EventManager, networkStorage: NetworkStorage, cdpTarget: CdpTarget, redirectCount?: number, logger?: LoggerFn);
get id(): string;
get fetchId(): string | undefined;
/**
* When blocked returns the phase for it
*/
get interceptPhase(): Network.InterceptPhase | undefined;
get url(): string;
get redirectCount(): number;
get cdpTarget(): CdpTarget;
/** CdpTarget can be changed when frame is moving out of process. */
updateCdpTarget(cdpTarget: CdpTarget): void;
get cdpClient(): import("../../BidiMapper.js").CdpClient;
isRedirecting(): boolean;
get bodySize(): number;
handleRedirect(event: Protocol.Network.RequestWillBeSentEvent): void;
onRequestWillBeSentEvent(event: Protocol.Network.RequestWillBeSentEvent): void;
onRequestWillBeSentExtraInfoEvent(event: Protocol.Network.RequestWillBeSentExtraInfoEvent): void;
onResponseReceivedExtraInfoEvent(event: Protocol.Network.ResponseReceivedExtraInfoEvent): void;
onResponseReceivedEvent(event: Protocol.Network.ResponseReceivedEvent): void;
onServedFromCache(): void;
onLoadingFinishedEvent(event: Protocol.Network.LoadingFinishedEvent): void;
onDataReceivedEvent(event: Protocol.Network.DataReceivedEvent): void;
onLoadingFailedEvent(event: Protocol.Network.LoadingFailedEvent): void;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-failRequest */
failRequest(errorReason: Protocol.Network.ErrorReason): Promise<void>;
onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void;
onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueRequest */
continueRequest(overrides?: Omit<Network.ContinueRequestParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueResponse */
continueResponse(overrides?: Omit<Network.ContinueResponseParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueWithAuth */
continueWithAuth(authChallenge: Omit<Network.ContinueWithAuthParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-provideResponse */
provideResponse(overrides: Omit<Network.ProvideResponseParameters, 'request'>): Promise<void>;
dispose(): void;
get encodedResponseBodySize(): number;
}
@@ -0,0 +1,890 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
var _a;
import { ChromiumBidi, } from '../../../protocol/protocol.js';
import { assert } from '../../../utils/assert.js';
import { DefaultMap } from '../../../utils/DefaultMap.js';
import { Deferred } from '../../../utils/Deferred.js';
import { LogType } from '../../../utils/log.js';
import { bidiBodySizeFromCdpPostDataEntries, bidiNetworkHeadersFromCdpNetworkHeaders, cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction, cdpFetchHeadersFromBidiNetworkHeaders, cdpToBiDiCookie, computeHeadersSize, getTiming, networkHeaderFromCookieHeaders, stringToBase64, } from './NetworkUtils.js';
const REALM_REGEX = /(?<=realm=").*(?=")/;
/** Abstracts one individual network request. */
export class NetworkRequest {
static unknownParameter = 'UNKNOWN';
/**
* Each network request has an associated request id, which is a string
* uniquely identifying that request.
*
* The identifier for a request resulting from a redirect matches that of the
* request that initiated it.
*/
#id;
#fetchId;
/**
* Indicates the network intercept phase, if the request is currently blocked.
* Undefined necessarily implies that the request is not blocked.
*/
#interceptPhase;
#servedFromCache = false;
#redirectCount;
#request = {};
#requestOverrides;
#responseOverrides;
#response = {
decodedSize: 0,
encodedSize: 0,
};
#eventManager;
#networkStorage;
#cdpTarget;
#logger;
#emittedEvents = {
[ChromiumBidi.Network.EventNames.AuthRequired]: false,
[ChromiumBidi.Network.EventNames.BeforeRequestSent]: false,
[ChromiumBidi.Network.EventNames.FetchError]: false,
[ChromiumBidi.Network.EventNames.ResponseCompleted]: false,
[ChromiumBidi.Network.EventNames.ResponseStarted]: false,
};
waitNextPhase = new Deferred();
constructor(id, eventManager, networkStorage, cdpTarget, redirectCount = 0, logger) {
this.#id = id;
this.#eventManager = eventManager;
this.#networkStorage = networkStorage;
this.#cdpTarget = cdpTarget;
this.#redirectCount = redirectCount;
this.#logger = logger;
}
get id() {
return this.#id;
}
get fetchId() {
return this.#fetchId;
}
/**
* When blocked returns the phase for it
*/
get interceptPhase() {
return this.#interceptPhase;
}
get url() {
const fragment = this.#request.info?.request.urlFragment ??
this.#request.paused?.request.urlFragment ??
'';
const url = this.#response.paused?.request.url ??
this.#requestOverrides?.url ??
this.#response.info?.url ??
this.#request.auth?.request.url ??
this.#request.info?.request.url ??
this.#request.paused?.request.url ??
_a.unknownParameter;
return `${url}${fragment}`;
}
get redirectCount() {
return this.#redirectCount;
}
get cdpTarget() {
return this.#cdpTarget;
}
/** CdpTarget can be changed when frame is moving out of process. */
updateCdpTarget(cdpTarget) {
if (cdpTarget !== this.#cdpTarget) {
this.#logger?.(LogType.debugInfo, `Request ${this.id} was moved from ${this.#cdpTarget.id} to ${cdpTarget.id}`);
this.#cdpTarget = cdpTarget;
}
}
get cdpClient() {
return this.#cdpTarget.cdpClient;
}
isRedirecting() {
return Boolean(this.#request.info);
}
#isDataUrl() {
return this.url.startsWith('data:');
}
#isNonInterceptable() {
return (
// We can't intercept data urls from CDP
this.#isDataUrl() ||
// Cached requests never hit the network
this.#servedFromCache);
}
get #method() {
return (this.#requestOverrides?.method ??
this.#request.info?.request.method ??
this.#request.paused?.request.method ??
this.#request.auth?.request.method ??
this.#response.paused?.request.method);
}
get #navigationId() {
// Heuristic to determine if this is a navigation request, and if not return null.
if (!this.#request.info ||
!this.#request.info.loaderId ||
// When we navigate all CDP network events have `loaderId`
// CDP's `loaderId` and `requestId` match when
// that request triggered the loading
this.#request.info.loaderId !== this.#request.info.requestId) {
return null;
}
// Get virtual navigation ID from the browsing context.
return this.#networkStorage.getNavigationId(this.#context ?? undefined);
}
get #cookies() {
let cookies = [];
if (this.#request.extraInfo) {
cookies = this.#request.extraInfo.associatedCookies
.filter(({ blockedReasons }) => {
return !Array.isArray(blockedReasons) || blockedReasons.length === 0;
})
.map(({ cookie }) => cdpToBiDiCookie(cookie));
}
return cookies;
}
#getBodySizeFromHeaders(headers) {
if (headers === undefined) {
return undefined;
}
if (headers['Content-Length'] !== undefined) {
const bodySize = Number.parseInt(headers['Content-Length']);
if (Number.isInteger(bodySize)) {
return bodySize;
}
this.#logger?.(LogType.debugError, "Unexpected non-integer 'Content-Length' header");
}
// TODO: process `Transfer-Encoding: chunked` case properly.
return undefined;
}
get bodySize() {
if (typeof this.#requestOverrides?.bodySize === 'number') {
return this.#requestOverrides.bodySize;
}
if (this.#request.info?.request.postDataEntries !== undefined) {
return bidiBodySizeFromCdpPostDataEntries(this.#request.info?.request.postDataEntries);
}
// Try to guess the body size based on the `Content-Length` header.
return (this.#getBodySizeFromHeaders(this.#request.info?.request.headers) ??
this.#getBodySizeFromHeaders(this.#request.extraInfo?.headers) ??
0);
}
get #context() {
const result = this.#response.paused?.frameId ??
this.#request.info?.frameId ??
this.#request.paused?.frameId ??
this.#request.auth?.frameId;
if (result !== undefined) {
return result;
}
// Heuristic for associating a preflight request with context via it's initiator
// request. Useful for preflight requests.
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/3570
if (this.#request?.info?.initiator.type === 'preflight' &&
this.#request?.info?.initiator.requestId !== undefined) {
const maybeInitiator = this.#networkStorage.getRequestById(this.#request?.info?.initiator.requestId);
if (maybeInitiator !== undefined) {
return maybeInitiator.#request.info?.frameId ?? null;
}
}
return null;
}
/** Returns the HTTP status code associated with this request if any. */
get #statusCode() {
return (this.#responseOverrides?.statusCode ??
this.#response.paused?.responseStatusCode ??
this.#response.extraInfo?.statusCode ??
this.#response.info?.status);
}
get #requestHeaders() {
let headers = [];
if (this.#requestOverrides?.headers) {
const headerMap = new DefaultMap(() => []);
for (const header of this.#requestOverrides.headers) {
headerMap.get(header.name).push(header.value.value);
}
for (const [name, value] of headerMap.entries()) {
headers.push({
name,
value: {
type: 'string',
value: value.join('\n').trimEnd(),
},
});
}
}
else {
headers = [
...bidiNetworkHeadersFromCdpNetworkHeaders(this.#request.info?.request.headers),
...bidiNetworkHeadersFromCdpNetworkHeaders(this.#request.extraInfo?.headers),
];
}
return headers;
}
get #authChallenges() {
// TODO: get headers from Fetch.requestPaused
if (!this.#response.info) {
return;
}
if (!(this.#statusCode === 401 || this.#statusCode === 407)) {
return undefined;
}
const headerName = this.#statusCode === 401 ? 'WWW-Authenticate' : 'Proxy-Authenticate';
const authChallenges = [];
for (const [header, value] of Object.entries(this.#response.info.headers)) {
// TODO: Do a proper match based on https://httpwg.org/specs/rfc9110.html#credentials
// Or verify this works
if (header.localeCompare(headerName, undefined, { sensitivity: 'base' }) === 0) {
authChallenges.push({
scheme: value.split(' ').at(0) ?? '',
realm: value.match(REALM_REGEX)?.at(0) ?? '',
});
}
}
return authChallenges;
}
get #timings() {
// The timing in the CDP events are provided relative to the event's baseline.
// However, the baseline can be different for different events, and the events have to
// be normalized throughout resource events. Normalize events timestamps by the
// request.
// TODO: Verify this is correct.
const responseTimeOffset = getTiming(getTiming(this.#response.info?.timing?.requestTime) -
getTiming(this.#request.info?.timestamp));
return {
// TODO: Verify this is correct
timeOrigin: Math.round(getTiming(this.#request.info?.wallTime) * 1000),
// Timing baseline.
// TODO: Verify this is correct.
requestTime: 0,
// TODO: set if redirect detected.
redirectStart: 0,
// TODO: set if redirect detected.
redirectEnd: 0,
// TODO: Verify this is correct
// https://source.chromium.org/chromium/chromium/src/+/main:net/base/load_timing_info.h;l=145
fetchStart: getTiming(this.#response.info?.timing?.workerFetchStart, responseTimeOffset),
// fetchStart: 0,
dnsStart: getTiming(this.#response.info?.timing?.dnsStart, responseTimeOffset),
dnsEnd: getTiming(this.#response.info?.timing?.dnsEnd, responseTimeOffset),
connectStart: getTiming(this.#response.info?.timing?.connectStart, responseTimeOffset),
connectEnd: getTiming(this.#response.info?.timing?.connectEnd, responseTimeOffset),
tlsStart: getTiming(this.#response.info?.timing?.sslStart, responseTimeOffset),
requestStart: getTiming(this.#response.info?.timing?.sendStart, responseTimeOffset),
// https://source.chromium.org/chromium/chromium/src/+/main:net/base/load_timing_info.h;l=196
responseStart: getTiming(this.#response.info?.timing?.receiveHeadersStart, responseTimeOffset),
responseEnd: getTiming(this.#response.info?.timing?.receiveHeadersEnd, responseTimeOffset),
};
}
#phaseChanged() {
this.waitNextPhase.resolve();
this.waitNextPhase = new Deferred();
}
#interceptsInPhase(phase) {
if (this.#isNonInterceptable() ||
!this.#cdpTarget.isSubscribedTo(`network.${phase}`)) {
return new Set();
}
return this.#networkStorage.getInterceptsForPhase(this, phase);
}
#isBlockedInPhase(phase) {
return this.#interceptsInPhase(phase).size > 0;
}
handleRedirect(event) {
// TODO: use event.redirectResponse;
// Temporary workaround to emit ResponseCompleted event for redirects
this.#response.hasExtraInfo = false;
this.#response.decodedSize = 0;
this.#response.encodedSize = 0;
this.#response.info = event.redirectResponse;
this.#emitEventsIfReady({
wasRedirected: true,
});
}
#emitEventsIfReady(options = {}) {
const requestExtraInfoCompleted =
// Flush redirects
options.wasRedirected ||
Boolean(this.#response.loadingFailed) ||
this.#isDataUrl() ||
Boolean(this.#request.extraInfo) ||
// If the request is intercepted during the `authRequired` phase, there
// will be no `Network.requestWillBeSentExtraInfo` CDP events.
this.#isBlockedInPhase("authRequired" /* Network.InterceptPhase.AuthRequired */) ||
// Requests from cache don't have extra info
this.#servedFromCache ||
// Sometimes there is no extra info and the response
// is the only place we can find out
Boolean(this.#response.info && !this.#response.hasExtraInfo);
const noInterceptionExpected = this.#isNonInterceptable();
const requestInterceptionExpected = !noInterceptionExpected &&
this.#isBlockedInPhase("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */);
const requestInterceptionCompleted = !requestInterceptionExpected ||
(requestInterceptionExpected && Boolean(this.#request.paused));
if (Boolean(this.#request.info) &&
(requestInterceptionExpected
? requestInterceptionCompleted
: requestExtraInfoCompleted)) {
this.#emitEvent(this.#getBeforeRequestEvent.bind(this));
}
const responseExtraInfoCompleted = Boolean(this.#response.extraInfo) ||
// Response from cache don't have extra info
this.#servedFromCache ||
// Don't expect extra info if the flag is false
Boolean(this.#response.info && !this.#response.hasExtraInfo);
const responseInterceptionExpected = !noInterceptionExpected &&
this.#isBlockedInPhase("responseStarted" /* Network.InterceptPhase.ResponseStarted */);
if (this.#response.info ||
(responseInterceptionExpected && Boolean(this.#response.paused))) {
this.#emitEvent(this.#getResponseStartedEvent.bind(this));
}
const responseInterceptionCompleted = !responseInterceptionExpected ||
(responseInterceptionExpected && Boolean(this.#response.paused));
const loadingFinished = Boolean(this.#response.loadingFailed) ||
Boolean(this.#response.loadingFinished);
if (Boolean(this.#response.info) &&
responseExtraInfoCompleted &&
responseInterceptionCompleted &&
(loadingFinished || options.wasRedirected)) {
this.#emitEvent(this.#getResponseReceivedEvent.bind(this));
this.#networkStorage.disposeRequest(this.id);
}
}
onRequestWillBeSentEvent(event) {
this.#request.info = event;
this.#networkStorage.collectIfNeeded(this, "request" /* Network.DataType.Request */);
this.#emitEventsIfReady();
}
onRequestWillBeSentExtraInfoEvent(event) {
this.#request.extraInfo = event;
this.#emitEventsIfReady();
}
onResponseReceivedExtraInfoEvent(event) {
if (event.statusCode >= 300 &&
event.statusCode <= 399 &&
this.#request.info &&
event.headers['location'] === this.#request.info.request.url) {
// We received the Response Extra info for the redirect
// Too late so we need to skip it as it will
// fire wrongly for the last one
return;
}
this.#response.extraInfo = event;
this.#emitEventsIfReady();
}
onResponseReceivedEvent(event) {
this.#response.hasExtraInfo = event.hasExtraInfo;
this.#response.info = event.response;
this.#networkStorage.collectIfNeeded(this, "response" /* Network.DataType.Response */);
this.#emitEventsIfReady();
}
onServedFromCache() {
this.#servedFromCache = true;
this.#emitEventsIfReady();
}
onLoadingFinishedEvent(event) {
this.#response.loadingFinished = event;
this.#emitEventsIfReady();
}
onDataReceivedEvent(event) {
this.#response.decodedSize += event.dataLength;
this.#response.encodedSize += event.encodedDataLength;
}
onLoadingFailedEvent(event) {
this.#response.loadingFailed = event;
this.#emitEventsIfReady();
this.#emitEvent(() => {
return {
method: ChromiumBidi.Network.EventNames.FetchError,
params: {
...this.#getBaseEventParams(),
errorText: event.errorText,
},
};
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-failRequest */
async failRequest(errorReason) {
assert(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.failRequest', {
requestId: this.#fetchId,
errorReason,
});
this.#interceptPhase = undefined;
}
onRequestPaused(event) {
this.#fetchId = event.requestId;
// CDP https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused
if (event.responseStatusCode || event.responseErrorReason) {
this.#response.paused = event;
if (this.#isBlockedInPhase("responseStarted" /* Network.InterceptPhase.ResponseStarted */) &&
// CDP may emit multiple events for a single request
!this.#emittedEvents[ChromiumBidi.Network.EventNames.ResponseStarted] &&
// Continue all response that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "responseStarted" /* Network.InterceptPhase.ResponseStarted */;
}
else {
void this.#continueResponse();
}
}
else {
this.#request.paused = event;
if (this.#isBlockedInPhase("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */) &&
// CDP may emit multiple events for a single request
!this.#emittedEvents[ChromiumBidi.Network.EventNames.BeforeRequestSent] &&
// Continue all requests that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */;
}
else {
void this.#continueRequest();
}
}
this.#emitEventsIfReady();
}
onAuthRequired(event) {
this.#fetchId = event.requestId;
this.#request.auth = event;
if (this.#isBlockedInPhase("authRequired" /* Network.InterceptPhase.AuthRequired */) &&
// Continue all auth requests that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "authRequired" /* Network.InterceptPhase.AuthRequired */;
// Make sure the `network.beforeRequestSent` is emitted before
// `network.authRequired`.
this.#emitEventsIfReady();
}
else {
void this.#continueWithAuth({
response: 'Default',
});
}
this.#emitEvent(() => {
return {
method: ChromiumBidi.Network.EventNames.AuthRequired,
params: {
...this.#getBaseEventParams("authRequired" /* Network.InterceptPhase.AuthRequired */),
response: this.#getResponseEventParams(),
},
};
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueRequest */
async continueRequest(overrides = {}) {
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const headers = cdpFetchHeadersFromBidiNetworkHeaders(overrideHeaders);
const postData = getCdpBodyFromBiDiBytesValue(overrides.body);
await this.#continueRequest({
url: overrides.url,
method: overrides.method,
headers,
postData,
});
this.#requestOverrides = {
url: overrides.url,
method: overrides.method,
headers: overrides.headers,
cookies: overrides.cookies,
bodySize: getSizeFromBiDiBytesValue(overrides.body),
};
}
async #continueRequest(overrides = {}) {
assert(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueRequest', {
requestId: this.#fetchId,
url: overrides.url,
method: overrides.method,
headers: overrides.headers,
postData: overrides.postData,
});
this.#interceptPhase = undefined;
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueResponse */
async continueResponse(overrides = {}) {
if (this.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
if (overrides.credentials) {
await Promise.all([
this.waitNextPhase,
await this.#continueWithAuth({
response: 'ProvideCredentials',
username: overrides.credentials.username,
password: overrides.credentials.password,
}),
]);
}
else {
// We need to use `ProvideCredentials`
// As `Default` may cancel the request
return await this.#continueWithAuth({
response: 'ProvideCredentials',
});
}
}
if (this.#interceptPhase === "responseStarted" /* Network.InterceptPhase.ResponseStarted */) {
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const responseHeaders = cdpFetchHeadersFromBidiNetworkHeaders(overrideHeaders);
await this.#continueResponse({
responseCode: overrides.statusCode ?? this.#response.paused?.responseStatusCode,
responsePhrase: overrides.reasonPhrase ?? this.#response.paused?.responseStatusText,
responseHeaders: responseHeaders ?? this.#response.paused?.responseHeaders,
});
this.#responseOverrides = {
statusCode: overrides.statusCode,
headers: overrideHeaders,
};
}
}
async #continueResponse({ responseCode, responsePhrase, responseHeaders, } = {}) {
assert(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueResponse', {
requestId: this.#fetchId,
responseCode,
responsePhrase,
responseHeaders,
});
this.#interceptPhase = undefined;
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueWithAuth */
async continueWithAuth(authChallenge) {
let username;
let password;
if (authChallenge.action === 'provideCredentials') {
const { credentials } = authChallenge;
username = credentials.username;
password = credentials.password;
}
const response = cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction(authChallenge.action);
await this.#continueWithAuth({
response,
username,
password,
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-provideResponse */
async provideResponse(overrides) {
assert(this.#fetchId, 'Network Interception not set-up.');
// We need to pass through if the request is already in
// AuthRequired phase
if (this.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
// We need to use `ProvideCredentials`
// As `Default` may cancel the request
return await this.#continueWithAuth({
response: 'ProvideCredentials',
});
}
// If we don't modify the response
// just continue the request
if (!overrides.body && !overrides.headers) {
return await this.#continueRequest();
}
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const responseHeaders = cdpFetchHeadersFromBidiNetworkHeaders(overrideHeaders);
const responseCode = overrides.statusCode ?? this.#statusCode ?? 200;
await this.cdpClient.sendCommand('Fetch.fulfillRequest', {
requestId: this.#fetchId,
responseCode,
responsePhrase: overrides.reasonPhrase,
responseHeaders,
body: getCdpBodyFromBiDiBytesValue(overrides.body),
});
this.#interceptPhase = undefined;
}
dispose() {
this.waitNextPhase.reject(new Error('waitNextPhase disposed'));
}
async #continueWithAuth(authChallengeResponse) {
assert(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueWithAuth', {
requestId: this.#fetchId,
authChallengeResponse,
});
this.#interceptPhase = undefined;
}
#emitEvent(getEvent) {
let event;
try {
event = getEvent();
}
catch (error) {
this.#logger?.(LogType.debugError, error);
return;
}
if (this.#isIgnoredEvent() ||
(this.#emittedEvents[event.method] &&
// Special case this event can be emitted multiple times
event.method !== ChromiumBidi.Network.EventNames.AuthRequired)) {
return;
}
this.#phaseChanged();
this.#emittedEvents[event.method] = true;
if (this.#context) {
this.#eventManager.registerEvent(Object.assign(event, {
type: 'event',
}), this.#context);
}
else {
this.#eventManager.registerGlobalEvent(Object.assign(event, {
type: 'event',
}));
}
}
#getBaseEventParams(phase) {
const interceptProps = {
isBlocked: false,
};
if (phase) {
const blockedBy = this.#interceptsInPhase(phase);
interceptProps.isBlocked = blockedBy.size > 0;
if (interceptProps.isBlocked) {
interceptProps.intercepts = [...blockedBy];
}
}
return {
context: this.#context,
navigation: this.#navigationId,
redirectCount: this.#redirectCount,
request: this.#getRequestData(),
// Timestamp should be in milliseconds, while CDP provides it in seconds.
timestamp: Math.round(getTiming(this.#request.info?.wallTime) * 1000),
// Contains isBlocked and intercepts
...interceptProps,
};
}
#getResponseEventParams() {
// Chromium sends wrong extraInfo events for responses served from cache.
// See https://github.com/puppeteer/puppeteer/issues/9965 and
// https://crbug.com/1340398.
if (this.#response.info?.fromDiskCache) {
this.#response.extraInfo = undefined;
}
// TODO: Also this.#response.paused?.responseHeaders have to be merged here.
const cdpHeaders = this.#response.info?.headers ?? {};
const cdpRawHeaders = this.#response.extraInfo?.headers ?? {};
for (const [key, value] of Object.entries(cdpRawHeaders)) {
cdpHeaders[key] = value;
}
const headers = bidiNetworkHeadersFromCdpNetworkHeaders(cdpHeaders);
const authChallenges = this.#authChallenges;
const response = {
url: this.url,
protocol: this.#response.info?.protocol ?? '',
status: this.#statusCode ?? -1, // TODO: Throw an exception or use some other status code?
statusText: this.#response.info?.statusText ||
this.#response.paused?.responseStatusText ||
'',
fromCache: this.#response.info?.fromDiskCache ||
this.#response.info?.fromPrefetchCache ||
this.#servedFromCache,
headers: this.#responseOverrides?.headers ?? headers,
mimeType: this.#response.info?.mimeType || '',
// TODO: this should be the size for the entire HTTP response.
bytesReceived: this.encodedResponseBodySize,
headersSize: computeHeadersSize(headers),
bodySize: this.encodedResponseBodySize,
content: {
size: this.#response.decodedSize ?? 0,
},
...(authChallenges ? { authChallenges } : {}),
};
return {
...response,
'goog:securityDetails': this.#response.info?.securityDetails,
};
}
get encodedResponseBodySize() {
return (this.#response.loadingFinished?.encodedDataLength ??
this.#response.info?.encodedDataLength ??
this.#response.encodedSize ??
0);
}
#getRequestData() {
const headers = this.#requestHeaders;
const request = {
request: this.#id,
url: this.url,
method: this.#method ?? _a.unknownParameter,
headers,
cookies: this.#cookies,
headersSize: computeHeadersSize(headers),
bodySize: this.bodySize,
// TODO: populate
destination: this.#getDestination(),
// TODO: populate
initiatorType: this.#getInitiatorType(),
timings: this.#timings,
};
return {
...request,
'goog:postData': this.#request.info?.request?.postData,
'goog:hasPostData': this.#request.info?.request?.hasPostData,
'goog:resourceType': this.#request.info?.type,
'goog:resourceInitiator': this.#request.info?.initiator,
};
}
/**
* Heuristic trying to guess the destination.
* Specification: https://fetch.spec.whatwg.org/#concept-request-destination.
* Specified values: "audio", "audioworklet", "document", "embed", "font", "frame",
* "iframe", "image", "json", "manifest", "object", "paintworklet", "report", "script",
* "serviceworker", "sharedworker", "style", "track", "video", "webidentity", "worker",
* "xslt".
*/
#getDestination() {
switch (this.#request.info?.type) {
case 'Script':
return 'script';
case 'Stylesheet':
return 'style';
case 'Image':
return 'image';
case 'Document':
// If request to document is initiated by parser, assume it is expected to
// arrive in an iframe. Otherwise, consider it is a navigation and the request
// result will end up in the document.
return this.#request.info?.initiator.type === 'parser'
? 'iframe'
: 'document';
default:
return '';
}
}
/**
* Heuristic trying to guess the initiator type.
* Specification: https://fetch.spec.whatwg.org/#request-initiator-type.
* Specified values: "audio", "beacon", "body", "css", "early-hints", "embed", "fetch",
* "font", "frame", "iframe", "image", "img", "input", "link", "object", "ping",
* "script", "track", "video", "xmlhttprequest", "other".
*/
#getInitiatorType() {
if (this.#request.info?.initiator.type === 'parser') {
switch (this.#request.info?.type) {
case 'Document':
// The request to document is initiated by the parser. Assuming it's an iframe.
return 'iframe';
case 'Font':
// If the document's url is not the parser's url, assume the resource is loaded
// from css. Otherwise, it's a `font` element.
return this.#request.info?.initiator?.url ===
this.#request.info?.documentURL
? 'font'
: 'css';
case 'Image':
// If the document's url is not the parser's url, assume the resource is loaded
// from css. Otherwise, it's a `img` element.
return this.#request.info?.initiator?.url ===
this.#request.info?.documentURL
? 'img'
: 'css';
case 'Script':
return 'script';
case 'Stylesheet':
return 'link';
default:
return null;
}
}
if (this.#request?.info?.type === 'Fetch') {
return 'fetch';
}
return null;
}
#getBeforeRequestEvent() {
assert(this.#request.info, 'RequestWillBeSentEvent is not set');
return {
method: ChromiumBidi.Network.EventNames.BeforeRequestSent,
params: {
...this.#getBaseEventParams("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */),
initiator: {
type: _a.#getInitiator(this.#request.info.initiator.type),
columnNumber: this.#request.info.initiator.columnNumber,
lineNumber: this.#request.info.initiator.lineNumber,
stackTrace: this.#request.info.initiator.stack,
request: this.#request.info.initiator.requestId,
},
},
};
}
#getResponseStartedEvent() {
return {
method: ChromiumBidi.Network.EventNames.ResponseStarted,
params: {
...this.#getBaseEventParams("responseStarted" /* Network.InterceptPhase.ResponseStarted */),
response: this.#getResponseEventParams(),
},
};
}
#getResponseReceivedEvent() {
return {
method: ChromiumBidi.Network.EventNames.ResponseCompleted,
params: {
...this.#getBaseEventParams(),
response: this.#getResponseEventParams(),
},
};
}
#isIgnoredEvent() {
const faviconUrl = '/favicon.ico';
return (this.#request.paused?.request.url.endsWith(faviconUrl) ??
this.#request.info?.request.url.endsWith(faviconUrl) ??
false);
}
#getOverrideHeader(headers, cookies) {
if (!headers && !cookies) {
return undefined;
}
let overrideHeaders = headers;
const cookieHeader = networkHeaderFromCookieHeaders(cookies);
if (cookieHeader && !overrideHeaders) {
overrideHeaders = this.#requestHeaders;
}
if (cookieHeader && overrideHeaders) {
overrideHeaders.filter((header) => header.name.localeCompare('cookie', undefined, {
sensitivity: 'base',
}) !== 0);
overrideHeaders.push(cookieHeader);
}
return overrideHeaders;
}
static #getInitiator(initiatorType) {
switch (initiatorType) {
case 'parser':
case 'script':
case 'preflight':
return initiatorType;
default:
return 'other';
}
}
}
_a = NetworkRequest;
function getCdpBodyFromBiDiBytesValue(body) {
let parsedBody;
if (body?.type === 'string') {
parsedBody = stringToBase64(body.value);
}
else if (body?.type === 'base64') {
parsedBody = body.value;
}
return parsedBody;
}
function getSizeFromBiDiBytesValue(body) {
if (body?.type === 'string') {
return body.value.length;
}
else if (body?.type === 'base64') {
return atob(body.value).length;
}
return 0;
}
//# sourceMappingURL=NetworkRequest.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,57 @@
import { type BrowsingContext, Network } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { CdpClient } from '../../BidiMapper.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
import { NetworkRequest } from './NetworkRequest.js';
import { type ParsedUrlPattern } from './NetworkUtils.js';
export declare const MAX_TOTAL_COLLECTED_SIZE = 200000000;
type NetworkInterception = Omit<Network.AddInterceptParameters, 'urlPatterns'> & {
urlPatterns: ParsedUrlPattern[];
};
/** Stores network and intercept maps. */
export declare class NetworkStorage {
#private;
constructor(eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, browserClient: CdpClient, logger?: LoggerFn);
onCdpTargetCreated(cdpTarget: CdpTarget): void;
getCollectedData(params: Network.GetDataParameters): Promise<Network.GetDataResult>;
collectIfNeeded(request: NetworkRequest, dataType: Network.DataType): void;
getInterceptionStages(browsingContextId: BrowsingContext.BrowsingContext): {
request: boolean;
response: boolean;
auth: boolean;
};
getInterceptsForPhase(request: NetworkRequest, phase: Network.InterceptPhase): Set<Network.Intercept>;
disposeRequestMap(sessionId: string): void;
/**
* Adds the given entry to the intercept map.
* URL patterns are assumed to be parsed.
*
* @return The intercept ID.
*/
addIntercept(value: NetworkInterception): Network.Intercept;
/**
* Removes the given intercept from the intercept map.
* Throws NoSuchInterceptException if the intercept does not exist.
*/
removeIntercept(intercept: Network.Intercept): void;
getRequestsByTarget(target: CdpTarget): NetworkRequest[];
getRequestById(id: Network.Request): NetworkRequest | undefined;
getRequestByFetchId(fetchId: Network.Request): NetworkRequest | undefined;
addRequest(request: NetworkRequest): void;
/**
* Disposes the given request, if no collectors targeting it are left.
*/
disposeRequest(id: Network.Request): void;
/**
* Gets the virtual navigation ID for the given navigable ID.
*/
getNavigationId(contextId: string | undefined): string | null;
set defaultCacheBehavior(behavior: Network.SetCacheBehaviorParameters['cacheBehavior']);
get defaultCacheBehavior(): Network.SetCacheBehaviorParameters["cacheBehavior"];
addDataCollector(params: Network.AddDataCollectorParameters): string;
removeDataCollector(params: Network.RemoveDataCollectorParameters): void;
disownData(params: Network.DisownDataParameters): void;
}
export {};
@@ -0,0 +1,349 @@
import { InvalidArgumentException, NoSuchInterceptException, NoSuchNetworkDataException, UnsupportedOperationException, } from '../../../protocol/protocol.js';
import { uuidv4 } from '../../../utils/uuid.js';
import { CollectorsStorage } from './CollectorsStorage.js';
import { NetworkRequest } from './NetworkRequest.js';
import { matchUrlPattern } from './NetworkUtils.js';
// The default total data size limit in CDP.
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_network_agent.cc;drc=da1f749634c9a401cc756f36c2e6ce233e1c9b4d;l=133
export const MAX_TOTAL_COLLECTED_SIZE = 200_000_000;
/** Stores network and intercept maps. */
export class NetworkStorage {
#browsingContextStorage;
#eventManager;
#collectorsStorage;
#logger;
/**
* A map from network request ID to Network Request objects.
* Needed as long as information about requests comes from different events.
*/
#requests = new Map();
/** A map from intercept ID to track active network intercepts. */
#intercepts = new Map();
#defaultCacheBehavior = 'default';
constructor(eventManager, browsingContextStorage, browserClient, logger) {
this.#browsingContextStorage = browsingContextStorage;
this.#eventManager = eventManager;
this.#collectorsStorage = new CollectorsStorage(MAX_TOTAL_COLLECTED_SIZE, logger);
browserClient.on('Target.detachedFromTarget', ({ sessionId }) => {
this.disposeRequestMap(sessionId);
});
this.#logger = logger;
}
/**
* Gets the network request with the given ID, if any.
* Otherwise, creates a new network request with the given ID and cdp target.
*/
#getOrCreateNetworkRequest(id, cdpTarget, redirectCount) {
let request = this.getRequestById(id);
if (redirectCount === undefined && request) {
// Force re-creating requests for redirects.
return request;
}
request = new NetworkRequest(id, this.#eventManager, this, cdpTarget, redirectCount, this.#logger);
this.addRequest(request);
return request;
}
onCdpTargetCreated(cdpTarget) {
const cdpClient = cdpTarget.cdpClient;
// TODO: Wrap into object
const listeners = [
[
'Network.requestWillBeSent',
(params) => {
const request = this.getRequestById(params.requestId);
request?.updateCdpTarget(cdpTarget);
if (request && request.isRedirecting()) {
request.handleRedirect(params);
this.disposeRequest(params.requestId);
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget, request.redirectCount + 1).onRequestWillBeSentEvent(params);
}
else {
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget).onRequestWillBeSentEvent(params);
}
},
],
[
'Network.requestWillBeSentExtraInfo',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onRequestWillBeSentExtraInfoEvent(params);
},
],
[
'Network.responseReceived',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onResponseReceivedEvent(params);
},
],
[
'Network.responseReceivedExtraInfo',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onResponseReceivedExtraInfoEvent(params);
},
],
[
'Network.requestServedFromCache',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onServedFromCache();
},
],
[
'Fetch.requestPaused',
(event) => {
const request = this.#getOrCreateNetworkRequest(
// CDP quirk if the Network domain is not present this is undefined
event.networkId ?? event.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onRequestPaused(event);
},
],
[
'Fetch.authRequired',
(event) => {
let request = this.getRequestByFetchId(event.requestId);
if (!request) {
request = this.#getOrCreateNetworkRequest(event.requestId, cdpTarget);
}
request.updateCdpTarget(cdpTarget);
request.onAuthRequired(event);
},
],
[
'Network.dataReceived',
(params) => {
const request = this.getRequestById(params.requestId);
request?.updateCdpTarget(cdpTarget);
request?.onDataReceivedEvent(params);
},
],
[
'Network.loadingFailed',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onLoadingFailedEvent(params);
},
],
[
'Network.loadingFinished',
(params) => {
const request = this.getRequestById(params.requestId);
request?.updateCdpTarget(cdpTarget);
request?.onLoadingFinishedEvent(params);
},
],
];
for (const [event, listener] of listeners) {
cdpClient.on(event, listener);
}
}
async getCollectedData(params) {
if (!this.#collectorsStorage.isCollected(params.request, params.dataType, params.collector)) {
throw new NoSuchNetworkDataException(params.collector === undefined
? `No collected ${params.dataType} data`
: `Collector ${params.collector} didn't collect ${params.dataType} data`);
}
if (params.disown && params.collector === undefined) {
throw new InvalidArgumentException('Cannot disown collected data without collector ID');
}
const request = this.getRequestById(params.request);
if (request === undefined) {
throw new NoSuchNetworkDataException(`No data for ${params.request}`);
}
let result = undefined;
switch (params.dataType) {
case "response" /* Network.DataType.Response */:
result = await this.#getCollectedResponseData(request);
break;
case "request" /* Network.DataType.Request */:
result = await this.#getCollectedRequestData(request);
break;
default:
throw new UnsupportedOperationException(`Unsupported data type ${params.dataType}`);
}
if (params.disown && params.collector !== undefined) {
this.#collectorsStorage.disownData(request.id, params.dataType, params.collector);
// `disposeRequest` disposes request only if no other collectors for it are left.
this.disposeRequest(request.id);
}
return result;
}
async #getCollectedResponseData(request) {
try {
const responseBody = await request.cdpClient.sendCommand('Network.getResponseBody', { requestId: request.id });
return {
bytes: {
type: responseBody.base64Encoded ? 'base64' : 'string',
value: responseBody.body,
},
};
}
catch (error) {
if (error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
error.message === 'No resource with given identifier found') {
// The data has be gone for whatever reason.
throw new NoSuchNetworkDataException(`Response data was disposed`);
}
if (error.code === -32001 /* CdpErrorConstants.CONNECTION_CLOSED */) {
// The request's CDP session is gone. http://b/450771615.
throw new NoSuchNetworkDataException(`Response data is disposed after the related page`);
}
throw error;
}
}
async #getCollectedRequestData(request) {
// TODO: handle CDP error in case of the renderer is gone.
const requestPostData = await request.cdpClient.sendCommand('Network.getRequestPostData', { requestId: request.id });
return {
bytes: {
type: 'string',
value: requestPostData.postData,
},
};
}
collectIfNeeded(request, dataType) {
this.#collectorsStorage.collectIfNeeded(request, dataType, request.cdpTarget.topLevelId, request.cdpTarget.userContext);
}
getInterceptionStages(browsingContextId) {
const stages = {
request: false,
response: false,
auth: false,
};
for (const intercept of this.#intercepts.values()) {
if (intercept.contexts &&
!intercept.contexts.includes(browsingContextId)) {
continue;
}
stages.request ||= intercept.phases.includes("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */);
stages.response ||= intercept.phases.includes("responseStarted" /* Network.InterceptPhase.ResponseStarted */);
stages.auth ||= intercept.phases.includes("authRequired" /* Network.InterceptPhase.AuthRequired */);
}
return stages;
}
getInterceptsForPhase(request, phase) {
if (request.url === NetworkRequest.unknownParameter) {
return new Set();
}
const intercepts = new Set();
for (const [interceptId, intercept] of this.#intercepts.entries()) {
if (!intercept.phases.includes(phase) ||
(intercept.contexts &&
!intercept.contexts.includes(request.cdpTarget.topLevelId))) {
continue;
}
if (intercept.urlPatterns.length === 0) {
intercepts.add(interceptId);
continue;
}
for (const pattern of intercept.urlPatterns) {
if (matchUrlPattern(pattern, request.url)) {
intercepts.add(interceptId);
break;
}
}
}
return intercepts;
}
disposeRequestMap(sessionId) {
for (const request of this.#requests.values()) {
if (request.cdpClient.sessionId === sessionId) {
this.#requests.delete(request.id);
request.dispose();
}
}
}
/**
* Adds the given entry to the intercept map.
* URL patterns are assumed to be parsed.
*
* @return The intercept ID.
*/
addIntercept(value) {
const interceptId = uuidv4();
this.#intercepts.set(interceptId, value);
return interceptId;
}
/**
* Removes the given intercept from the intercept map.
* Throws NoSuchInterceptException if the intercept does not exist.
*/
removeIntercept(intercept) {
if (!this.#intercepts.has(intercept)) {
throw new NoSuchInterceptException(`Intercept '${intercept}' does not exist.`);
}
this.#intercepts.delete(intercept);
}
getRequestsByTarget(target) {
const requests = [];
for (const request of this.#requests.values()) {
if (request.cdpTarget === target) {
requests.push(request);
}
}
return requests;
}
getRequestById(id) {
return this.#requests.get(id);
}
getRequestByFetchId(fetchId) {
for (const request of this.#requests.values()) {
if (request.fetchId === fetchId) {
return request;
}
}
return;
}
addRequest(request) {
this.#requests.set(request.id, request);
}
/**
* Disposes the given request, if no collectors targeting it are left.
*/
disposeRequest(id) {
if (this.#collectorsStorage.isCollected(id)) {
// Keep request, as it's data can be accessed later.
return;
}
// TODO: dispose Network data from Chromium once there is a CDP command for that.
this.#requests.delete(id);
}
/**
* Gets the virtual navigation ID for the given navigable ID.
*/
getNavigationId(contextId) {
if (contextId === undefined) {
return null;
}
return (this.#browsingContextStorage.findContext(contextId)?.navigationId ?? null);
}
set defaultCacheBehavior(behavior) {
this.#defaultCacheBehavior = behavior;
}
get defaultCacheBehavior() {
return this.#defaultCacheBehavior;
}
addDataCollector(params) {
return this.#collectorsStorage.addDataCollector(params);
}
removeDataCollector(params) {
const releasedRequests = this.#collectorsStorage.removeDataCollector(params.collector);
releasedRequests.map((request) => this.disposeRequest(request));
}
disownData(params) {
if (!this.#collectorsStorage.isCollected(params.request, params.dataType, params.collector)) {
throw new NoSuchNetworkDataException(`Collector ${params.collector} didn't collect ${params.dataType} data`);
}
this.#collectorsStorage.disownData(params.request, params.dataType, params.collector);
// `disposeRequest` disposes request only if no other collectors for it are left.
this.disposeRequest(params.request);
}
}
//# sourceMappingURL=NetworkStorage.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,59 @@
/**
* @fileoverview Utility functions for the Network module.
*/
import type { Protocol } from 'devtools-protocol';
import { Network, type Storage } from '../../../protocol/protocol.js';
export declare function computeHeadersSize(headers: Network.Header[]): number;
export declare function stringToBase64(str: string): string;
/** Converts from CDP Network domain headers to BiDi network headers. */
export declare function bidiNetworkHeadersFromCdpNetworkHeaders(headers?: Protocol.Network.Headers): Network.Header[];
/** Converts from CDP Fetch domain headers to BiDi network headers. */
export declare function bidiNetworkHeadersFromCdpNetworkHeadersEntries(headers?: Protocol.Fetch.HeaderEntry[]): Network.Header[];
/** Converts from Bidi network headers to CDP Network domain headers. */
export declare function cdpNetworkHeadersFromBidiNetworkHeaders(headers?: Network.Header[]): Protocol.Network.Headers | undefined;
/** Converts from CDP Fetch domain header entries to Bidi network headers. */
export declare function bidiNetworkHeadersFromCdpFetchHeaders(headers?: Protocol.Fetch.HeaderEntry[]): Network.Header[];
/** Converts from Bidi network headers to CDP Fetch domain header entries. */
export declare function cdpFetchHeadersFromBidiNetworkHeaders(headers?: Network.Header[]): Protocol.Fetch.HeaderEntry[] | undefined;
export declare function networkHeaderFromCookieHeaders(headers?: Network.CookieHeader[]): Network.Header | undefined;
/** Converts from Bidi auth action to CDP auth challenge response. */
export declare function cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction(action: 'default' | 'cancel' | 'provideCredentials'): "Default" | "CancelAuth" | "ProvideCredentials";
/**
* Converts from CDP Network domain cookie to BiDi network cookie.
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
*/
export declare function cdpToBiDiCookie(cookie: Protocol.Network.Cookie): Network.Cookie;
/**
* Decodes a byte value to a string.
* @param {Network.BytesValue} value
* @return {string}
*/
export declare function deserializeByteValue(value: Network.BytesValue): string;
/**
* Converts from BiDi set network cookie params to CDP Network domain cookie.
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-CookieParam
*/
export declare function bidiToCdpCookie(params: Storage.SetCookieParameters, partitionKey: Storage.PartitionKey): Protocol.Network.CookieParam;
export declare function sameSiteBiDiToCdp(sameSite: Network.SameSite): Protocol.Network.CookieSameSite;
/**
* Returns true if the given protocol is special.
* Special protocols are those that have a default port.
*
* Example inputs: 'http', 'http:'
*
* @see https://url.spec.whatwg.org/#special-scheme
*/
export declare function isSpecialScheme(protocol: string): boolean;
export interface ParsedUrlPattern {
protocol?: string;
hostname?: string;
port?: string;
pathname?: string;
search?: string;
}
/** Matches the given URLPattern against the given URL. */
export declare function matchUrlPattern(pattern: ParsedUrlPattern, url: string): boolean;
export declare function bidiBodySizeFromCdpPostDataEntries(entries: Protocol.Network.PostDataEntry[]): number;
export declare function getTiming(timing: number | undefined, offset?: number): number;
@@ -0,0 +1,303 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { InvalidArgumentException } from '../../../protocol/ErrorResponse.js';
import { base64ToString } from '../../../utils/base64.js';
export function computeHeadersSize(headers) {
const requestHeaders = headers.reduce((acc, header) => {
return `${acc}${header.name}: ${header.value.value}\r\n`;
}, '');
return new TextEncoder().encode(requestHeaders).length;
}
export function stringToBase64(str) {
return typedArrayToBase64(new TextEncoder().encode(str));
}
function typedArrayToBase64(typedArray) {
// chunkSize should be less V8 limit on number of arguments!
// https://github.com/v8/v8/blob/d3de848bea727518aee94dd2fd42ba0b62037a27/src/objects/code.h#L444
const chunkSize = 65534;
const chunks = [];
for (let i = 0; i < typedArray.length; i += chunkSize) {
const chunk = typedArray.subarray(i, i + chunkSize);
chunks.push(String.fromCodePoint.apply(null, chunk));
}
const binaryString = chunks.join('');
return btoa(binaryString);
}
/** Converts from CDP Network domain headers to BiDi network headers. */
export function bidiNetworkHeadersFromCdpNetworkHeaders(headers) {
if (!headers) {
return [];
}
return Object.entries(headers).map(([name, value]) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from CDP Fetch domain headers to BiDi network headers. */
export function bidiNetworkHeadersFromCdpNetworkHeadersEntries(headers) {
if (!headers) {
return [];
}
return headers.map(({ name, value }) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from Bidi network headers to CDP Network domain headers. */
export function cdpNetworkHeadersFromBidiNetworkHeaders(headers) {
if (headers === undefined) {
return undefined;
}
return headers.reduce((result, header) => {
// TODO: Distinguish between string and bytes?
result[header.name] = header.value.value;
return result;
}, {});
}
/** Converts from CDP Fetch domain header entries to Bidi network headers. */
export function bidiNetworkHeadersFromCdpFetchHeaders(headers) {
if (!headers) {
return [];
}
return headers.map(({ name, value }) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from Bidi network headers to CDP Fetch domain header entries. */
export function cdpFetchHeadersFromBidiNetworkHeaders(headers) {
if (headers === undefined) {
return undefined;
}
return headers.map(({ name, value }) => ({
name,
value: value.value,
}));
}
export function networkHeaderFromCookieHeaders(headers) {
if (headers === undefined) {
return undefined;
}
const value = headers.reduce((acc, value, index) => {
if (index > 0) {
acc += ';';
}
const cookieValue = value.value.type === 'base64'
? btoa(value.value.value)
: value.value.value;
acc += `${value.name}=${cookieValue}`;
return acc;
}, '');
return {
name: 'Cookie',
value: {
type: 'string',
value,
},
};
}
/** Converts from Bidi auth action to CDP auth challenge response. */
export function cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction(action) {
switch (action) {
case 'default':
return 'Default';
case 'cancel':
return 'CancelAuth';
case 'provideCredentials':
return 'ProvideCredentials';
}
}
/**
* Converts from CDP Network domain cookie to BiDi network cookie.
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
*/
export function cdpToBiDiCookie(cookie) {
const result = {
name: cookie.name,
value: { type: 'string', value: cookie.value },
domain: cookie.domain,
path: cookie.path,
size: cookie.size,
httpOnly: cookie.httpOnly,
secure: cookie.secure,
sameSite: cookie.sameSite === undefined
? "none" /* Network.SameSite.None */
: sameSiteCdpToBiDi(cookie.sameSite),
...(cookie.expires >= 0 ? { expiry: Math.round(cookie.expires) } : undefined),
};
// Extending with CDP-specific properties with `goog:` prefix.
result[`goog:session`] = cookie.session;
result[`goog:priority`] = cookie.priority;
result[`goog:sourceScheme`] = cookie.sourceScheme;
result[`goog:sourcePort`] = cookie.sourcePort;
if (cookie.partitionKey !== undefined) {
result[`goog:partitionKey`] = cookie.partitionKey;
}
if (cookie.partitionKeyOpaque !== undefined) {
result[`goog:partitionKeyOpaque`] = cookie.partitionKeyOpaque;
}
return result;
}
/**
* Decodes a byte value to a string.
* @param {Network.BytesValue} value
* @return {string}
*/
export function deserializeByteValue(value) {
if (value.type === 'base64') {
return base64ToString(value.value);
}
return value.value;
}
/**
* Converts from BiDi set network cookie params to CDP Network domain cookie.
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-CookieParam
*/
export function bidiToCdpCookie(params, partitionKey) {
const deserializedValue = deserializeByteValue(params.cookie.value);
const result = {
name: params.cookie.name,
value: deserializedValue,
domain: params.cookie.domain,
path: params.cookie.path ?? '/',
secure: params.cookie.secure ?? false,
httpOnly: params.cookie.httpOnly ?? false,
...(partitionKey.sourceOrigin !== undefined && {
partitionKey: {
hasCrossSiteAncestor: false,
// CDP's `partitionKey.topLevelSite` is the BiDi's `partition.sourceOrigin`.
topLevelSite: partitionKey.sourceOrigin,
},
}),
...(params.cookie.expiry !== undefined && {
expires: params.cookie.expiry,
}),
...(params.cookie.sameSite !== undefined && {
sameSite: sameSiteBiDiToCdp(params.cookie.sameSite),
}),
};
// Extending with CDP-specific properties with `goog:` prefix.
if (params.cookie[`goog:url`] !== undefined) {
result.url = params.cookie[`goog:url`];
}
if (params.cookie[`goog:priority`] !== undefined) {
result.priority = params.cookie[`goog:priority`];
}
if (params.cookie[`goog:sourceScheme`] !== undefined) {
result.sourceScheme = params.cookie[`goog:sourceScheme`];
}
if (params.cookie[`goog:sourcePort`] !== undefined) {
result.sourcePort = params.cookie[`goog:sourcePort`];
}
return result;
}
function sameSiteCdpToBiDi(sameSite) {
switch (sameSite) {
case 'Strict':
return "strict" /* Network.SameSite.Strict */;
case 'None':
return "none" /* Network.SameSite.None */;
case 'Lax':
return "lax" /* Network.SameSite.Lax */;
default:
// Defaults to `Lax`:
// https://web.dev/articles/samesite-cookies-explained#samesitelax_by_default
return "lax" /* Network.SameSite.Lax */;
}
}
export function sameSiteBiDiToCdp(sameSite) {
switch (sameSite) {
case "none" /* Network.SameSite.None */:
return 'None';
case "strict" /* Network.SameSite.Strict */:
return 'Strict';
// Defaults to `Lax`:
// https://web.dev/articles/samesite-cookies-explained#samesitelax_by_default
case "default" /* Network.SameSite.Default */:
case "lax" /* Network.SameSite.Lax */:
return 'Lax';
}
throw new InvalidArgumentException(`Unknown 'sameSite' value ${sameSite}`);
}
/**
* Returns true if the given protocol is special.
* Special protocols are those that have a default port.
*
* Example inputs: 'http', 'http:'
*
* @see https://url.spec.whatwg.org/#special-scheme
*/
export function isSpecialScheme(protocol) {
return ['ftp', 'file', 'http', 'https', 'ws', 'wss'].includes(protocol.replace(/:$/, ''));
}
function getScheme(url) {
return url.protocol.replace(/:$/, '');
}
/** Matches the given URLPattern against the given URL. */
export function matchUrlPattern(pattern, url) {
// Roughly https://w3c.github.io/webdriver-bidi/#match-url-pattern
// plus some differences based on the URL parsing methods.
const parsedUrl = new URL(url);
if (pattern.protocol !== undefined &&
pattern.protocol !== getScheme(parsedUrl)) {
return false;
}
if (pattern.hostname !== undefined &&
pattern.hostname !== parsedUrl.hostname) {
return false;
}
if (pattern.port !== undefined && pattern.port !== parsedUrl.port) {
return false;
}
if (pattern.pathname !== undefined &&
pattern.pathname !== parsedUrl.pathname) {
return false;
}
if (pattern.search !== undefined && pattern.search !== parsedUrl.search) {
return false;
}
return true;
}
export function bidiBodySizeFromCdpPostDataEntries(entries) {
let size = 0;
for (const entry of entries) {
size += atob(entry.bytes ?? '').length;
}
return size;
}
export function getTiming(timing, offset = 0) {
if (!timing) {
return 0;
}
if (timing <= 0 || timing + offset <= 0) {
return 0;
}
return timing + offset;
}
//# sourceMappingURL=NetworkUtils.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,23 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { type EmptyResult, type Permissions } from '../../../protocol/protocol.js';
export declare class PermissionsProcessor {
#private;
constructor(browserCdpClient: CdpClient);
setPermissions(params: Permissions.SetPermissionParameters): Promise<EmptyResult>;
}
@@ -0,0 +1,51 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, } from '../../../protocol/protocol.js';
export class PermissionsProcessor {
#browserCdpClient;
constructor(browserCdpClient) {
this.#browserCdpClient = browserCdpClient;
}
async setPermissions(params) {
try {
const userContextId = params['goog:userContext'] ||
params.userContext;
await this.#browserCdpClient.sendCommand('Browser.setPermission', {
origin: params.origin,
embeddedOrigin: params.embeddedOrigin,
browserContextId: userContextId && userContextId !== 'default'
? userContextId
: undefined,
permission: {
name: params.descriptor.name,
},
setting: params.state,
});
}
catch (err) {
if (err.message ===
`Permission can't be granted to opaque origins.`) {
// Return success if the origin is not valid (does not match any
// existing origins).
return {};
}
throw new InvalidArgumentException(err.message);
}
return {};
}
}
//# sourceMappingURL=PermissionsProcessor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"PermissionsProcessor.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/permissions/PermissionsProcessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EACL,wBAAwB,GAGzB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,OAAO,oBAAoB;IAC/B,iBAAiB,CAAY;IAE7B,YAAY,gBAA2B;QACrC,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,MAA2C;QAE3C,IAAI,CAAC;YACH,MAAM,aAAa,GAChB,MAAwC,CAAC,kBAAkB,CAAC;gBAC7D,MAAM,CAAC,WAAW,CAAC;YACrB,MAAM,IAAI,CAAC,iBAAiB,CAAC,WAAW,CAAC,uBAAuB,EAAE;gBAChE,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,cAAc,EAAE,MAAM,CAAC,cAAc;gBACrC,gBAAgB,EACd,aAAa,IAAI,aAAa,KAAK,SAAS;oBAC1C,CAAC,CAAC,aAAa;oBACf,CAAC,CAAC,SAAS;gBACf,UAAU,EAAE;oBACV,IAAI,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI;iBAC7B;gBACD,OAAO,EAAE,MAAM,CAAC,KAAK;aACtB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IACG,GAAa,CAAC,OAAO;gBACtB,gDAAgD,EAChD,CAAC;gBACD,gEAAgE;gBAChE,qBAAqB;gBACrB,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,IAAI,wBAAwB,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;CACF"}
@@ -0,0 +1,30 @@
import { Script } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { EventManager } from '../session/EventManager.js';
import type { Realm } from './Realm.js';
/**
* Used to send messages from realm to BiDi user.
*/
export declare class ChannelProxy {
#private;
constructor(channel: Script.ChannelProperties, logger?: LoggerFn);
/**
* Creates a channel proxy in the given realm, initialises listener and
* returns a handle to `sendMessage` delegate.
*/
init(realm: Realm, eventManager: EventManager): Promise<Script.Handle>;
/** Gets a ChannelProxy from window and returns its handle. */
startListenerFromWindow(realm: Realm, eventManager: EventManager): Promise<void>;
/**
* String to be evaluated to create a ProxyChannel and put it to window.
* Returns the delegate `sendMessage`. Used to provide an argument for preload
* script. Does the following:
* 1. Creates a ChannelProxy.
* 2. Puts the ChannelProxy to window['${this.#id}'] or resolves the promise
* by calling delegate stored in window['${this.#id}'].
* This is needed because `#getHandleFromWindow` can be called before or
* after this method.
* 3. Returns the delegate `sendMessage` of the created ChannelProxy.
*/
getEvalInWindowStr(): string;
}
@@ -0,0 +1,231 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { ChromiumBidi } from '../../../protocol/protocol.js';
import { LogType } from '../../../utils/log.js';
import { uuidv4 } from '../../../utils/uuid.js';
/**
* Used to send messages from realm to BiDi user.
*/
export class ChannelProxy {
#properties;
#id = uuidv4();
#logger;
constructor(channel, logger) {
this.#properties = channel;
this.#logger = logger;
}
/**
* Creates a channel proxy in the given realm, initialises listener and
* returns a handle to `sendMessage` delegate.
*/
async init(realm, eventManager) {
const channelHandle = await ChannelProxy.#createAndGetHandleInRealm(realm);
const sendMessageHandle = await ChannelProxy.#createSendMessageHandle(realm, channelHandle);
void this.#startListener(realm, channelHandle, eventManager);
return sendMessageHandle;
}
/** Gets a ChannelProxy from window and returns its handle. */
async startListenerFromWindow(realm, eventManager) {
try {
const channelHandle = await this.#getHandleFromWindow(realm);
void this.#startListener(realm, channelHandle, eventManager);
}
catch (error) {
this.#logger?.(LogType.debugError, error);
}
}
/**
* Evaluation string which creates a ChannelProxy object on the client side.
*/
static #createChannelProxyEvalStr() {
const functionStr = String(() => {
const queue = [];
let queueNonEmptyResolver = null;
return {
/**
* Gets a promise, which is resolved as soon as a message occurs
* in the queue.
*/
async getMessage() {
const onMessage = queue.length > 0
? Promise.resolve()
: new Promise((resolve) => {
queueNonEmptyResolver = resolve;
});
await onMessage;
return queue.shift();
},
/**
* Adds a message to the queue.
* Resolves the pending promise if needed.
*/
sendMessage(message) {
queue.push(message);
if (queueNonEmptyResolver !== null) {
queueNonEmptyResolver();
queueNonEmptyResolver = null;
}
},
};
});
return `(${functionStr})()`;
}
/** Creates a ChannelProxy in the given realm. */
static async #createAndGetHandleInRealm(realm) {
const createChannelHandleResult = await realm.cdpClient.sendCommand('Runtime.evaluate', {
expression: this.#createChannelProxyEvalStr(),
contextId: realm.executionContextId,
serializationOptions: {
serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */,
},
});
if (createChannelHandleResult.exceptionDetails ||
createChannelHandleResult.result.objectId === undefined) {
throw new Error(`Cannot create channel`);
}
return createChannelHandleResult.result.objectId;
}
/** Gets a handle to `sendMessage` delegate from the ChannelProxy handle. */
static async #createSendMessageHandle(realm, channelHandle) {
const sendMessageArgResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((channelHandle) => {
return channelHandle.sendMessage;
}),
arguments: [{ objectId: channelHandle }],
executionContextId: realm.executionContextId,
serializationOptions: {
serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */,
},
});
// TODO: check for exceptionDetails.
return sendMessageArgResult.result.objectId;
}
/** Starts listening for the channel events of the provided ChannelProxy. */
async #startListener(realm, channelHandle, eventManager) {
// noinspection InfiniteLoopJS
for (;;) {
try {
const message = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String(async (channelHandle) => await channelHandle.getMessage()),
arguments: [
{
objectId: channelHandle,
},
],
awaitPromise: true,
executionContextId: realm.executionContextId,
serializationOptions: {
serialization: "deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */,
maxDepth: this.#properties.serializationOptions?.maxObjectDepth ??
undefined,
},
});
if (message.exceptionDetails) {
throw new Error('Runtime.callFunctionOn in ChannelProxy', {
cause: message.exceptionDetails,
});
}
for (const browsingContext of realm.associatedBrowsingContexts) {
eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.Script.EventNames.Message,
params: {
channel: this.#properties.channel,
data: realm.cdpToBidiValue(message, this.#properties.ownership ?? "none" /* Script.ResultOwnership.None */),
source: realm.source,
},
}, browsingContext.id);
}
}
catch (error) {
// If an error is thrown, then the channel is permanently broken, so we
// exit the loop.
this.#logger?.(LogType.debugError, error);
break;
}
}
}
/**
* Returns a handle of ChannelProxy from window's property which was set there
* by `getEvalInWindowStr`. If window property is not set yet, sets a promise
* resolver to the window property, so that `getEvalInWindowStr` can resolve
* the promise later on with the channel.
* This is needed because `getEvalInWindowStr` can be called before or
* after this method.
*/
async #getHandleFromWindow(realm) {
const channelHandleResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((id) => {
const w = window;
if (w[id] === undefined) {
// The channelProxy is not created yet. Create a promise, put the
// resolver to window property and return the promise.
// `getEvalInWindowStr` will resolve the promise later.
return new Promise((resolve) => (w[id] = resolve));
}
// The channelProxy is already created by `getEvalInWindowStr` and
// is set into window property. Return it.
const channelProxy = w[id];
delete w[id];
return channelProxy;
}),
arguments: [{ value: this.#id }],
executionContextId: realm.executionContextId,
awaitPromise: true,
serializationOptions: {
serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */,
},
});
if (channelHandleResult.exceptionDetails !== undefined ||
channelHandleResult.result.objectId === undefined) {
throw new Error(`ChannelHandle not found in window["${this.#id}"]`);
}
return channelHandleResult.result.objectId;
}
/**
* String to be evaluated to create a ProxyChannel and put it to window.
* Returns the delegate `sendMessage`. Used to provide an argument for preload
* script. Does the following:
* 1. Creates a ChannelProxy.
* 2. Puts the ChannelProxy to window['${this.#id}'] or resolves the promise
* by calling delegate stored in window['${this.#id}'].
* This is needed because `#getHandleFromWindow` can be called before or
* after this method.
* 3. Returns the delegate `sendMessage` of the created ChannelProxy.
*/
getEvalInWindowStr() {
const delegate = String((id, channelProxy) => {
const w = window;
if (w[id] === undefined) {
// `#getHandleFromWindow` is not initialized yet, and will get the
// channelProxy later.
w[id] = channelProxy;
}
else {
// `#getHandleFromWindow` is already set a delegate to window property
// and is waiting for it to be called with the channelProxy.
w[id](channelProxy);
delete w[id];
}
return channelProxy.sendMessage;
});
const channelProxyEval = ChannelProxy.#createChannelProxyEvalStr();
return `(${delegate})('${this.#id}',${channelProxyEval})`;
}
}
//# sourceMappingURL=ChannelProxy.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,43 @@
import type { Protocol } from 'devtools-protocol';
import type { Browser, BrowsingContext, Script } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import { ChannelProxy } from './ChannelProxy.js';
/**
* BiDi IDs are generated by the server and are unique within contexts.
*
* CDP preload script IDs are generated by the client and are unique
* within sessions.
*
* The mapping between BiDi and CDP preload script IDs is 1:many.
* BiDi IDs are needed by the mapper to keep track of potential multiple CDP IDs
* in the client.
*/
export declare class PreloadScript {
#private;
get id(): string;
get targetIds(): Set<Protocol.Target.TargetID>;
constructor(params: Script.AddPreloadScriptParameters, logger?: LoggerFn);
/** Channels of the preload script. */
get channels(): ChannelProxy[];
/** Contexts of the preload script, if any */
get contexts(): BrowsingContext.BrowsingContext[] | undefined;
/** UserContexts of the preload script, if any */
get userContexts(): Browser.UserContext[] | undefined;
/**
* Adds the script to the given CDP targets by calling the
* `Page.addScriptToEvaluateOnNewDocument` command.
*/
initInTargets(cdpTargets: Iterable<CdpTarget>, runImmediately: boolean): Promise<void>;
/**
* Adds the script to the given CDP target by calling the
* `Page.addScriptToEvaluateOnNewDocument` command.
*/
initInTarget(cdpTarget: CdpTarget, runImmediately: boolean): Promise<void>;
/**
* Removes this script from all CDP targets.
*/
remove(): Promise<void>;
/** Removes the provided cdp target from the list of cdp preload scripts. */
dispose(cdpTargetId: Protocol.Target.TargetID): void;
}
@@ -0,0 +1,129 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { uuidv4 } from '../../../utils/uuid.js';
import { ChannelProxy } from './ChannelProxy.js';
/**
* BiDi IDs are generated by the server and are unique within contexts.
*
* CDP preload script IDs are generated by the client and are unique
* within sessions.
*
* The mapping between BiDi and CDP preload script IDs is 1:many.
* BiDi IDs are needed by the mapper to keep track of potential multiple CDP IDs
* in the client.
*/
export class PreloadScript {
/** BiDi ID, an automatically generated UUID. */
#id = uuidv4();
/** CDP preload scripts. */
#cdpPreloadScripts = [];
/** The script itself, in a format expected by the spec i.e. a function. */
#functionDeclaration;
/** Targets, in which the preload script is initialized. */
#targetIds = new Set();
/** Channels to be added as arguments to functionDeclaration. */
#channels;
/** The script sandbox / world name. */
#sandbox;
/** The browsing contexts to execute the preload scripts in, if any. */
#contexts;
/** The browsing contexts to execute the preload scripts in, if any. */
#userContexts;
get id() {
return this.#id;
}
get targetIds() {
return this.#targetIds;
}
constructor(params, logger) {
this.#channels =
params.arguments?.map((a) => new ChannelProxy(a.value, logger)) ?? [];
this.#functionDeclaration = params.functionDeclaration;
this.#sandbox = params.sandbox;
this.#contexts = params.contexts;
this.#userContexts = params.userContexts;
}
/** Channels of the preload script. */
get channels() {
return this.#channels;
}
/** Contexts of the preload script, if any */
get contexts() {
return this.#contexts;
}
/** UserContexts of the preload script, if any */
get userContexts() {
return this.#userContexts;
}
/**
* String to be evaluated. Wraps user-provided function so that the following
* steps are run:
* 1. Create channels.
* 2. Store the created channels in window.
* 3. Call the user-provided function with channels as arguments.
*/
#getEvaluateString() {
const channelsArgStr = `[${this.channels
.map((c) => c.getEvalInWindowStr())
.join(', ')}]`;
return `(()=>{(${this.#functionDeclaration})(...${channelsArgStr})})()`;
}
/**
* Adds the script to the given CDP targets by calling the
* `Page.addScriptToEvaluateOnNewDocument` command.
*/
async initInTargets(cdpTargets, runImmediately) {
await Promise.all(Array.from(cdpTargets).map((cdpTarget) => this.initInTarget(cdpTarget, runImmediately)));
}
/**
* Adds the script to the given CDP target by calling the
* `Page.addScriptToEvaluateOnNewDocument` command.
*/
async initInTarget(cdpTarget, runImmediately) {
const addCdpPreloadScriptResult = await cdpTarget.cdpClient.sendCommand('Page.addScriptToEvaluateOnNewDocument', {
source: this.#getEvaluateString(),
worldName: this.#sandbox,
runImmediately,
});
this.#cdpPreloadScripts.push({
target: cdpTarget,
preloadScriptId: addCdpPreloadScriptResult.identifier,
});
this.#targetIds.add(cdpTarget.id);
}
/**
* Removes this script from all CDP targets.
*/
async remove() {
await Promise.all([
this.#cdpPreloadScripts.map(async (cdpPreloadScript) => {
const cdpTarget = cdpPreloadScript.target;
const cdpPreloadScriptId = cdpPreloadScript.preloadScriptId;
return await cdpTarget.cdpClient.sendCommand('Page.removeScriptToEvaluateOnNewDocument', {
identifier: cdpPreloadScriptId,
});
}),
]);
}
/** Removes the provided cdp target from the list of cdp preload scripts. */
dispose(cdpTargetId) {
this.#cdpPreloadScripts = this.#cdpPreloadScripts.filter((cdpPreloadScript) => cdpPreloadScript.target?.id !== cdpTargetId);
this.#targetIds.delete(cdpTargetId);
}
}
//# sourceMappingURL=PreloadScript.js.map
@@ -0,0 +1 @@
{"version":3,"file":"PreloadScript.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/script/PreloadScript.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
import type { Browser } from '../../../protocol/protocol.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { PreloadScript } from './PreloadScript.js';
/** PreloadScripts can be filtered by BiDi ID or target ID. */
export interface PreloadScriptFilter {
targetId: CdpTarget['id'];
}
/**
* Container class for preload scripts.
*/
export declare class PreloadScriptStorage {
#private;
/**
* Finds all entries that match the given filter (OR logic).
*/
find(filter?: PreloadScriptFilter): PreloadScript[];
add(preloadScript: PreloadScript): void;
/** Deletes all BiDi preload script entries that match the given filter. */
remove(id: string): void;
/** Gets the preload script with the given ID, if any, otherwise throws. */
getPreloadScript(id: string): PreloadScript;
onCdpTargetCreated(targetId: string, userContext: Browser.UserContext): void;
}
@@ -0,0 +1,75 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NoSuchScriptException } from '../../../protocol/ErrorResponse.js';
/**
* Container class for preload scripts.
*/
export class PreloadScriptStorage {
/** Tracks all BiDi preload scripts. */
#scripts = new Set();
/**
* Finds all entries that match the given filter (OR logic).
*/
find(filter) {
if (!filter) {
return [...this.#scripts];
}
return [...this.#scripts].filter((script) => {
// Global scripts have no contexts or userContext
if (script.contexts === undefined && script.userContexts === undefined) {
return true;
}
if (filter.targetId !== undefined &&
script.targetIds.has(filter.targetId)) {
return true;
}
return false;
});
}
add(preloadScript) {
this.#scripts.add(preloadScript);
}
/** Deletes all BiDi preload script entries that match the given filter. */
remove(id) {
const script = [...this.#scripts].find((script) => script.id === id);
if (script === undefined) {
throw new NoSuchScriptException(`No preload script with id '${id}'`);
}
this.#scripts.delete(script);
}
/** Gets the preload script with the given ID, if any, otherwise throws. */
getPreloadScript(id) {
const script = [...this.#scripts].find((script) => script.id === id);
if (script === undefined) {
throw new NoSuchScriptException(`No preload script with id '${id}'`);
}
return script;
}
onCdpTargetCreated(targetId, userContext) {
const scriptInUserContext = [...this.#scripts].filter((script) => {
// Global scripts
if (!script.userContexts && !script.contexts) {
return true;
}
return script.userContexts?.includes(userContext);
});
for (const script of scriptInUserContext) {
script.targetIds.add(targetId);
}
}
}
//# sourceMappingURL=PreloadScriptStorage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"PreloadScriptStorage.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/script/PreloadScriptStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAC,qBAAqB,EAAC,MAAM,oCAAoC,CAAC;AAWzE;;GAEG;AACH,MAAM,OAAO,oBAAoB;IAC/B,wCAAwC;IAC/B,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAE7C;;OAEG;IACH,IAAI,CAAC,MAA4B;QAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;YAC1C,iDAAiD;YACjD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;gBACvE,OAAO,IAAI,CAAC;YACd,CAAC;YAED,IACE,MAAM,CAAC,QAAQ,KAAK,SAAS;gBAC7B,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EACrC,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAED,GAAG,CAAC,aAA4B;QAC9B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACnC,CAAC;IAED,2EAA2E;IAC3E,MAAM,CAAC,EAAU;QACf,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACrE,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,qBAAqB,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,2EAA2E;IAC3E,gBAAgB,CAAC,EAAU;QACzB,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACrE,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,qBAAqB,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,kBAAkB,CAAC,QAAgB,EAAE,WAAgC;QACnE,MAAM,mBAAmB,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;YAC/D,iBAAiB;YACjB,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;gBAC7C,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QACH,KAAK,MAAM,MAAM,IAAI,mBAAmB,EAAE,CAAC;YACzC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,67 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Protocol } from 'devtools-protocol';
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { Script } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import type { EventManager } from '../session/EventManager.js';
import type { RealmStorage } from './RealmStorage.js';
export declare abstract class Realm {
#private;
protected realmStorage: RealmStorage;
constructor(cdpClient: CdpClient, eventManager: EventManager, executionContextId: Protocol.Runtime.ExecutionContextId, logger: LoggerFn | undefined, origin: string, realmId: Script.Realm, realmStorage: RealmStorage);
cdpToBidiValue(cdpValue: Protocol.Runtime.CallFunctionOnResponse | Protocol.Runtime.EvaluateResponse, resultOwnership: Script.ResultOwnership): Script.RemoteValue;
isHidden(): boolean;
/**
* Relies on the CDP to implement proper BiDi serialization, except:
* * CDP integer property `backendNodeId` is replaced with `sharedId` of
* `{documentId}_element_{backendNodeId}`;
* * CDP integer property `weakLocalObjectReference` is replaced with UUID `internalId`
* using unique-per serialization `internalIdMap`.
* * CDP type `platformobject` is replaced with `object`.
* @param deepSerializedValue - CDP value to be converted to BiDi.
* @param internalIdMap - Map from CDP integer `weakLocalObjectReference` to BiDi UUID
* `internalId`.
*/
protected serializeForBiDi(deepSerializedValue: Protocol.Runtime.DeepSerializedValue, internalIdMap: Map<number, string>): Script.RemoteValue;
get realmId(): Script.Realm;
get executionContextId(): Protocol.Runtime.ExecutionContextId;
get origin(): string;
get source(): Script.Source;
get cdpClient(): CdpClient;
abstract get associatedBrowsingContexts(): BrowsingContextImpl[];
abstract get realmType(): Script.RealmType;
protected get baseInfo(): Script.BaseRealmInfo;
abstract get realmInfo(): Script.RealmInfo;
evaluate(expression: string, awaitPromise: boolean, resultOwnership?: Script.ResultOwnership, serializationOptions?: Script.SerializationOptions, userActivation?: boolean, includeCommandLineApi?: boolean): Promise<Script.EvaluateResult>;
protected initialize(): void;
/**
* Serializes a given CDP object into BiDi, keeping references in the
* target's `globalThis`.
*/
serializeCdpObject(cdpRemoteObject: Protocol.Runtime.RemoteObject, resultOwnership: Script.ResultOwnership): Promise<Script.RemoteValue>;
/**
* Gets the string representation of an object. This is equivalent to
* calling `toString()` on the object value.
*/
stringifyObject(cdpRemoteObject: Protocol.Runtime.RemoteObject): Promise<string>;
callFunction(functionDeclaration: string, awaitPromise: boolean, thisLocalValue?: Script.LocalValue, argumentsLocalValues?: Script.LocalValue[], resultOwnership?: Script.ResultOwnership, serializationOptions?: Script.SerializationOptions, userActivation?: boolean): Promise<Script.EvaluateResult>;
deserializeForCdp(localValue: Script.LocalValue): Promise<Protocol.Runtime.CallArgument>;
disown(handle: Script.Handle): Promise<void>;
dispose(): void;
}
@@ -0,0 +1,481 @@
import { ChromiumBidi, NoSuchHandleException, } from '../../../protocol/protocol.js';
import { LogType } from '../../../utils/log.js';
import { uuidv4 } from '../../../utils/uuid.js';
import { ChannelProxy } from './ChannelProxy.js';
export class Realm {
#cdpClient;
#eventManager;
#executionContextId;
#logger;
#origin;
#realmId;
realmStorage;
constructor(cdpClient, eventManager, executionContextId, logger, origin, realmId, realmStorage) {
this.#cdpClient = cdpClient;
this.#eventManager = eventManager;
this.#executionContextId = executionContextId;
this.#logger = logger;
this.#origin = origin;
this.#realmId = realmId;
this.realmStorage = realmStorage;
this.realmStorage.addRealm(this);
}
cdpToBidiValue(cdpValue, resultOwnership) {
const bidiValue = this.serializeForBiDi(cdpValue.result.deepSerializedValue, new Map());
if (cdpValue.result.objectId) {
const objectId = cdpValue.result.objectId;
if (resultOwnership === "root" /* Script.ResultOwnership.Root */) {
// Extend BiDi value with `handle` based on required `resultOwnership`
// and CDP response but not on the actual BiDi type.
bidiValue.handle = objectId;
// Remember all the handles sent to client.
this.realmStorage.knownHandlesToRealmMap.set(objectId, this.realmId);
}
else {
// No need to await for the object to be released.
void this.#releaseObject(objectId).catch((error) => this.#logger?.(LogType.debugError, error));
}
}
return bidiValue;
}
isHidden() {
return false;
}
/**
* Relies on the CDP to implement proper BiDi serialization, except:
* * CDP integer property `backendNodeId` is replaced with `sharedId` of
* `{documentId}_element_{backendNodeId}`;
* * CDP integer property `weakLocalObjectReference` is replaced with UUID `internalId`
* using unique-per serialization `internalIdMap`.
* * CDP type `platformobject` is replaced with `object`.
* @param deepSerializedValue - CDP value to be converted to BiDi.
* @param internalIdMap - Map from CDP integer `weakLocalObjectReference` to BiDi UUID
* `internalId`.
*/
serializeForBiDi(deepSerializedValue, internalIdMap) {
if (Object.hasOwn(deepSerializedValue, 'weakLocalObjectReference')) {
const weakLocalObjectReference = deepSerializedValue.weakLocalObjectReference;
if (!internalIdMap.has(weakLocalObjectReference)) {
internalIdMap.set(weakLocalObjectReference, uuidv4());
}
deepSerializedValue.internalId = internalIdMap.get(weakLocalObjectReference);
delete deepSerializedValue['weakLocalObjectReference'];
}
if (deepSerializedValue.type === 'node' &&
deepSerializedValue.value &&
Object.hasOwn(deepSerializedValue.value, 'frameId')) {
// `frameId` is not needed in BiDi as it is not yet specified.
delete deepSerializedValue.value['frameId'];
}
// Platform object is a special case. It should have only `{type: object}`
// without `value` field.
if (deepSerializedValue.type === 'platformobject') {
return { type: 'object' };
}
const bidiValue = deepSerializedValue.value;
if (bidiValue === undefined) {
return deepSerializedValue;
}
// Recursively update the nested values.
if (['array', 'set', 'htmlcollection', 'nodelist'].includes(deepSerializedValue.type)) {
for (const i in bidiValue) {
bidiValue[i] = this.serializeForBiDi(bidiValue[i], internalIdMap);
}
}
if (['object', 'map'].includes(deepSerializedValue.type)) {
for (const i in bidiValue) {
bidiValue[i] = [
this.serializeForBiDi(bidiValue[i][0], internalIdMap),
this.serializeForBiDi(bidiValue[i][1], internalIdMap),
];
}
}
return deepSerializedValue;
}
get realmId() {
return this.#realmId;
}
get executionContextId() {
return this.#executionContextId;
}
get origin() {
return this.#origin;
}
get source() {
return {
realm: this.realmId,
};
}
get cdpClient() {
return this.#cdpClient;
}
get baseInfo() {
return {
realm: this.realmId,
origin: this.origin,
};
}
async evaluate(expression, awaitPromise, resultOwnership = "none" /* Script.ResultOwnership.None */, serializationOptions = {}, userActivation = false, includeCommandLineApi = false) {
const cdpEvaluateResult = await this.cdpClient.sendCommand('Runtime.evaluate', {
contextId: this.executionContextId,
expression,
awaitPromise,
serializationOptions: Realm.#getSerializationOptions("deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */, serializationOptions),
userGesture: userActivation,
includeCommandLineAPI: includeCommandLineApi,
});
if (cdpEvaluateResult.exceptionDetails) {
return await this.#getExceptionResult(cdpEvaluateResult.exceptionDetails, 0, resultOwnership);
}
return {
realm: this.realmId,
result: this.cdpToBidiValue(cdpEvaluateResult, resultOwnership),
type: 'success',
};
}
#registerEvent(event) {
if (this.associatedBrowsingContexts.length === 0) {
this.#eventManager.registerGlobalEvent(event);
}
else {
for (const browsingContext of this.associatedBrowsingContexts) {
this.#eventManager.registerEvent(event, browsingContext.id);
}
}
}
initialize() {
if (!this.isHidden()) {
// Report only not-hidden realms.
this.#registerEvent({
type: 'event',
method: ChromiumBidi.Script.EventNames.RealmCreated,
params: this.realmInfo,
});
}
}
/**
* Serializes a given CDP object into BiDi, keeping references in the
* target's `globalThis`.
*/
async serializeCdpObject(cdpRemoteObject, resultOwnership) {
// TODO: if the object is a primitive, return it directly without CDP roundtrip.
const argument = Realm.#cdpRemoteObjectToCallArgument(cdpRemoteObject);
const cdpValue = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((remoteObject) => remoteObject),
awaitPromise: false,
arguments: [argument],
serializationOptions: {
serialization: "deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */,
},
executionContextId: this.executionContextId,
});
return this.cdpToBidiValue(cdpValue, resultOwnership);
}
static #cdpRemoteObjectToCallArgument(cdpRemoteObject) {
if (cdpRemoteObject.objectId !== undefined) {
return { objectId: cdpRemoteObject.objectId };
}
if (cdpRemoteObject.unserializableValue !== undefined) {
return { unserializableValue: cdpRemoteObject.unserializableValue };
}
return { value: cdpRemoteObject.value };
}
/**
* Gets the string representation of an object. This is equivalent to
* calling `toString()` on the object value.
*/
async stringifyObject(cdpRemoteObject) {
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((remoteObject) => String(remoteObject)),
awaitPromise: false,
arguments: [cdpRemoteObject],
returnByValue: true,
executionContextId: this.executionContextId,
});
return result.value;
}
async #flattenKeyValuePairs(mappingLocalValue) {
const keyValueArray = await Promise.all(mappingLocalValue.map(async ([key, value]) => {
let keyArg;
if (typeof key === 'string') {
// Key is a string.
keyArg = { value: key };
}
else {
// Key is a serialized value.
keyArg = await this.deserializeForCdp(key);
}
const valueArg = await this.deserializeForCdp(value);
return [keyArg, valueArg];
}));
return keyValueArray.flat();
}
async #flattenValueList(listLocalValue) {
return await Promise.all(listLocalValue.map((localValue) => this.deserializeForCdp(localValue)));
}
async #serializeCdpExceptionDetails(cdpExceptionDetails, lineOffset, resultOwnership) {
const callFrames = cdpExceptionDetails.stackTrace?.callFrames.map((frame) => ({
url: frame.url,
functionName: frame.functionName,
lineNumber: frame.lineNumber - lineOffset,
columnNumber: frame.columnNumber,
})) ?? [];
// Exception should always be there.
const exception = cdpExceptionDetails.exception;
return {
exception: await this.serializeCdpObject(exception, resultOwnership),
columnNumber: cdpExceptionDetails.columnNumber,
lineNumber: cdpExceptionDetails.lineNumber - lineOffset,
stackTrace: {
callFrames,
},
text: (await this.stringifyObject(exception)) || cdpExceptionDetails.text,
};
}
async callFunction(functionDeclaration, awaitPromise, thisLocalValue = {
type: 'undefined',
}, argumentsLocalValues = [], resultOwnership = "none" /* Script.ResultOwnership.None */, serializationOptions = {}, userActivation = false) {
const callFunctionAndSerializeScript = `(...args) => {
function callFunction(f, args) {
const deserializedThis = args.shift();
const deserializedArgs = args;
return f.apply(deserializedThis, deserializedArgs);
}
return callFunction((
${functionDeclaration}
), args);
}`;
const thisAndArgumentsList = [
await this.deserializeForCdp(thisLocalValue),
...(await Promise.all(argumentsLocalValues.map(async (argumentLocalValue) => await this.deserializeForCdp(argumentLocalValue)))),
];
let cdpCallFunctionResult;
try {
cdpCallFunctionResult = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: callFunctionAndSerializeScript,
awaitPromise,
arguments: thisAndArgumentsList,
serializationOptions: Realm.#getSerializationOptions("deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */, serializationOptions),
executionContextId: this.executionContextId,
userGesture: userActivation,
});
}
catch (error) {
// Heuristic to determine if the problem is in the argument.
// The check can be done on the `deserialization` step, but this approach
// helps to save round-trips.
if (error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
[
'Could not find object with given id',
'Argument should belong to the same JavaScript world as target object',
'Invalid remote object id',
].includes(error.message)) {
throw new NoSuchHandleException('Handle was not found.');
}
throw error;
}
if (cdpCallFunctionResult.exceptionDetails) {
return await this.#getExceptionResult(cdpCallFunctionResult.exceptionDetails, 1, resultOwnership);
}
return {
type: 'success',
result: this.cdpToBidiValue(cdpCallFunctionResult, resultOwnership),
realm: this.realmId,
};
}
async deserializeForCdp(localValue) {
if ('handle' in localValue && localValue.handle) {
return { objectId: localValue.handle };
// We tried to find a handle value but failed
// This allows us to have exhaustive switch on `localValue.type`
}
else if ('handle' in localValue || 'sharedId' in localValue) {
throw new NoSuchHandleException('Handle was not found.');
}
switch (localValue.type) {
case 'undefined':
return { unserializableValue: 'undefined' };
case 'null':
return { unserializableValue: 'null' };
case 'string':
return { value: localValue.value };
case 'number':
if (localValue.value === 'NaN') {
return { unserializableValue: 'NaN' };
}
else if (localValue.value === '-0') {
return { unserializableValue: '-0' };
}
else if (localValue.value === 'Infinity') {
return { unserializableValue: 'Infinity' };
}
else if (localValue.value === '-Infinity') {
return { unserializableValue: '-Infinity' };
}
return {
value: localValue.value,
};
case 'boolean':
return { value: Boolean(localValue.value) };
case 'bigint':
return {
unserializableValue: `BigInt(${JSON.stringify(localValue.value)})`,
};
case 'date':
return {
unserializableValue: `new Date(Date.parse(${JSON.stringify(localValue.value)}))`,
};
case 'regexp':
return {
unserializableValue: `new RegExp(${JSON.stringify(localValue.value.pattern)}, ${JSON.stringify(localValue.value.flags)})`,
};
case 'map': {
// TODO: If none of the nested keys and values has a remote
// reference, serialize to `unserializableValue` without CDP roundtrip.
const keyValueArray = await this.#flattenKeyValuePairs(localValue.value);
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((...args) => {
const result = new Map();
for (let i = 0; i < args.length; i += 2) {
result.set(args[i], args[i + 1]);
}
return result;
}),
awaitPromise: false,
arguments: keyValueArray,
returnByValue: false,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `result.objectId` after using.
return { objectId: result.objectId };
}
case 'object': {
// TODO: If none of the nested keys and values has a remote
// reference, serialize to `unserializableValue` without CDP roundtrip.
const keyValueArray = await this.#flattenKeyValuePairs(localValue.value);
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((...args) => {
const result = {};
for (let i = 0; i < args.length; i += 2) {
// Key should be either `string`, `number`, or `symbol`.
const key = args[i];
result[key] = args[i + 1];
}
return result;
}),
awaitPromise: false,
arguments: keyValueArray,
returnByValue: false,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `result.objectId` after using.
return { objectId: result.objectId };
}
case 'array': {
// TODO: If none of the nested items has a remote reference,
// serialize to `unserializableValue` without CDP roundtrip.
const args = await this.#flattenValueList(localValue.value);
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((...args) => args),
awaitPromise: false,
arguments: args,
returnByValue: false,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `result.objectId` after using.
return { objectId: result.objectId };
}
case 'set': {
// TODO: if none of the nested items has a remote reference,
// serialize to `unserializableValue` without CDP roundtrip.
const args = await this.#flattenValueList(localValue.value);
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((...args) => new Set(args)),
awaitPromise: false,
arguments: args,
returnByValue: false,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `result.objectId` after using.
return { objectId: result.objectId };
}
case 'channel': {
const channelProxy = new ChannelProxy(localValue.value, this.#logger);
const channelProxySendMessageHandle = await channelProxy.init(this, this.#eventManager);
return { objectId: channelProxySendMessageHandle };
}
// TODO(#375): Dispose of nested objects.
}
// Intentionally outside to handle unknown types
throw new Error(`Value ${JSON.stringify(localValue)} is not deserializable.`);
}
async #getExceptionResult(exceptionDetails, lineOffset, resultOwnership) {
return {
exceptionDetails: await this.#serializeCdpExceptionDetails(exceptionDetails, lineOffset, resultOwnership),
realm: this.realmId,
type: 'exception',
};
}
static #getSerializationOptions(serialization, serializationOptions) {
return {
serialization,
additionalParameters: Realm.#getAdditionalSerializationParameters(serializationOptions),
...Realm.#getMaxObjectDepth(serializationOptions),
};
}
static #getAdditionalSerializationParameters(serializationOptions) {
const additionalParameters = {};
if (serializationOptions.maxDomDepth !== undefined) {
additionalParameters['maxNodeDepth'] =
serializationOptions.maxDomDepth === null
? 1000
: serializationOptions.maxDomDepth;
}
if (serializationOptions.includeShadowTree !== undefined) {
additionalParameters['includeShadowTree'] =
serializationOptions.includeShadowTree;
}
return additionalParameters;
}
static #getMaxObjectDepth(serializationOptions) {
return serializationOptions.maxObjectDepth === undefined ||
serializationOptions.maxObjectDepth === null
? {}
: { maxDepth: serializationOptions.maxObjectDepth };
}
async #releaseObject(handle) {
try {
await this.cdpClient.sendCommand('Runtime.releaseObject', {
objectId: handle,
});
}
catch (error) {
// Heuristic to determine if the problem is in the unknown handler.
// Ignore the error if so.
if (!(error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
error.message === 'Invalid remote object id')) {
throw error;
}
}
}
async disown(handle) {
// Disowning an object from different realm does nothing.
if (this.realmStorage.knownHandlesToRealmMap.get(handle) !== this.realmId) {
return;
}
await this.#releaseObject(handle);
this.realmStorage.knownHandlesToRealmMap.delete(handle);
}
dispose() {
if (!this.isHidden()) {
this.#registerEvent({
type: 'event',
method: ChromiumBidi.Script.EventNames.RealmDestroyed,
params: {
realm: this.realmId,
},
});
}
}
}
//# sourceMappingURL=Realm.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,45 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Protocol } from 'devtools-protocol';
import { type BrowsingContext, type Script } from '../../../protocol/protocol.js';
import type { Realm } from './Realm.js';
interface RealmFilter {
realmId?: Script.Realm;
browsingContextId?: BrowsingContext.BrowsingContext;
executionContextId?: Protocol.Runtime.ExecutionContextId;
origin?: string;
type?: Script.RealmType;
sandbox?: string | null;
cdpSessionId?: Protocol.Target.SessionID;
isHidden?: boolean;
}
/** Container class for browsing realms. */
export declare class RealmStorage {
#private;
/** List of the internal sandboxed realms which should not be reported to the user. */
readonly hiddenSandboxes: Set<string | undefined>;
get knownHandlesToRealmMap(): Map<string, string>;
addRealm(realm: Realm): void;
/** Finds all realms that match the given filter. */
findRealms(filter: RealmFilter): Realm[];
findRealm(filter: RealmFilter): Realm | undefined;
/** Gets the only realm that matches the given filter, if any, otherwise throws. */
getRealm(filter: RealmFilter): Realm;
/** Deletes all realms that match the given filter. */
deleteRealms(filter: RealmFilter): void;
}
export {};
@@ -0,0 +1,78 @@
import { NoSuchFrameException, } from '../../../protocol/protocol.js';
import { WindowRealm } from './WindowRealm.js';
/** Container class for browsing realms. */
export class RealmStorage {
/** Tracks handles and their realms sent to the client. */
#knownHandlesToRealmMap = new Map();
/** Map from realm ID to Realm. */
#realmMap = new Map();
/** List of the internal sandboxed realms which should not be reported to the user. */
hiddenSandboxes = new Set();
get knownHandlesToRealmMap() {
return this.#knownHandlesToRealmMap;
}
addRealm(realm) {
this.#realmMap.set(realm.realmId, realm);
}
/** Finds all realms that match the given filter. */
findRealms(filter) {
const sandboxFilterValue = filter.sandbox === null ? undefined : filter.sandbox;
return Array.from(this.#realmMap.values()).filter((realm) => {
if (filter.realmId !== undefined && filter.realmId !== realm.realmId) {
return false;
}
if (filter.browsingContextId !== undefined &&
!realm.associatedBrowsingContexts
.map((browsingContext) => browsingContext.id)
.includes(filter.browsingContextId)) {
return false;
}
if (filter.sandbox !== undefined &&
(!(realm instanceof WindowRealm) ||
sandboxFilterValue !== realm.sandbox)) {
return false;
}
if (filter.executionContextId !== undefined &&
filter.executionContextId !== realm.executionContextId) {
return false;
}
if (filter.origin !== undefined && filter.origin !== realm.origin) {
return false;
}
if (filter.type !== undefined && filter.type !== realm.realmType) {
return false;
}
if (filter.cdpSessionId !== undefined &&
filter.cdpSessionId !== realm.cdpClient.sessionId) {
return false;
}
if (filter.isHidden !== undefined &&
filter.isHidden !== realm.isHidden()) {
return false;
}
return true;
});
}
findRealm(filter) {
return this.findRealms(filter)[0];
}
/** Gets the only realm that matches the given filter, if any, otherwise throws. */
getRealm(filter) {
const maybeRealm = this.findRealm(filter);
if (maybeRealm === undefined) {
throw new NoSuchFrameException(`Realm ${JSON.stringify(filter)} not found`);
}
return maybeRealm;
}
/** Deletes all realms that match the given filter. */
deleteRealms(filter) {
this.findRealms(filter).map((realm) => {
realm.dispose();
this.#realmMap.delete(realm.realmId);
Array.from(this.knownHandlesToRealmMap.entries())
.filter(([, r]) => r === realm.realmId)
.map(([handle]) => this.knownHandlesToRealmMap.delete(handle));
});
}
}
//# sourceMappingURL=RealmStorage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"RealmStorage.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/script/RealmStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type EmptyResult, type Script } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { PreloadScriptStorage } from './PreloadScriptStorage.js';
import type { RealmStorage } from './RealmStorage.js';
export declare class ScriptProcessor {
#private;
constructor(eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, preloadScriptStorage: PreloadScriptStorage, userContextStorage: UserContextStorage, logger?: LoggerFn);
addPreloadScript(params: Script.AddPreloadScriptParameters): Promise<Script.AddPreloadScriptResult>;
removePreloadScript(params: Script.RemovePreloadScriptParameters): Promise<EmptyResult>;
callFunction(params: Script.CallFunctionParameters): Promise<Script.EvaluateResult>;
evaluate(params: Script.EvaluateParameters): Promise<Script.EvaluateResult>;
disown(params: Script.DisownParameters): Promise<EmptyResult>;
getRealms(params: Script.GetRealmsParameters): Script.GetRealmsResult;
}
@@ -0,0 +1,132 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChromiumBidi, InvalidArgumentException, } from '../../../protocol/protocol.js';
import { PreloadScript } from './PreloadScript.js';
export class ScriptProcessor {
#eventManager;
#browsingContextStorage;
#realmStorage;
#preloadScriptStorage;
#userContextStorage;
#logger;
constructor(eventManager, browsingContextStorage, realmStorage, preloadScriptStorage, userContextStorage, logger) {
this.#browsingContextStorage = browsingContextStorage;
this.#realmStorage = realmStorage;
this.#preloadScriptStorage = preloadScriptStorage;
this.#userContextStorage = userContextStorage;
this.#logger = logger;
this.#eventManager = eventManager;
this.#eventManager.addSubscribeHook(ChromiumBidi.Script.EventNames.RealmCreated, this.#onRealmCreatedSubscribeHook.bind(this));
}
#onRealmCreatedSubscribeHook(contextId) {
const context = this.#browsingContextStorage.getContext(contextId);
const contextsToReport = [
context,
...this.#browsingContextStorage.getContext(contextId).allChildren,
];
const realms = new Set();
for (const reportContext of contextsToReport) {
const realmsForContext = this.#realmStorage.findRealms({
browsingContextId: reportContext.id,
});
for (const realm of realmsForContext) {
realms.add(realm);
}
}
for (const realm of realms) {
this.#eventManager.registerEvent({
type: 'event',
method: ChromiumBidi.Script.EventNames.RealmCreated,
params: realm.realmInfo,
}, context.id);
}
return Promise.resolve();
}
async addPreloadScript(params) {
if (params.userContexts?.length && params.contexts?.length) {
throw new InvalidArgumentException('Both userContexts and contexts cannot be specified.');
}
const userContexts = await this.#userContextStorage.verifyUserContextIdList(params.userContexts ?? []);
const browsingContexts = this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
const preloadScript = new PreloadScript(params, this.#logger);
this.#preloadScriptStorage.add(preloadScript);
let contextsToRunIn = [];
if (userContexts.size) {
contextsToRunIn = this.#browsingContextStorage
.getTopLevelContexts()
.filter((context) => {
return userContexts.has(context.userContext);
});
}
else if (browsingContexts.size) {
contextsToRunIn = [...browsingContexts.values()];
}
else {
contextsToRunIn = this.#browsingContextStorage.getTopLevelContexts();
}
const cdpTargets = new Set(contextsToRunIn.map((context) => context.cdpTarget));
await preloadScript.initInTargets(cdpTargets, false);
return {
script: preloadScript.id,
};
}
async removePreloadScript(params) {
const { script: id } = params;
const script = this.#preloadScriptStorage.getPreloadScript(id);
await script.remove();
this.#preloadScriptStorage.remove(id);
return {};
}
async callFunction(params) {
const realm = await this.#getRealm(params.target);
return await realm.callFunction(params.functionDeclaration, params.awaitPromise, params.this, params.arguments, params.resultOwnership, params.serializationOptions, params.userActivation);
}
async evaluate(params) {
const realm = await this.#getRealm(params.target);
return await realm.evaluate(params.expression, params.awaitPromise, params.resultOwnership, params.serializationOptions, params.userActivation);
}
async disown(params) {
const realm = await this.#getRealm(params.target);
await Promise.all(params.handles.map(async (handle) => await realm.disown(handle)));
return {};
}
getRealms(params) {
if (params.context !== undefined) {
// Make sure the context is known.
this.#browsingContextStorage.getContext(params.context);
}
const realms = this.#realmStorage
.findRealms({
browsingContextId: params.context,
type: params.type,
isHidden: false,
})
.map((realm) => realm.realmInfo);
return { realms };
}
async #getRealm(target) {
if ('context' in target) {
const context = this.#browsingContextStorage.getContext(target.context);
return await context.getOrCreateUserSandbox(target.sandbox);
}
return this.#realmStorage.getRealm({
realmId: target.realm,
isHidden: false,
});
}
}
//# sourceMappingURL=ScriptProcessor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ScriptProcessor.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/script/ScriptProcessor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
export declare function getSharedId(frameId: string, documentId: string, backendNodeId: number): string;
export declare function parseSharedId(sharedId: string): {
frameId: string | undefined;
documentId: string;
backendNodeId: number;
} | null;
@@ -0,0 +1,70 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const SHARED_ID_DIVIDER = '_element_';
export function getSharedId(frameId, documentId, backendNodeId) {
return `f.${frameId}.d.${documentId}.e.${backendNodeId}`;
}
function parseLegacySharedId(sharedId) {
const match = sharedId.match(new RegExp(`(.*)${SHARED_ID_DIVIDER}(.*)`));
if (!match) {
// SharedId is incorrectly formatted.
return null;
}
const documentId = match[1];
const elementId = match[2];
if (documentId === undefined || elementId === undefined) {
return null;
}
const backendNodeId = parseInt(elementId ?? '');
if (isNaN(backendNodeId)) {
return null;
}
return {
documentId,
backendNodeId,
};
}
export function parseSharedId(sharedId) {
// TODO: remove legacy check once ChromeDriver provides sharedId in the new format.
const legacyFormattedSharedId = parseLegacySharedId(sharedId);
if (legacyFormattedSharedId !== null) {
return { ...legacyFormattedSharedId, frameId: undefined };
}
const match = sharedId.match(/f\.(.*)\.d\.(.*)\.e\.([0-9]*)/);
if (!match) {
// SharedId is incorrectly formatted.
return null;
}
const frameId = match[1];
const documentId = match[2];
const elementId = match[3];
if (frameId === undefined ||
documentId === undefined ||
elementId === undefined) {
return null;
}
const backendNodeId = parseInt(elementId ?? '');
if (isNaN(backendNodeId)) {
return null;
}
return {
frameId,
documentId,
backendNodeId,
};
}
//# sourceMappingURL=SharedId.js.map
@@ -0,0 +1 @@
{"version":3,"file":"SharedId.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/script/SharedId.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAEtC,MAAM,UAAU,WAAW,CACzB,OAAe,EACf,UAAkB,EAClB,aAAqB;IAErB,OAAO,KAAK,OAAO,MAAM,UAAU,MAAM,aAAa,EAAE,CAAC;AAC3D,CAAC;AAED,SAAS,mBAAmB,CAAC,QAAgB;IAI3C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,iBAAiB,MAAM,CAAC,CAAC,CAAC;IACzE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,qCAAqC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAE3B,IAAI,UAAU,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,aAAa,GAAG,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAChD,IAAI,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,UAAU;QACV,aAAa;KACd,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAgB;IAQ5C,mFAAmF;IACnF,MAAM,uBAAuB,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC9D,IAAI,uBAAuB,KAAK,IAAI,EAAE,CAAC;QACrC,OAAO,EAAC,GAAG,uBAAuB,EAAE,OAAO,EAAE,SAAS,EAAC,CAAC;IAC1D,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAC9D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,qCAAqC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAE3B,IACE,OAAO,KAAK,SAAS;QACrB,UAAU,KAAK,SAAS;QACxB,SAAS,KAAK,SAAS,EACvB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,aAAa,GAAG,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAChD,IAAI,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO;QACP,UAAU;QACV,aAAa;KACd,CAAC;AACJ,CAAC"}
@@ -0,0 +1,43 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Protocol } from 'devtools-protocol';
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { type BrowsingContext, type Script } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
import { Realm } from './Realm.js';
import type { RealmStorage } from './RealmStorage.js';
export declare class WindowRealm extends Realm {
#private;
readonly sandbox: string | undefined;
constructor(browsingContextId: BrowsingContext.BrowsingContext, browsingContextStorage: BrowsingContextStorage, cdpClient: CdpClient, eventManager: EventManager, executionContextId: Protocol.Runtime.ExecutionContextId, logger: LoggerFn | undefined, origin: string, realmId: Script.Realm, realmStorage: RealmStorage, sandbox: string | undefined);
get browsingContext(): BrowsingContextImpl;
/**
* Do not expose to user hidden realms.
*/
isHidden(): boolean;
get associatedBrowsingContexts(): [BrowsingContextImpl];
get realmType(): 'window';
get realmInfo(): Script.WindowRealmInfo;
get source(): Script.Source;
serializeForBiDi(deepSerializedValue: Protocol.Runtime.DeepSerializedValue, internalIdMap: Map<number, string>): Script.RemoteValue;
deserializeForCdp(localValue: Script.LocalValue): Promise<Protocol.Runtime.CallArgument>;
evaluate(expression: string, awaitPromise: boolean, resultOwnership: Script.ResultOwnership, serializationOptions: Script.SerializationOptions, userActivation?: boolean, includeCommandLineApi?: boolean): Promise<Script.EvaluateResult>;
callFunction(functionDeclaration: string, awaitPromise: boolean, thisLocalValue: Script.LocalValue, argumentsLocalValues: Script.LocalValue[], resultOwnership: Script.ResultOwnership, serializationOptions: Script.SerializationOptions, userActivation?: boolean): Promise<Script.EvaluateResult>;
}
@@ -0,0 +1,142 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NoSuchNodeException, UnknownErrorException, } from '../../../protocol/protocol.js';
import { Realm } from './Realm.js';
import { getSharedId, parseSharedId } from './SharedId.js';
export class WindowRealm extends Realm {
#browsingContextId;
#browsingContextStorage;
sandbox;
constructor(browsingContextId, browsingContextStorage, cdpClient, eventManager, executionContextId, logger, origin, realmId, realmStorage, sandbox) {
super(cdpClient, eventManager, executionContextId, logger, origin, realmId, realmStorage);
this.#browsingContextId = browsingContextId;
this.#browsingContextStorage = browsingContextStorage;
this.sandbox = sandbox;
this.initialize();
}
#getBrowsingContextId(navigableId) {
const maybeBrowsingContext = this.#browsingContextStorage
.getAllContexts()
.find((context) => context.navigableId === navigableId);
return maybeBrowsingContext?.id ?? 'UNKNOWN';
}
get browsingContext() {
return this.#browsingContextStorage.getContext(this.#browsingContextId);
}
/**
* Do not expose to user hidden realms.
*/
isHidden() {
return this.realmStorage.hiddenSandboxes.has(this.sandbox);
}
get associatedBrowsingContexts() {
return [this.browsingContext];
}
get realmType() {
return 'window';
}
get realmInfo() {
return {
...this.baseInfo,
type: this.realmType,
context: this.#browsingContextId,
sandbox: this.sandbox,
};
}
get source() {
return {
realm: this.realmId,
context: this.browsingContext.id,
};
}
serializeForBiDi(deepSerializedValue, internalIdMap) {
const bidiValue = deepSerializedValue.value;
if (deepSerializedValue.type === 'node' && bidiValue !== undefined) {
if (Object.hasOwn(bidiValue, 'backendNodeId')) {
let navigableId = this.browsingContext.navigableId ?? 'UNKNOWN';
if (Object.hasOwn(bidiValue, 'loaderId')) {
// `loaderId` should be always there after ~2024-03-05, when
// https://crrev.com/c/5116240 reaches stable.
// TODO: remove the check after the date.
navigableId = bidiValue.loaderId;
delete bidiValue['loaderId'];
}
deepSerializedValue.sharedId =
getSharedId(this.#getBrowsingContextId(navigableId), navigableId, bidiValue.backendNodeId);
delete bidiValue['backendNodeId'];
}
if (Object.hasOwn(bidiValue, 'children')) {
for (const i in bidiValue.children) {
bidiValue.children[i] = this.serializeForBiDi(bidiValue.children[i], internalIdMap);
}
}
if (Object.hasOwn(bidiValue, 'shadowRoot') &&
bidiValue.shadowRoot !== null) {
bidiValue.shadowRoot = this.serializeForBiDi(bidiValue.shadowRoot, internalIdMap);
}
// `namespaceURI` can be is either `null` or non-empty string.
if (bidiValue.namespaceURI === '') {
bidiValue.namespaceURI = null;
}
}
return super.serializeForBiDi(deepSerializedValue, internalIdMap);
}
async deserializeForCdp(localValue) {
if ('sharedId' in localValue && localValue.sharedId) {
const parsedSharedId = parseSharedId(localValue.sharedId);
if (parsedSharedId === null) {
throw new NoSuchNodeException(`SharedId "${localValue.sharedId}" was not found.`);
}
const { documentId, backendNodeId } = parsedSharedId;
// TODO: add proper validation if the element is accessible from the current realm.
if (this.browsingContext.navigableId !== documentId) {
throw new NoSuchNodeException(`SharedId "${localValue.sharedId}" belongs to different document. Current document is ${this.browsingContext.navigableId}.`);
}
try {
const { object } = await this.cdpClient.sendCommand('DOM.resolveNode', {
backendNodeId,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `obj.object.objectId` after using.
return { objectId: object.objectId };
}
catch (error) {
// Heuristic to detect "no such node" exception. Based on the specific
// CDP implementation.
if (error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
error.message === 'No node with given id found') {
throw new NoSuchNodeException(`SharedId "${localValue.sharedId}" was not found.`);
}
throw new UnknownErrorException(error.message, error.stack);
}
}
return await super.deserializeForCdp(localValue);
}
async evaluate(expression, awaitPromise, resultOwnership, serializationOptions, userActivation, includeCommandLineApi) {
await this.#browsingContextStorage
.getContext(this.#browsingContextId)
.targetUnblockedOrThrow();
return await super.evaluate(expression, awaitPromise, resultOwnership, serializationOptions, userActivation, includeCommandLineApi);
}
async callFunction(functionDeclaration, awaitPromise, thisLocalValue, argumentsLocalValues, resultOwnership, serializationOptions, userActivation) {
await this.#browsingContextStorage
.getContext(this.#browsingContextId)
.targetUnblockedOrThrow();
return await super.callFunction(functionDeclaration, awaitPromise, thisLocalValue, argumentsLocalValues, resultOwnership, serializationOptions, userActivation);
}
}
//# sourceMappingURL=WindowRealm.js.map
@@ -0,0 +1 @@
{"version":3,"file":"WindowRealm.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/script/WindowRealm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Protocol } from 'devtools-protocol';
import type { CdpClient } from '../../../cdp/CdpClient.js';
import type { Script } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import type { EventManager } from '../session/EventManager.js';
import { Realm } from './Realm.js';
import type { RealmStorage } from './RealmStorage.js';
export type WorkerRealmType = Pick<Script.SharedWorkerRealmInfo | Script.DedicatedWorkerRealmInfo | Script.ServiceWorkerRealmInfo, 'type'>['type'];
export declare class WorkerRealm extends Realm {
#private;
constructor(cdpClient: CdpClient, eventManager: EventManager, executionContextId: Protocol.Runtime.ExecutionContextId, logger: LoggerFn | undefined, origin: string, ownerRealms: Realm[], realmId: Script.Realm, realmStorage: RealmStorage, realmType: WorkerRealmType);
get associatedBrowsingContexts(): BrowsingContextImpl[];
get realmType(): WorkerRealmType;
get source(): Script.Source;
get realmInfo(): Script.RealmInfo;
}
@@ -0,0 +1,66 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Realm } from './Realm.js';
export class WorkerRealm extends Realm {
#realmType;
#ownerRealms;
constructor(cdpClient, eventManager, executionContextId, logger, origin, ownerRealms, realmId, realmStorage, realmType) {
super(cdpClient, eventManager, executionContextId, logger, origin, realmId, realmStorage);
this.#ownerRealms = ownerRealms;
this.#realmType = realmType;
this.initialize();
}
get associatedBrowsingContexts() {
return this.#ownerRealms.flatMap((realm) => realm.associatedBrowsingContexts);
}
get realmType() {
return this.#realmType;
}
get source() {
return {
realm: this.realmId,
// This is a hack to make Puppeteer able to track workers.
// TODO: remove after Puppeteer tracks workers by owners and use the base version.
context: this.associatedBrowsingContexts[0]?.id,
};
}
get realmInfo() {
const owners = this.#ownerRealms.map((realm) => realm.realmId);
const { realmType } = this;
switch (realmType) {
case 'dedicated-worker': {
const owner = owners[0];
if (owner === undefined || owners.length !== 1) {
throw new Error('Dedicated worker must have exactly one owner');
}
return {
...this.baseInfo,
type: realmType,
owners: [owner],
};
}
case 'service-worker':
case 'shared-worker': {
return {
...this.baseInfo,
type: realmType,
};
}
}
}
}
//# sourceMappingURL=WorkerRealm.js.map
@@ -0,0 +1 @@
{"version":3,"file":"WorkerRealm.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/script/WorkerRealm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAUH,OAAO,EAAC,KAAK,EAAC,MAAM,YAAY,CAAC;AAUjC,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,UAAU,CAAkB;IAC5B,YAAY,CAAU;IAE/B,YACE,SAAoB,EACpB,YAA0B,EAC1B,kBAAuD,EACvD,MAA4B,EAC5B,MAAc,EACd,WAAoB,EACpB,OAAqB,EACrB,YAA0B,EAC1B,SAA0B;QAE1B,KAAK,CACH,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,MAAM,EACN,MAAM,EACN,OAAO,EACP,YAAY,CACb,CAAC;QAEF,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAE5B,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC;IAED,IAAa,0BAA0B;QACrC,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAC9B,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,0BAA0B,CAC5C,CAAC;IACJ,CAAC;IAED,IAAa,SAAS;QACpB,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,IAAa,MAAM;QACjB,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,OAAO;YACnB,0DAA0D;YAC1D,kFAAkF;YAClF,OAAO,EAAE,IAAI,CAAC,0BAA0B,CAAC,CAAC,CAAC,EAAE,EAAE;SAChD,CAAC;IACJ,CAAC;IAED,IAAa,SAAS;QACpB,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC/D,MAAM,EAAC,SAAS,EAAC,GAAG,IAAI,CAAC;QACzB,QAAQ,SAAS,EAAE,CAAC;YAClB,KAAK,kBAAkB,CAAC,CAAC,CAAC;gBACxB,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;gBACxB,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC/C,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;gBAClE,CAAC;gBACD,OAAO;oBACL,GAAG,IAAI,CAAC,QAAQ;oBAChB,IAAI,EAAE,SAAS;oBACf,MAAM,EAAE,CAAC,KAAK,CAAC;iBAChB,CAAC;YACJ,CAAC;YACD,KAAK,gBAAgB,CAAC;YACtB,KAAK,eAAe,CAAC,CAAC,CAAC;gBACrB,OAAO;oBACL,GAAG,IAAI,CAAC,QAAQ;oBAChB,IAAI,EAAE,SAAS;iBAChB,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,56 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { GoogChannel } from '../../../protocol/chromium-bidi.js';
import { type Browser, ChromiumBidi, type BrowsingContext } from '../../../protocol/protocol.js';
import { EventEmitter } from '../../../utils/EventEmitter.js';
import type { Result } from '../../../utils/result.js';
import { OutgoingMessage } from '../../OutgoingMessage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import { SubscriptionManager } from './SubscriptionManager.js';
export declare const enum EventManagerEvents {
Event = "event"
}
interface EventManagerEventsMap extends Record<string | symbol, unknown> {
[EventManagerEvents.Event]: {
message: Promise<Result<OutgoingMessage>>;
event: string;
};
}
/**
* Subscription item is a pair of event name and context id.
*/
export interface SubscriptionItem {
contextId: BrowsingContext.BrowsingContext;
event: ChromiumBidi.EventNames;
}
export declare class EventManager extends EventEmitter<EventManagerEventsMap> {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, userContextStorage: UserContextStorage);
get subscriptionManager(): SubscriptionManager;
addSubscribeHook(event: ChromiumBidi.EventNames, hook: (contextId: BrowsingContext.BrowsingContext) => Promise<void>): void;
registerEvent(event: ChromiumBidi.Event, contextId: BrowsingContext.BrowsingContext): void;
registerGlobalEvent(event: ChromiumBidi.Event): void;
registerPromiseEvent(event: Promise<Result<ChromiumBidi.Event>>, contextId: BrowsingContext.BrowsingContext, eventName: ChromiumBidi.EventNames): void;
registerGlobalPromiseEvent(event: Promise<Result<ChromiumBidi.Event>>, eventName: ChromiumBidi.EventNames): void;
subscribe(eventNames: ChromiumBidi.EventNames[], contextIds: BrowsingContext.BrowsingContext[], userContextIds: Browser.UserContext[], googChannel: GoogChannel): Promise<string>;
unsubscribe(eventNames: ChromiumBidi.EventNames[], googChannel: GoogChannel): Promise<void>;
unsubscribeByIds(subscriptionIds: string[]): Promise<void>;
toggleModulesIfNeeded(): Promise<void>;
clearBufferedEvents(contextId: string): void;
}
export {};
@@ -0,0 +1,265 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var _a;
import { ChromiumBidi, InvalidArgumentException, } from '../../../protocol/protocol.js';
import { Buffer } from '../../../utils/Buffer.js';
import { DefaultMap } from '../../../utils/DefaultMap.js';
import { EventEmitter } from '../../../utils/EventEmitter.js';
import { IdWrapper } from '../../../utils/IdWrapper.js';
import { OutgoingMessage } from '../../OutgoingMessage.js';
import { assertSupportedEvent } from './events.js';
import { difference, SubscriptionManager, unrollEvents, } from './SubscriptionManager.js';
class EventWrapper {
#idWrapper = new IdWrapper();
#contextId;
#event;
constructor(event, contextId) {
this.#event = event;
this.#contextId = contextId;
}
get id() {
return this.#idWrapper.id;
}
get contextId() {
return this.#contextId;
}
get event() {
return this.#event;
}
}
/**
* Maps event name to a desired buffer length.
*/
const eventBufferLength = new Map([[ChromiumBidi.Log.EventNames.LogEntryAdded, 100]]);
export class EventManager extends EventEmitter {
/**
* Maps event name to a set of contexts where this event already happened.
* Needed for getting buffered events from all the contexts in case of
* subscripting to all contexts.
*/
#eventToContextsMap = new DefaultMap(() => new Set());
/**
* Maps `eventName` + `browsingContext` to buffer. Used to get buffered events
* during subscription. Channel-agnostic.
*/
#eventBuffers = new Map();
/**
* Maps `eventName` + `browsingContext` to Map of goog:channel to last id.
* Used to avoid sending duplicated events when user
* subscribes -> unsubscribes -> subscribes.
*/
#lastMessageSent = new Map();
#subscriptionManager;
#browsingContextStorage;
/**
* Map of event name to hooks to be called when client is subscribed to the event.
*/
#subscribeHooks;
#userContextStorage;
constructor(browsingContextStorage, userContextStorage) {
super();
this.#browsingContextStorage = browsingContextStorage;
this.#userContextStorage = userContextStorage;
this.#subscriptionManager = new SubscriptionManager(browsingContextStorage);
this.#subscribeHooks = new DefaultMap(() => []);
}
get subscriptionManager() {
return this.#subscriptionManager;
}
/**
* Returns consistent key to be used to access value maps.
*/
static #getMapKey(eventName, browsingContext) {
return JSON.stringify({ eventName, browsingContext });
}
addSubscribeHook(event, hook) {
this.#subscribeHooks.get(event).push(hook);
}
registerEvent(event, contextId) {
this.registerPromiseEvent(Promise.resolve({
kind: 'success',
value: event,
}), contextId, event.method);
}
registerGlobalEvent(event) {
this.registerGlobalPromiseEvent(Promise.resolve({
kind: 'success',
value: event,
}), event.method);
}
registerPromiseEvent(event, contextId, eventName) {
const eventWrapper = new EventWrapper(event, contextId);
const sortedGoogChannels = this.#subscriptionManager.getGoogChannelsSubscribedToEvent(eventName, contextId);
this.#bufferEvent(eventWrapper, eventName);
// Send events to channels in the subscription priority.
for (const googChannel of sortedGoogChannels) {
this.emit("event" /* EventManagerEvents.Event */, {
message: OutgoingMessage.createFromPromise(event, googChannel),
event: eventName,
});
this.#markEventSent(eventWrapper, googChannel, eventName);
}
}
registerGlobalPromiseEvent(event, eventName) {
const eventWrapper = new EventWrapper(event, null);
const sortedGoogChannels = this.#subscriptionManager.getGoogChannelsSubscribedToEventGlobally(eventName);
this.#bufferEvent(eventWrapper, eventName);
// Send events to goog:channels in the subscription priority.
for (const googChannel of sortedGoogChannels) {
this.emit("event" /* EventManagerEvents.Event */, {
message: OutgoingMessage.createFromPromise(event, googChannel),
event: eventName,
});
this.#markEventSent(eventWrapper, googChannel, eventName);
}
}
async subscribe(eventNames, contextIds, userContextIds, googChannel) {
for (const name of eventNames) {
assertSupportedEvent(name);
}
if (userContextIds.length && contextIds.length) {
throw new InvalidArgumentException('Both userContexts and contexts cannot be specified.');
}
// First check if all the contexts are known.
this.#browsingContextStorage.verifyContextsList(contextIds);
// Validate user contexts.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
const unrolledEventNames = new Set(unrollEvents(eventNames));
const subscribeStepEvents = new Map();
const subscriptionNavigableIds = new Set(contextIds.length
? contextIds.map((contextId) => {
const id = this.#browsingContextStorage.findTopLevelContextId(contextId);
if (!id) {
throw new InvalidArgumentException('Invalid context id');
}
return id;
})
: this.#browsingContextStorage.getTopLevelContexts().map((c) => c.id));
for (const eventName of unrolledEventNames) {
const subscribedNavigableIds = new Set(this.#browsingContextStorage
.getTopLevelContexts()
.map((c) => c.id)
.filter((id) => {
return this.#subscriptionManager.isSubscribedTo(eventName, id);
}));
subscribeStepEvents.set(eventName, difference(subscriptionNavigableIds, subscribedNavigableIds));
}
const subscription = this.#subscriptionManager.subscribe(eventNames, contextIds, userContextIds, googChannel);
for (const eventName of subscription.eventNames) {
for (const contextId of subscriptionNavigableIds) {
for (const eventWrapper of this.#getBufferedEvents(eventName, contextId, googChannel)) {
// The order of the events is important.
this.emit("event" /* EventManagerEvents.Event */, {
message: OutgoingMessage.createFromPromise(eventWrapper.event, googChannel),
event: eventName,
});
this.#markEventSent(eventWrapper, googChannel, eventName);
}
}
}
for (const [eventName, contextIds] of subscribeStepEvents) {
for (const contextId of contextIds) {
this.#subscribeHooks.get(eventName).forEach((hook) => hook(contextId));
}
}
await this.toggleModulesIfNeeded();
return subscription.id;
}
async unsubscribe(eventNames, googChannel) {
for (const name of eventNames) {
assertSupportedEvent(name);
}
this.#subscriptionManager.unsubscribe(eventNames, googChannel);
await this.toggleModulesIfNeeded();
}
async unsubscribeByIds(subscriptionIds) {
this.#subscriptionManager.unsubscribeById(subscriptionIds);
await this.toggleModulesIfNeeded();
}
async toggleModulesIfNeeded() {
// TODO(1): Only update changed subscribers
// TODO(2): Enable for Worker Targets
await Promise.all(this.#browsingContextStorage.getAllContexts().map(async (context) => {
return await context.toggleModulesIfNeeded();
}));
}
clearBufferedEvents(contextId) {
for (const eventName of eventBufferLength.keys()) {
const bufferMapKey = _a.#getMapKey(eventName, contextId);
this.#eventBuffers.delete(bufferMapKey);
}
}
/**
* If the event is buffer-able, put it in the buffer.
*/
#bufferEvent(eventWrapper, eventName) {
if (!eventBufferLength.has(eventName)) {
// Do nothing if the event is no buffer-able.
return;
}
const bufferMapKey = _a.#getMapKey(eventName, eventWrapper.contextId);
if (!this.#eventBuffers.has(bufferMapKey)) {
this.#eventBuffers.set(bufferMapKey, new Buffer(eventBufferLength.get(eventName)));
}
this.#eventBuffers.get(bufferMapKey).add(eventWrapper);
// Add the context to the list of contexts having `eventName` events.
this.#eventToContextsMap.get(eventName).add(eventWrapper.contextId);
}
/**
* If the event is buffer-able, mark it as sent to the given contextId and goog:channel.
*/
#markEventSent(eventWrapper, googChannel, eventName) {
if (!eventBufferLength.has(eventName)) {
// Do nothing if the event is no buffer-able.
return;
}
const lastSentMapKey = _a.#getMapKey(eventName, eventWrapper.contextId);
const lastId = Math.max(this.#lastMessageSent.get(lastSentMapKey)?.get(googChannel) ?? 0, eventWrapper.id);
const googChannelMap = this.#lastMessageSent.get(lastSentMapKey);
if (googChannelMap) {
googChannelMap.set(googChannel, lastId);
}
else {
this.#lastMessageSent.set(lastSentMapKey, new Map([[googChannel, lastId]]));
}
}
/**
* Returns events which are buffered and not yet sent to the given goog:channel events.
*/
#getBufferedEvents(eventName, contextId, googChannel) {
const bufferMapKey = _a.#getMapKey(eventName, contextId);
const lastSentMessageId = this.#lastMessageSent.get(bufferMapKey)?.get(googChannel) ?? -Infinity;
const result = this.#eventBuffers
.get(bufferMapKey)
?.get()
.filter((wrapper) => wrapper.id > lastSentMessageId) ?? [];
if (contextId === null) {
// For global subscriptions, events buffered in each context should be sent back.
Array.from(this.#eventToContextsMap.get(eventName).keys())
.filter((_contextId) =>
// Events without context are already in the result.
_contextId !== null &&
// Events from deleted contexts should not be sent.
this.#browsingContextStorage.hasContext(_contextId))
.map((_contextId) => this.#getBufferedEvents(eventName, _contextId, googChannel))
.forEach((events) => result.push(...events));
}
return result.sort((e1, e2) => e1.id - e2.id);
}
}
_a = EventManager;
//# sourceMappingURL=EventManager.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CdpClient } from '../../../cdp/CdpClient.js';
import type { GoogChannel } from '../../../protocol/chromium-bidi.js';
import { type EmptyResult, Session } from '../../../protocol/protocol.js';
import type { MapperOptions } from '../../MapperOptions.js';
import type { EventManager } from './EventManager.js';
export declare class SessionProcessor {
#private;
constructor(eventManager: EventManager, browserCdpClient: CdpClient, initConnection: (opts: MapperOptions) => Promise<void>);
status(): Session.StatusResult;
new(params: Session.NewParameters): Promise<Session.NewResult>;
subscribe(params: Session.SubscribeParameters, googChannel?: GoogChannel): Promise<Session.SubscribeResult>;
unsubscribe(params: Session.UnsubscribeParameters, googChannel?: GoogChannel): Promise<EmptyResult>;
}
@@ -0,0 +1,127 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidArgumentException, } from '../../../protocol/protocol.js';
export class SessionProcessor {
#eventManager;
#browserCdpClient;
#initConnection;
#created = false;
constructor(eventManager, browserCdpClient, initConnection) {
this.#eventManager = eventManager;
this.#browserCdpClient = browserCdpClient;
this.#initConnection = initConnection;
}
status() {
return { ready: false, message: 'already connected' };
}
#mergeCapabilities(capabilitiesRequest) {
// Roughly following https://www.w3.org/TR/webdriver2/#dfn-capabilities-processing.
// Validations should already be done by the parser.
const mergedCapabilities = [];
for (const first of capabilitiesRequest.firstMatch ?? [{}]) {
const result = {
...capabilitiesRequest.alwaysMatch,
};
for (const key of Object.keys(first)) {
if (result[key] !== undefined) {
throw new InvalidArgumentException(`Capability ${key} in firstMatch is already defined in alwaysMatch`);
}
result[key] = first[key];
}
mergedCapabilities.push(result);
}
const match = mergedCapabilities.find((c) => c.browserName === 'chrome') ??
mergedCapabilities[0] ??
{};
match.unhandledPromptBehavior = this.#getUnhandledPromptBehavior(match.unhandledPromptBehavior);
return match;
}
#getUnhandledPromptBehavior(capabilityValue) {
if (capabilityValue === undefined) {
return undefined;
}
if (typeof capabilityValue === 'object') {
// Do not validate capabilities. Incorrect ones will be ignored by Mapper.
return capabilityValue;
}
if (typeof capabilityValue !== 'string') {
throw new InvalidArgumentException(`Unexpected 'unhandledPromptBehavior' type: ${typeof capabilityValue}`);
}
switch (capabilityValue) {
// `beforeUnload: accept` has higher priority over string capability, as the latest
// one is set to "fallbackDefault".
// https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior
// https://w3c.github.io/webdriver/#dfn-get-the-prompt-handler
case 'accept':
case 'accept and notify':
return {
default: "accept" /* Session.UserPromptHandlerType.Accept */,
beforeUnload: "accept" /* Session.UserPromptHandlerType.Accept */,
};
case 'dismiss':
case 'dismiss and notify':
return {
default: "dismiss" /* Session.UserPromptHandlerType.Dismiss */,
beforeUnload: "accept" /* Session.UserPromptHandlerType.Accept */,
};
case 'ignore':
return {
default: "ignore" /* Session.UserPromptHandlerType.Ignore */,
beforeUnload: "accept" /* Session.UserPromptHandlerType.Accept */,
};
default:
throw new InvalidArgumentException(`Unexpected 'unhandledPromptBehavior' value: ${capabilityValue}`);
}
}
async new(params) {
if (this.#created) {
throw new Error('Session has been already created.');
}
this.#created = true;
const matchedCapabitlites = this.#mergeCapabilities(params.capabilities);
await this.#initConnection(matchedCapabitlites);
const version = await this.#browserCdpClient.sendCommand('Browser.getVersion');
return {
sessionId: 'unknown',
capabilities: {
...matchedCapabitlites,
acceptInsecureCerts: matchedCapabitlites.acceptInsecureCerts ?? false,
browserName: version.product,
browserVersion: version.revision,
platformName: '',
setWindowRect: false,
webSocketUrl: '',
userAgent: version.userAgent,
},
};
}
async subscribe(params, googChannel = null) {
const subscription = await this.#eventManager.subscribe(params.events, params.contexts ?? [], params.userContexts ?? [], googChannel);
return {
subscription,
};
}
async unsubscribe(params, googChannel = null) {
if ('subscriptions' in params) {
await this.#eventManager.unsubscribeByIds(params.subscriptions);
return {};
}
await this.#eventManager.unsubscribe(params.events, googChannel);
return {};
}
}
//# sourceMappingURL=SessionProcessor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"SessionProcessor.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/session/SessionProcessor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { GoogChannel } from '../../../protocol/chromium-bidi.js';
import { type Browser, type BrowsingContext, ChromiumBidi } from '../../../protocol/protocol.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
/**
* Returns the cartesian product of the given arrays.
*
* Example:
* cartesian([1, 2], ['a', 'b']); => [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
*/
export declare function cartesianProduct(...a: any[][]): any[];
/** Expands "AllEvents" events into atomic events. */
export declare function unrollEvents(events: ChromiumBidi.EventNames[]): Iterable<ChromiumBidi.EventNames>;
export interface Subscription {
id: string;
topLevelTraversableIds: Set<BrowsingContext.BrowsingContext>;
userContextIds: Set<string>;
eventNames: Set<ChromiumBidi.EventNames>;
googChannel: GoogChannel;
}
export declare class SubscriptionManager {
#private;
constructor(browsingContextStorage: BrowsingContextStorage);
getGoogChannelsSubscribedToEvent(eventName: ChromiumBidi.EventNames, contextId: BrowsingContext.BrowsingContext): GoogChannel[];
getGoogChannelsSubscribedToEventGlobally(eventName: ChromiumBidi.EventNames): GoogChannel[];
isSubscribedTo(moduleOrEvent: ChromiumBidi.EventNames, contextId: BrowsingContext.BrowsingContext): boolean;
/**
* Subscribes to event in the given context and goog:channel.
* @return {SubscriptionItem[]} List of
* subscriptions. If the event is a whole module, it will return all the specific
* events. If the contextId is null, it will return all the top-level contexts which were
* not subscribed before the command.
*/
subscribe(eventNames: ChromiumBidi.EventNames[], contextIds: BrowsingContext.BrowsingContext[], userContextIds: Browser.UserContext[], googChannel: GoogChannel): Subscription;
/**
* Unsubscribes atomically from all events in the given contexts and channel.
*
* This is a legacy spec branch to unsubscribe by attributes.
*/
unsubscribe(inputEventNames: ChromiumBidi.EventNames[], googChannel: GoogChannel): void;
/**
* Unsubscribes by subscriptionId.
*/
unsubscribeById(subscriptionIds: string[]): void;
}
/**
* Replace with Set.prototype.difference once Node 20 is dropped.
*/
export declare function difference<T>(setA: Set<T>, setB: Set<T>): Set<T>;
@@ -0,0 +1,266 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChromiumBidi, InvalidArgumentException, NoSuchFrameException, } from '../../../protocol/protocol.js';
import { uuidv4 } from '../../../utils/uuid.js';
/**
* Returns the cartesian product of the given arrays.
*
* Example:
* cartesian([1, 2], ['a', 'b']); => [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
*/
export function cartesianProduct(...a) {
return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));
}
/** Expands "AllEvents" events into atomic events. */
export function unrollEvents(events) {
const allEvents = new Set();
function addEvents(events) {
for (const event of events) {
allEvents.add(event);
}
}
for (const event of events) {
switch (event) {
case ChromiumBidi.BiDiModule.Bluetooth:
addEvents(Object.values(ChromiumBidi.Bluetooth.EventNames));
break;
case ChromiumBidi.BiDiModule.BrowsingContext:
addEvents(Object.values(ChromiumBidi.BrowsingContext.EventNames));
break;
case ChromiumBidi.BiDiModule.Input:
addEvents(Object.values(ChromiumBidi.Input.EventNames));
break;
case ChromiumBidi.BiDiModule.Log:
addEvents(Object.values(ChromiumBidi.Log.EventNames));
break;
case ChromiumBidi.BiDiModule.Network:
addEvents(Object.values(ChromiumBidi.Network.EventNames));
break;
case ChromiumBidi.BiDiModule.Script:
addEvents(Object.values(ChromiumBidi.Script.EventNames));
break;
case ChromiumBidi.BiDiModule.Speculation:
addEvents(Object.values(ChromiumBidi.Speculation.EventNames));
break;
default:
allEvents.add(event);
}
}
return allEvents.values();
}
export class SubscriptionManager {
#subscriptions = [];
#knownSubscriptionIds = new Set();
#browsingContextStorage;
constructor(browsingContextStorage) {
this.#browsingContextStorage = browsingContextStorage;
}
getGoogChannelsSubscribedToEvent(eventName, contextId) {
const googChannels = new Set();
for (const subscription of this.#subscriptions) {
if (this.#isSubscribedTo(subscription, eventName, contextId)) {
googChannels.add(subscription.googChannel);
}
}
return Array.from(googChannels);
}
getGoogChannelsSubscribedToEventGlobally(eventName) {
const googChannels = new Set();
for (const subscription of this.#subscriptions) {
if (this.#isSubscribedTo(subscription, eventName)) {
googChannels.add(subscription.googChannel);
}
}
return Array.from(googChannels);
}
#isSubscribedTo(subscription, moduleOrEvent, browsingContextId) {
let includesEvent = false;
for (const eventName of subscription.eventNames) {
// This also covers the `goog:cdp` case where
// we don't unroll the event names
if (
// Event explicitly subscribed
eventName === moduleOrEvent ||
// Event subscribed via module
eventName === moduleOrEvent.split('.').at(0) ||
// Event explicitly subscribed compared to module
eventName.split('.').at(0) === moduleOrEvent) {
includesEvent = true;
break;
}
}
if (!includesEvent) {
return false;
}
// user context subscription.
if (subscription.userContextIds.size !== 0) {
if (!browsingContextId) {
return false;
}
const context = this.#browsingContextStorage.findContext(browsingContextId);
if (!context) {
return false;
}
return subscription.userContextIds.has(context.userContext);
}
// context subscription.
if (subscription.topLevelTraversableIds.size !== 0) {
if (!browsingContextId) {
return false;
}
const topLevelContext = this.#browsingContextStorage.findTopLevelContextId(browsingContextId);
return (topLevelContext !== null &&
subscription.topLevelTraversableIds.has(topLevelContext));
}
// global subscription.
return true;
}
isSubscribedTo(moduleOrEvent, contextId) {
for (const subscription of this.#subscriptions) {
if (this.#isSubscribedTo(subscription, moduleOrEvent, contextId)) {
return true;
}
}
return false;
}
/**
* Subscribes to event in the given context and goog:channel.
* @return {SubscriptionItem[]} List of
* subscriptions. If the event is a whole module, it will return all the specific
* events. If the contextId is null, it will return all the top-level contexts which were
* not subscribed before the command.
*/
subscribe(eventNames, contextIds, userContextIds, googChannel) {
// All the subscriptions are handled on the top-level contexts.
const subscription = {
id: uuidv4(),
eventNames: new Set(unrollEvents(eventNames)),
topLevelTraversableIds: new Set(contextIds.map((contextId) => {
const topLevelContext = this.#browsingContextStorage.findTopLevelContextId(contextId);
if (!topLevelContext) {
throw new NoSuchFrameException(`Top-level navigable not found for context id ${contextId}`);
}
return topLevelContext;
})),
userContextIds: new Set(userContextIds),
googChannel,
};
this.#subscriptions.push(subscription);
this.#knownSubscriptionIds.add(subscription.id);
return subscription;
}
/**
* Unsubscribes atomically from all events in the given contexts and channel.
*
* This is a legacy spec branch to unsubscribe by attributes.
*/
unsubscribe(inputEventNames, googChannel) {
const eventNames = new Set(unrollEvents(inputEventNames));
const newSubscriptions = [];
const eventsMatched = new Set();
for (const subscription of this.#subscriptions) {
if (subscription.googChannel !== googChannel) {
newSubscriptions.push(subscription);
continue;
}
// Skip user context subscriptions.
if (subscription.userContextIds.size !== 0) {
newSubscriptions.push(subscription);
continue;
}
// Skip subscriptions when none of the event names match.
if (intersection(subscription.eventNames, eventNames).size === 0) {
newSubscriptions.push(subscription);
continue;
}
// Skip non-global subscriptions.
if (subscription.topLevelTraversableIds.size !== 0) {
newSubscriptions.push(subscription);
continue;
}
const subscriptionEventNames = new Set(subscription.eventNames);
for (const eventName of eventNames) {
if (subscriptionEventNames.has(eventName)) {
eventsMatched.add(eventName);
subscriptionEventNames.delete(eventName);
}
}
if (subscriptionEventNames.size !== 0) {
newSubscriptions.push({
...subscription,
eventNames: subscriptionEventNames,
});
}
}
// If some events did not match, it is an invalid request.
if (!equal(eventsMatched, eventNames)) {
throw new InvalidArgumentException('No subscription found');
}
// Committing the new subscriptions.
this.#subscriptions = newSubscriptions;
}
/**
* Unsubscribes by subscriptionId.
*/
unsubscribeById(subscriptionIds) {
const subscriptionIdsSet = new Set(subscriptionIds);
const unknownIds = difference(subscriptionIdsSet, this.#knownSubscriptionIds);
if (unknownIds.size !== 0) {
throw new InvalidArgumentException('No subscription found');
}
this.#subscriptions = this.#subscriptions.filter((subscription) => {
return !subscriptionIdsSet.has(subscription.id);
});
this.#knownSubscriptionIds = difference(this.#knownSubscriptionIds, subscriptionIdsSet);
}
}
/**
* Replace with Set.prototype.intersection once Node 20 is dropped.
*/
function intersection(setA, setB) {
const result = new Set();
for (const a of setA) {
if (setB.has(a)) {
result.add(a);
}
}
return result;
}
/**
* Replace with Set.prototype.difference once Node 20 is dropped.
*/
export function difference(setA, setB) {
const result = new Set();
for (const a of setA) {
if (!setB.has(a)) {
result.add(a);
}
}
return result;
}
function equal(setA, setB) {
if (setA.size !== setB.size) {
return false;
}
for (const a of setA) {
if (!setB.has(a)) {
return false;
}
}
return true;
}
//# sourceMappingURL=SubscriptionManager.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChromiumBidi } from '../../../protocol/protocol.js';
/**
* Returns true if the given event is a CDP event.
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export declare function isCdpEvent(name: string): boolean;
/**
* Asserts that the given event is known to BiDi or BiDi+, or throws otherwise.
*/
export declare function assertSupportedEvent(name: string): asserts name is ChromiumBidi.EventNames;
@@ -0,0 +1,33 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChromiumBidi, InvalidArgumentException, } from '../../../protocol/protocol.js';
/**
* Returns true if the given event is a CDP event.
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export function isCdpEvent(name) {
return (name.split('.').at(0)?.startsWith(ChromiumBidi.BiDiModule.Cdp) ?? false);
}
/**
* Asserts that the given event is known to BiDi or BiDi+, or throws otherwise.
*/
export function assertSupportedEvent(name) {
if (!ChromiumBidi.EVENT_NAMES.has(name) && !isCdpEvent(name)) {
throw new InvalidArgumentException(`Unknown event: ${name}`);
}
}
//# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/session/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,YAAY,EACZ,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,CACL,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CACxE,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAY;IAEZ,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,CAAC,IAAa,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,wBAAwB,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC"}
@@ -0,0 +1,24 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { EventManager } from '../session/EventManager.js';
export declare class SpeculationProcessor {
#private;
constructor(eventManager: EventManager, logger: LoggerFn | undefined);
onCdpTargetCreated(cdpTarget: CdpTarget): void;
}
@@ -0,0 +1,58 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LogType } from '../../../utils/log.js';
export class SpeculationProcessor {
#eventManager;
#logger;
constructor(eventManager, logger) {
this.#eventManager = eventManager;
this.#logger = logger;
}
onCdpTargetCreated(cdpTarget) {
cdpTarget.cdpClient.on('Preload.prefetchStatusUpdated', (event) => {
let prefetchStatus;
switch (event.status) {
case 'Running':
prefetchStatus = "pending" /* Speculation.PreloadingStatus.Pending */;
break;
case 'Ready':
prefetchStatus = "ready" /* Speculation.PreloadingStatus.Ready */;
break;
case 'Success':
prefetchStatus = "success" /* Speculation.PreloadingStatus.Success */;
break;
case 'Failure':
prefetchStatus = "failure" /* Speculation.PreloadingStatus.Failure */;
break;
default:
// If status is not recognized, skip the event
this.#logger?.(LogType.debugWarn, `Unknown prefetch status: ${event.status}`);
return;
}
this.#eventManager.registerEvent({
type: 'event',
method: 'speculation.prefetchStatusUpdated',
params: {
context: event.initiatingFrameId,
url: event.prefetchUrl,
status: prefetchStatus,
},
}, cdpTarget.id);
});
}
}
//# sourceMappingURL=SpeculationProcessor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"SpeculationProcessor.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/speculation/SpeculationProcessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAC,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAI9C,MAAM,OAAO,oBAAoB;IAC/B,aAAa,CAAe;IACnB,OAAO,CAAuB;IAEvC,YAAY,YAA0B,EAAE,MAA4B;QAClE,IAAI,CAAC,aAAa,GAAG,YAAY,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,kBAAkB,CAAC,SAAoB;QACrC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,+BAA+B,EAAE,CAAC,KAAK,EAAE,EAAE;YAChE,IAAI,cAA4C,CAAC;YACjD,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;gBACrB,KAAK,SAAS;oBACZ,cAAc,uDAAuC,CAAC;oBACtD,MAAM;gBACR,KAAK,OAAO;oBACV,cAAc,mDAAqC,CAAC;oBACpD,MAAM;gBACR,KAAK,SAAS;oBACZ,cAAc,uDAAuC,CAAC;oBACtD,MAAM;gBACR,KAAK,SAAS;oBACZ,cAAc,uDAAuC,CAAC;oBACtD,MAAM;gBACR;oBACE,8CAA8C;oBAC9C,IAAI,CAAC,OAAO,EAAE,CACZ,OAAO,CAAC,SAAS,EACjB,4BAA4B,KAAK,CAAC,MAAM,EAAE,CAC3C,CAAC;oBACF,OAAO;YACX,CAAC;YACD,IAAI,CAAC,aAAa,CAAC,aAAa,CAC9B;gBACE,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,mCAAmC;gBAC3C,MAAM,EAAE;oBACN,OAAO,EAAE,KAAK,CAAC,iBAAiB;oBAChC,GAAG,EAAE,KAAK,CAAC,WAAW;oBACtB,MAAM,EAAE,cAAc;iBACvB;aACF,EACD,SAAS,CAAC,EAAE,CACb,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,14 @@
import type { CdpClient } from '../../../cdp/CdpClient.js';
import type { Storage } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
/**
* Responsible for handling the `storage` module.
*/
export declare class StorageProcessor {
#private;
constructor(browserCdpClient: CdpClient, browsingContextStorage: BrowsingContextStorage, logger: LoggerFn | undefined);
deleteCookies(params: Storage.DeleteCookiesParameters): Promise<Storage.DeleteCookiesResult>;
getCookies(params: Storage.GetCookiesParameters): Promise<Storage.GetCookiesResult>;
setCookie(params: Storage.SetCookieParameters): Promise<Storage.SetCookieResult>;
}
@@ -0,0 +1,191 @@
import { NoSuchUserContextException, UnableToSetCookieException, } from '../../../protocol/protocol.js';
import { assert } from '../../../utils/assert.js';
import { LogType } from '../../../utils/log.js';
import { NetworkProcessor } from '../network/NetworkProcessor.js';
import { bidiToCdpCookie, cdpToBiDiCookie, deserializeByteValue, } from '../network/NetworkUtils.js';
/**
* Responsible for handling the `storage` module.
*/
export class StorageProcessor {
#browserCdpClient;
#browsingContextStorage;
#logger;
constructor(browserCdpClient, browsingContextStorage, logger) {
this.#browsingContextStorage = browsingContextStorage;
this.#browserCdpClient = browserCdpClient;
this.#logger = logger;
}
async deleteCookies(params) {
const partitionKey = this.#expandStoragePartitionSpec(params.partition);
let cdpResponse;
try {
cdpResponse = await this.#browserCdpClient.sendCommand('Storage.getCookies', {
browserContextId: this.#getCdpBrowserContextId(partitionKey),
});
}
catch (err) {
if (this.#isNoSuchUserContextError(err)) {
// If the user context is not found, special error is thrown.
throw new NoSuchUserContextException(err.message);
}
throw err;
}
const cdpCookiesToDelete = cdpResponse.cookies
.filter(
// CDP's partition key is the source origin. If the request specifies the
// `sourceOrigin` partition key, only cookies with the requested source origin
// are returned.
(c) => partitionKey.sourceOrigin === undefined ||
c.partitionKey?.topLevelSite === partitionKey.sourceOrigin)
.filter((cdpCookie) => {
const bidiCookie = cdpToBiDiCookie(cdpCookie);
return this.#matchCookie(bidiCookie, params.filter);
})
.map((cookie) => ({
...cookie,
// Set expiry to pass date to delete the cookie.
expires: 1,
}));
await this.#browserCdpClient.sendCommand('Storage.setCookies', {
cookies: cdpCookiesToDelete,
browserContextId: this.#getCdpBrowserContextId(partitionKey),
});
return {
partitionKey,
};
}
async getCookies(params) {
const partitionKey = this.#expandStoragePartitionSpec(params.partition);
let cdpResponse;
try {
cdpResponse = await this.#browserCdpClient.sendCommand('Storage.getCookies', {
browserContextId: this.#getCdpBrowserContextId(partitionKey),
});
}
catch (err) {
if (this.#isNoSuchUserContextError(err)) {
// If the user context is not found, special error is thrown.
throw new NoSuchUserContextException(err.message);
}
throw err;
}
const filteredBiDiCookies = cdpResponse.cookies
.filter(
// CDP's partition key is the source origin. If the request specifies the
// `sourceOrigin` partition key, only cookies with the requested source origin
// are returned.
(c) => partitionKey.sourceOrigin === undefined ||
c.partitionKey?.topLevelSite === partitionKey.sourceOrigin)
.map((c) => cdpToBiDiCookie(c))
.filter((c) => this.#matchCookie(c, params.filter));
return {
cookies: filteredBiDiCookies,
partitionKey,
};
}
async setCookie(params) {
const partitionKey = this.#expandStoragePartitionSpec(params.partition);
const cdpCookie = bidiToCdpCookie(params, partitionKey);
try {
await this.#browserCdpClient.sendCommand('Storage.setCookies', {
cookies: [cdpCookie],
browserContextId: this.#getCdpBrowserContextId(partitionKey),
});
}
catch (err) {
if (this.#isNoSuchUserContextError(err)) {
// If the user context is not found, special error is thrown.
throw new NoSuchUserContextException(err.message);
}
this.#logger?.(LogType.debugError, err);
throw new UnableToSetCookieException(err.toString());
}
return {
partitionKey,
};
}
#isNoSuchUserContextError(err) {
// Heuristic to detect if the user context is not found.
// See https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/browser_handler.cc;drc=a56154dd81e4679712422ac6eed2c9581cb51ab0;l=314
return err.message?.startsWith('Failed to find browser context for id');
}
#getCdpBrowserContextId(partitionKey) {
return partitionKey.userContext === 'default'
? undefined
: partitionKey.userContext;
}
#expandStoragePartitionSpecByBrowsingContext(descriptor) {
const browsingContextId = descriptor.context;
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
// https://w3c.github.io/webdriver-bidi/#associated-storage-partition.
// Each browsing context also has an associated storage partition, which is the
// storage partition it uses to persist data. In Chromium it's a `BrowserContext`
// which maps to BiDi `UserContext`.
return {
userContext: browsingContext.userContext,
};
}
#expandStoragePartitionSpecByStorageKey(descriptor) {
const unsupportedPartitionKeys = new Map();
let sourceOrigin = descriptor.sourceOrigin;
if (sourceOrigin !== undefined) {
const url = NetworkProcessor.parseUrlString(sourceOrigin);
if (url.origin === 'null') {
// Origin `null` is a special case for local pages.
sourceOrigin = url.origin;
}
else {
// Port is not supported in CDP Cookie's `partitionKey`, so it should be stripped
// from the requested source origin.
sourceOrigin = `${url.protocol}//${url.hostname}`;
}
}
for (const [key, value] of Object.entries(descriptor)) {
if (key !== undefined &&
value !== undefined &&
!['type', 'sourceOrigin', 'userContext'].includes(key)) {
unsupportedPartitionKeys.set(key, value);
}
}
if (unsupportedPartitionKeys.size > 0) {
this.#logger?.(LogType.debugInfo, `Unsupported partition keys: ${JSON.stringify(Object.fromEntries(unsupportedPartitionKeys))}`);
}
// Set `userContext` to `default` if not provided, as it's required in Chromium.
const userContext = descriptor.userContext ?? 'default';
return {
userContext,
...(sourceOrigin === undefined ? {} : { sourceOrigin }),
};
}
#expandStoragePartitionSpec(partitionSpec) {
if (partitionSpec === undefined) {
// `userContext` is required in Chromium.
return { userContext: 'default' };
}
if (partitionSpec.type === 'context') {
return this.#expandStoragePartitionSpecByBrowsingContext(partitionSpec);
}
assert(partitionSpec.type === 'storageKey', 'Unknown partition type');
// Partition spec is a storage partition.
// Let partition key be partition spec.
return this.#expandStoragePartitionSpecByStorageKey(partitionSpec);
}
#matchCookie(cookie, filter) {
if (filter === undefined) {
return true;
}
return ((filter.domain === undefined || filter.domain === cookie.domain) &&
(filter.name === undefined || filter.name === cookie.name) &&
// `value` contains fields `type` and `value`.
(filter.value === undefined ||
deserializeByteValue(filter.value) ===
deserializeByteValue(cookie.value)) &&
(filter.path === undefined || filter.path === cookie.path) &&
(filter.size === undefined || filter.size === cookie.size) &&
(filter.httpOnly === undefined || filter.httpOnly === cookie.httpOnly) &&
(filter.secure === undefined || filter.secure === cookie.secure) &&
(filter.sameSite === undefined || filter.sameSite === cookie.sameSite) &&
(filter.expiry === undefined || filter.expiry === cookie.expiry));
}
}
//# sourceMappingURL=StorageProcessor.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { type WebExtension, type EmptyResult } from '../../../protocol/protocol.js';
/**
* Responsible for handling the `webModule` module.
*/
export declare class WebExtensionProcessor {
#private;
constructor(browserCdpClient: CdpClient);
install(params: WebExtension.InstallParameters): Promise<WebExtension.InstallResult>;
uninstall(params: WebExtension.UninstallParameters): Promise<EmptyResult>;
}
@@ -0,0 +1,65 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InvalidWebExtensionException, UnsupportedOperationException, NoSuchWebExtensionException, } from '../../../protocol/protocol.js';
/**
* Responsible for handling the `webModule` module.
*/
export class WebExtensionProcessor {
#browserCdpClient;
constructor(browserCdpClient) {
this.#browserCdpClient = browserCdpClient;
}
async install(params) {
switch (params.extensionData.type) {
case 'archivePath':
case 'base64':
throw new UnsupportedOperationException('Archived and Base64 extensions are not supported');
case 'path':
break;
}
try {
const response = await this.#browserCdpClient.sendCommand('Extensions.loadUnpacked', {
path: params.extensionData.path,
});
return {
extension: response.id,
};
}
catch (err) {
if (err.message.startsWith('invalid web extension')) {
throw new InvalidWebExtensionException(err.message);
}
throw err;
}
}
async uninstall(params) {
try {
await this.#browserCdpClient.sendCommand('Extensions.uninstall', {
id: params.extension,
});
return {};
}
catch (err) {
if (err.message ===
'Uninstall failed. Reason: could not find extension.') {
throw new NoSuchWebExtensionException('no such web extension');
}
throw err;
}
}
}
//# sourceMappingURL=WebExtensionProcessor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"WebExtensionProcessor.js","sourceRoot":"","sources":["../../../../src/bidiMapper/modules/webExtension/WebExtensionProcessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EACL,4BAA4B,EAE5B,6BAA6B,EAE7B,2BAA2B,GAC5B,MAAM,+BAA+B,CAAC;AAEvC;;GAEG;AACH,MAAM,OAAO,qBAAqB;IACvB,iBAAiB,CAAY;IAEtC,YAAY,gBAA2B;QACrC,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,OAAO,CACX,MAAsC;QAEtC,QAAQ,MAAM,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;YAClC,KAAK,aAAa,CAAC;YACnB,KAAK,QAAQ;gBACX,MAAM,IAAI,6BAA6B,CACrC,kDAAkD,CACnD,CAAC;YACJ,KAAK,MAAM;gBACT,MAAM;QACV,CAAC;QACD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,WAAW,CACvD,yBAAyB,EACzB;gBACE,IAAI,EAAE,MAAM,CAAC,aAAa,CAAC,IAAI;aAChC,CACF,CAAC;YACF,OAAO;gBACL,SAAS,EAAE,QAAQ,CAAC,EAAE;aACvB,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAAa,CAAC,OAAO,CAAC,UAAU,CAAC,uBAAuB,CAAC,EAAE,CAAC;gBAC/D,MAAM,IAAI,4BAA4B,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;YACjE,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CACb,MAAwC;QAExC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,iBAAiB,CAAC,WAAW,CAAC,sBAAsB,EAAE;gBAC/D,EAAE,EAAE,MAAM,CAAC,SAAS;aACrB,CAAC,CAAC;YACH,OAAO,EAAE,CAAC;QACZ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IACG,GAAa,CAAC,OAAO;gBACtB,qDAAqD,EACrD,CAAC;gBACD,MAAM,IAAI,2BAA2B,CAAC,uBAAuB,CAAC,CAAC;YACjE,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;CACF"}
+23
View File
@@ -0,0 +1,23 @@
import { type Process } from '@puppeteer/browsers';
import { MapperServerCdpConnection } from './MapperCdpConnection.js';
import type { SimpleTransport } from './SimpleTransport.js';
export interface ChromeOptions {
chromeArgs: string[];
chromeBinary?: string;
}
/**
* BrowserProcess is responsible for running the browser and BiDi Mapper within
* it.
* 1. Launch Chromium (using Puppeteer for now).
* 2. Get `BiDi-CDP` mapper JS binaries using `MapperReader`.
* 3. Run `BiDi-CDP` mapper in launched browser using `MapperRunner`.
* 4. Bind `BiDi-CDP` mapper to the `BiDi server` to forward messages from BiDi
* Mapper to the client.
*/
export declare class BrowserInstance {
#private;
static run(chromeOptions: ChromeOptions, verbose: boolean): Promise<BrowserInstance>;
constructor(mapperCdpConnection: MapperServerCdpConnection, browserProcess: Process);
close(): Promise<void>;
bidiSession(): SimpleTransport;
}
+107
View File
@@ -0,0 +1,107 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { mkdtemp } from 'fs/promises';
import os from 'os';
import path from 'path';
import { launch } from '@puppeteer/browsers';
import debug from 'debug';
import { MapperCdpConnection } from '../cdp/CdpConnection.js';
import { MapperServerCdpConnection } from './MapperCdpConnection.js';
import { PipeTransport } from './PipeTransport.js';
import { getMapperTabSource } from './reader.js';
const debugInternal = debug('bidi:mapper:internal');
/**
* BrowserProcess is responsible for running the browser and BiDi Mapper within
* it.
* 1. Launch Chromium (using Puppeteer for now).
* 2. Get `BiDi-CDP` mapper JS binaries using `MapperReader`.
* 3. Run `BiDi-CDP` mapper in launched browser using `MapperRunner`.
* 4. Bind `BiDi-CDP` mapper to the `BiDi server` to forward messages from BiDi
* Mapper to the client.
*/
export class BrowserInstance {
#mapperCdpConnection;
#browserProcess;
static async run(chromeOptions, verbose) {
const profileDir = await mkdtemp(path.join(os.tmpdir(), 'web-driver-bidi-server-'));
// See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
const chromeArguments = [
// keep-sorted start
'--allow-browser-signin=false',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-component-update',
'--disable-default-apps',
'--disable-notifications',
'--disable-popup-blocking',
'--disable-search-engine-choice-screen',
'--enable-automation',
'--no-default-browser-check',
'--no-first-run',
'--password-store=basic',
'--remote-debugging-pipe',
'--use-mock-keychain',
`--user-data-dir=${profileDir}`,
// keep-sorted end
...chromeOptions.chromeArgs,
'about:blank',
];
const executablePath = chromeOptions.chromeBinary ?? process.env['BROWSER_BIN'];
if (!executablePath) {
throw new Error('Could not find Chrome binary');
}
const launchArguments = {
executablePath,
args: chromeArguments,
env: process.env,
pipe: true,
};
debugInternal(`Launching browser`, {
executablePath,
args: chromeArguments,
});
const browserProcess = launch(launchArguments);
const cdpConnection = this.#establishPipeConnection(browserProcess);
// 2. Get `BiDi-CDP` mapper JS binaries.
const mapperTabSource = await getMapperTabSource();
// 3. Run `BiDi-CDP` mapper in launched browser using `MapperRunner`.
const mapperCdpConnection = await MapperServerCdpConnection.create(cdpConnection, mapperTabSource, verbose);
return new BrowserInstance(mapperCdpConnection, browserProcess);
}
constructor(mapperCdpConnection, browserProcess) {
this.#mapperCdpConnection = mapperCdpConnection;
this.#browserProcess = browserProcess;
}
async close() {
// Close the mapper tab.
this.#mapperCdpConnection.close();
// Close browser.
await this.#browserProcess.close();
}
bidiSession() {
return this.#mapperCdpConnection.bidiSession();
}
static #establishPipeConnection(browserProcess) {
debugInternal('Establishing pipe connection to browser process with pid: ', browserProcess.nodeProcess.pid);
const { 3: pipeWrite, 4: pipeRead } = browserProcess.nodeProcess.stdio;
const transport = new PipeTransport(pipeWrite, pipeRead);
return new MapperCdpConnection(transport);
}
}
//# sourceMappingURL=BrowserInstance.js.map
@@ -0,0 +1 @@
{"version":3,"file":"BrowserInstance.js","sourceRoot":"","sources":["../../src/bidiServer/BrowserInstance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAC,OAAO,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAC,MAAM,EAAe,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAC,mBAAmB,EAAC,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAC,yBAAyB,EAAC,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAC,kBAAkB,EAAC,MAAM,aAAa,CAAC;AAG/C,MAAM,aAAa,GAAG,KAAK,CAAC,sBAAsB,CAAC,CAAC;AAOpD;;;;;;;;GAQG;AACH,MAAM,OAAO,eAAe;IAC1B,oBAAoB,CAA4B;IAChD,eAAe,CAAU;IAEzB,MAAM,CAAC,KAAK,CAAC,GAAG,CACd,aAA4B,EAC5B,OAAgB;QAEhB,MAAM,UAAU,GAAG,MAAM,OAAO,CAC9B,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,yBAAyB,CAAC,CAClD,CAAC;QAEF,+FAA+F;QAC/F,MAAM,eAAe,GAAG;YACtB,oBAAoB;YACpB,8BAA8B;YAC9B,iCAAiC;YACjC,uCAAuC;YACvC,0CAA0C;YAC1C,4BAA4B;YAC5B,wBAAwB;YACxB,yBAAyB;YACzB,0BAA0B;YAC1B,uCAAuC;YACvC,qBAAqB;YACrB,4BAA4B;YAC5B,gBAAgB;YAChB,wBAAwB;YACxB,yBAAyB;YACzB,qBAAqB;YACrB,mBAAmB,UAAU,EAAE;YAC/B,kBAAkB;YAClB,GAAG,aAAa,CAAC,UAAU;YAC3B,aAAa;SACd,CAAC;QAEF,MAAM,cAAc,GAClB,aAAa,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAE3D,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClD,CAAC;QAED,MAAM,eAAe,GAAG;YACtB,cAAc;YACd,IAAI,EAAE,eAAe;YACrB,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,IAAI,EAAE,IAAI;SACX,CAAC;QAEF,aAAa,CAAC,mBAAmB,EAAE;YACjC,cAAc;YACd,IAAI,EAAE,eAAe;SACtB,CAAC,CAAC;QAEH,MAAM,cAAc,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;QAE/C,MAAM,aAAa,GAAG,IAAI,CAAC,wBAAwB,CAAC,cAAc,CAAC,CAAC;QACpE,wCAAwC;QACxC,MAAM,eAAe,GAAG,MAAM,kBAAkB,EAAE,CAAC;QAEnD,qEAAqE;QACrE,MAAM,mBAAmB,GAAG,MAAM,yBAAyB,CAAC,MAAM,CAChE,aAAa,EACb,eAAe,EACf,OAAO,CACR,CAAC;QAEF,OAAO,IAAI,eAAe,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC;IAClE,CAAC;IAED,YACE,mBAA8C,EAC9C,cAAuB;QAEvB,IAAI,CAAC,oBAAoB,GAAG,mBAAmB,CAAC;QAChD,IAAI,CAAC,eAAe,GAAG,cAAc,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,wBAAwB;QACxB,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAE,CAAC;QAElC,iBAAiB;QACjB,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;IACrC,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,oBAAoB,CAAC,WAAW,EAAE,CAAC;IACjD,CAAC;IAED,MAAM,CAAC,wBAAwB,CAC7B,cAAuB;QAEvB,aAAa,CACX,4DAA4D,EAC5D,cAAc,CAAC,WAAW,CAAC,GAAG,CAC/B,CAAC;QACF,MAAM,EAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAC,GAAG,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC;QACrE,MAAM,SAAS,GAAG,IAAI,aAAa,CACjC,SAAkC,EAClC,QAAiC,CAClC,CAAC;QACF,OAAO,IAAI,mBAAmB,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC;CACF"}
@@ -0,0 +1,25 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { MapperCdpConnection } from '../cdp/CdpConnection.js';
import { SimpleTransport } from './SimpleTransport.js';
export declare class MapperServerCdpConnection {
#private;
static create(cdpConnection: MapperCdpConnection, mapperTabSource: string, verbose: boolean): Promise<MapperServerCdpConnection>;
private constructor();
close(): void;
bidiSession(): SimpleTransport;
}
@@ -0,0 +1,138 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import debug from 'debug';
import { SimpleTransport } from './SimpleTransport.js';
const debugInternal = debug('bidi:mapper:internal');
const debugInfo = debug('bidi:mapper:info');
const debugOthers = debug('bidi:mapper:debug:others');
// Memorizes a debug creation
const loggers = new Map();
const getLogger = (type) => {
const prefix = `bidi:mapper:${type}`;
let logger = loggers.get(prefix);
if (!logger) {
logger = debug(prefix);
loggers.set(prefix, logger);
}
return logger;
};
export class MapperServerCdpConnection {
#cdpConnection;
#bidiSession;
static async create(cdpConnection, mapperTabSource, verbose) {
try {
const bidiSession = await this.#initMapper(cdpConnection, mapperTabSource, verbose);
return new MapperServerCdpConnection(cdpConnection, bidiSession);
}
catch (e) {
cdpConnection.close();
throw e;
}
}
constructor(cdpConnection, bidiSession) {
this.#cdpConnection = cdpConnection;
this.#bidiSession = bidiSession;
}
static async #sendMessage(mapperCdpClient, message) {
try {
await mapperCdpClient.sendCommand('Runtime.evaluate', {
expression: `onBidiMessage(${JSON.stringify(message)})`,
});
}
catch (error) {
debugInternal('Call to onBidiMessage failed', error);
}
}
close() {
this.#cdpConnection.close();
}
bidiSession() {
return this.#bidiSession;
}
static #onBindingCalled = (params, bidiSession) => {
if (params.name === 'sendBidiResponse') {
bidiSession.emit('message', params.payload);
}
else if (params.name === 'sendDebugMessage') {
this.#onDebugMessage(params.payload);
}
};
static #onDebugMessage = (json) => {
try {
const log = JSON.parse(json);
if (log.logType !== undefined && log.messages !== undefined) {
const logger = getLogger(log.logType);
logger(log.messages);
}
}
catch {
// Fall back to raw log in case of unknown
debugOthers(json);
}
};
static #onConsoleAPICalled = (params) => {
debugInfo('consoleAPICalled: %s %O', params.type, params.args.map((arg) => arg.value));
};
static #onRuntimeExceptionThrown = (params) => {
debugInfo('exceptionThrown:', params);
};
static async #initMapper(cdpConnection, mapperTabSource, verbose) {
debugInternal('Initializing Mapper.');
const browserClient = await cdpConnection.createBrowserSession();
const { targetId: mapperTargetId } = await browserClient.sendCommand('Target.createTarget', {
url: 'about:blank#MAPPER_TARGET',
hidden: true,
background: true,
});
const { sessionId: mapperSessionId } = await browserClient.sendCommand('Target.attachToTarget', { targetId: mapperTargetId, flatten: true });
const mapperCdpClient = cdpConnection.getCdpClient(mapperSessionId);
const bidiSession = new SimpleTransport(async (message) => await this.#sendMessage(mapperCdpClient, message));
// Process responses from the mapper tab.
mapperCdpClient.on('Runtime.bindingCalled', (params) => this.#onBindingCalled(params, bidiSession));
// Forward console messages from the mapper tab.
mapperCdpClient.on('Runtime.consoleAPICalled', this.#onConsoleAPICalled);
// Catch unhandled exceptions in the mapper.
mapperCdpClient.on('Runtime.exceptionThrown', this.#onRuntimeExceptionThrown);
await mapperCdpClient.sendCommand('Runtime.enable');
await browserClient.sendCommand('Target.exposeDevToolsProtocol', {
bindingName: 'cdp',
targetId: mapperTargetId,
inheritPermissions: true,
});
await mapperCdpClient.sendCommand('Runtime.addBinding', {
name: 'sendBidiResponse',
});
if (verbose) {
// Needed to request verbose logs from Mapper.
await mapperCdpClient.sendCommand('Runtime.addBinding', {
name: 'sendDebugMessage',
});
}
// Evaluate Mapper Tab sources in the tab.
await mapperCdpClient.sendCommand('Runtime.evaluate', {
expression: mapperTabSource,
});
// TODO: handle errors in all these evaluate calls!
await mapperCdpClient.sendCommand('Runtime.evaluate', {
expression: `window.runMapperInstance('${mapperTargetId}')`,
awaitPromise: true,
});
debugInternal('Mapper is launched!');
return bidiSession;
}
}
//# sourceMappingURL=MapperCdpConnection.js.map
@@ -0,0 +1 @@
{"version":3,"file":"MapperCdpConnection.js","sourceRoot":"","sources":["../../src/bidiServer/MapperCdpConnection.ts"],"names":[],"mappings":"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"}
+8
View File
@@ -0,0 +1,8 @@
import type { Transport } from '../utils/transport.js';
export declare class PipeTransport implements Transport {
#private;
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream);
setOnMessage(onMessage: (message: string) => void): void;
sendMessage(message: string): void;
close(): void;
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import debug from 'debug';
const debugInternal = debug('bidi:server:pipeTranspot');
export class PipeTransport {
#pipeWrite;
#onMessage = null;
#pendingMessage = '';
constructor(pipeWrite, pipeRead) {
this.#pipeWrite = pipeWrite;
pipeRead.on('data', (chunk) => {
return this.#dispatch(chunk);
});
pipeRead.on('close', () => {
this.close();
});
pipeRead.on('error', (error) => {
debugInternal('Pipe read error: ', error);
this.close();
});
pipeWrite.on('error', (error) => {
debugInternal('Pipe read error: ', error);
this.close();
});
}
setOnMessage(onMessage) {
this.#onMessage = onMessage;
}
sendMessage(message) {
// TODO: WebSocketServer keeps sending messages after closing the transport.
// TODO: we should assert that the pipe was not closed.
this.#pipeWrite.write(message);
this.#pipeWrite.write('\0');
}
#dispatch(buffer) {
// TODO: WebSocketServer keeps sending messages after closing the transport.
// TODO: we should assert that the pipe was not closed.
let end = buffer.indexOf('\0');
if (end === -1) {
this.#pendingMessage += buffer.toString();
return;
}
const message = this.#pendingMessage + buffer.toString(undefined, 0, end);
if (this.#onMessage) {
this.#onMessage.call(null, message);
}
let start = end + 1;
end = buffer.indexOf('\0', start);
while (end !== -1) {
if (this.#onMessage) {
this.#onMessage.call(null, buffer.toString(undefined, start, end));
}
start = end + 1;
end = buffer.indexOf('\0', start);
}
this.#pendingMessage = buffer.toString(undefined, start);
}
close() {
debugInternal('Closing pipe');
}
}
//# sourceMappingURL=PipeTransport.js.map
@@ -0,0 +1 @@
{"version":3,"file":"PipeTransport.js","sourceRoot":"","sources":["../../src/bidiServer/PipeTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,MAAM,aAAa,GAAG,KAAK,CAAC,0BAA0B,CAAC,CAAC;AAExD,MAAM,OAAO,aAAa;IACxB,UAAU,CAAwB;IAClC,UAAU,GAAuC,IAAI,CAAC;IAEtD,eAAe,GAAG,EAAE,CAAC;IAErB,YACE,SAAgC,EAChC,QAA+B;QAE/B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAE5B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAC5B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACxB,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC7B,aAAa,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;YAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;QACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC9B,aAAa,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;YAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAED,YAAY,CAAC,SAAoC;QAC/C,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IACD,WAAW,CAAC,OAAe;QACzB,4EAA4E;QAC5E,uDAAuD;QAEvD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,SAAS,CAAC,MAAc;QACtB,4EAA4E;QAC5E,uDAAuD;QAEvD,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YACf,IAAI,CAAC,eAAe,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC1C,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QAC1E,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACtC,CAAC;QAED,IAAI,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC;QACpB,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAClC,OAAO,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACpB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;YACrE,CAAC;YACD,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC;YAChB,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACpC,CAAC;QACD,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK;QACH,aAAa,CAAC,cAAc,CAAC,CAAC;IAChC,CAAC;CACF"}
+13
View File
@@ -0,0 +1,13 @@
import { EventEmitter } from '../utils/EventEmitter.js';
/**
* Implements simple transport that allows sending string messages via
* `sendCommand` and receiving them via `on('message')`.
*/
export declare class SimpleTransport extends EventEmitter<Record<'message', string>> {
#private;
/**
* @param sendCommandDelegate delegate to be called in `sendCommand`.
*/
constructor(sendCommandDelegate: (plainCommand: string) => Promise<void>);
sendCommand(plainCommand: string): Promise<void>;
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from '../utils/EventEmitter.js';
/**
* Implements simple transport that allows sending string messages via
* `sendCommand` and receiving them via `on('message')`.
*/
export class SimpleTransport extends EventEmitter {
#sendCommandDelegate;
/**
* @param sendCommandDelegate delegate to be called in `sendCommand`.
*/
constructor(sendCommandDelegate) {
super();
this.#sendCommandDelegate = sendCommandDelegate;
}
async sendCommand(plainCommand) {
await this.#sendCommandDelegate(plainCommand);
}
}
//# sourceMappingURL=SimpleTransport.js.map
@@ -0,0 +1 @@
{"version":3,"file":"SimpleTransport.js","sourceRoot":"","sources":["../../src/bidiServer/SimpleTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAEtD;;;GAGG;AACH,MAAM,OAAO,eAAgB,SAAQ,YAAuC;IACjE,oBAAoB,CAA0C;IAEvE;;OAEG;IACH,YAAY,mBAA4D;QACtE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,oBAAoB,GAAG,mBAAmB,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,YAAoB;QACpC,MAAM,IAAI,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAC;IAChD,CAAC;CACF"}
@@ -0,0 +1,6 @@
import debug from 'debug';
export declare const debugInfo: debug.Debugger;
export declare class WebSocketServer {
#private;
constructor(port: number, verbose: boolean);
}
+374
View File
@@ -0,0 +1,374 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import http from 'http';
import debug from 'debug';
import * as websocket from 'websocket';
import { Deferred } from '../utils/Deferred.js';
import { uuidv4 } from '../utils/uuid.js';
import { BrowserInstance } from './BrowserInstance.js';
export const debugInfo = debug('bidi:server:info');
const debugInternal = debug('bidi:server:internal');
const debugSend = debug('bidi:server:SEND ▸');
const debugRecv = debug('bidi:server:RECV ◂');
export class WebSocketServer {
#sessions = new Map();
#port;
#verbose;
#server;
#wsServer;
constructor(port, verbose) {
this.#port = port;
this.#verbose = verbose;
this.#server = http.createServer((request, response) => {
return this.#onRequest(request, response).catch((e) => {
debugInfo('Error while processing request', e);
response.writeHead(500, String(e));
});
});
this.#wsServer = new websocket.server({
httpServer: this.#server,
autoAcceptConnections: false,
});
this.#wsServer.on('request', this.#onWsRequest.bind(this));
void this.#listen();
}
#logServerStarted() {
debugInfo('BiDi server is listening on port', this.#port);
debugInfo('BiDi server was started successfully.');
}
async #listen() {
try {
this.#server.listen(this.#port, () => {
this.#logServerStarted();
});
}
catch (error) {
if (error &&
typeof error === 'object' &&
'code' in error &&
error.code === 'EADDRINUSE') {
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
debugInfo('Retrying to run BiDi server');
this.#server.listen(this.#port, () => {
this.#logServerStarted();
});
}
throw error;
}
}
async #onRequest(request, response) {
debugInternal(`Received HTTP ${JSON.stringify(request.method)} request for ${JSON.stringify(request.url)}`);
if (!request.url) {
throw new Error('Request URL is empty.');
}
// https://w3c.github.io/webdriver-bidi/#transport, step 2.
if (request.url === '/session') {
const body = await new Promise((resolve, reject) => {
const bodyArray = [];
request.on('data', (chunk) => {
bodyArray.push(chunk);
});
request.on('error', reject);
request.on('end', () => {
resolve(Buffer.concat(bodyArray));
});
});
debugInternal(`Creating session by HTTP request ${body.toString()}`);
// https://w3c.github.io/webdriver-bidi/#transport, step 3.
const jsonBody = JSON.parse(body.toString());
response.writeHead(200, {
'Content-Type': 'application/json;charset=utf-8',
'Cache-Control': 'no-cache',
});
const sessionId = uuidv4();
const session = {
sessionId,
// TODO: launch browser instance and set it to the session after WPT
// tests clean up is switched to pure BiDi.
browserInstancePromise: undefined,
sessionOptions: {
chromeOptions: this.#getChromeOptions(jsonBody.capabilities),
verbose: this.#verbose,
sessionNewBody: `{"id":0,"method":"session.new","params":${body.toString()}}`,
},
};
this.#sessions.set(sessionId, session);
const webSocketUrl = `ws://localhost:${this.#port}/session/${sessionId}`;
debugInternal(`Session created. WebSocket URL: ${JSON.stringify(webSocketUrl)}.`);
response.write(JSON.stringify({
value: {
sessionId,
capabilities: {
webSocketUrl,
},
},
}));
return response.end();
}
else if (request.url.startsWith('/session')) {
debugInternal(`Unknown session command ${request.method ?? 'UNKNOWN METHOD'} request for ${request.url} with payload ${await this.#getHttpRequestPayload(request)}. 200 returned.`);
response.writeHead(200, {
'Content-Type': 'application/json;charset=utf-8',
'Cache-Control': 'no-cache',
});
response.write(JSON.stringify({
value: {},
}));
return response.end();
}
throw new Error(`Unknown "${request.method}" request for "${JSON.stringify(request.url)}" with payload "${await this.#getHttpRequestPayload(request)}".`);
}
#onWsRequest(request) {
// Session is set either by Classic or BiDi commands.
let session;
// Request to `/session` should be treated as a new session request.
let requestSessionId = '';
if ((request.resource ?? '').startsWith(`/session/`)) {
requestSessionId = (request.resource ?? '').split('/').pop() ?? '';
}
debugInternal(`new WS request received. Path: ${JSON.stringify(request.resourceURL.path)}, sessionId: ${JSON.stringify(requestSessionId)}`);
if (requestSessionId !== '' &&
requestSessionId !== undefined &&
!this.#sessions.has(requestSessionId)) {
debugInternal('Unknown session id:', requestSessionId);
request.reject();
return;
}
const connection = request.accept();
session = this.#sessions.get(requestSessionId ?? '');
if (session !== undefined) {
// BrowserInstance is created for each new WS connection, even for the
// same SessionId. This is because WPT uses a single session for all the
// tests, but cleans up tests using WebDriver Classic commands, which is
// not implemented in this Mapper runner.
// TODO: connect to an existing BrowserInstance instead.
const sessionOptions = session.sessionOptions;
session.browserInstancePromise = this.#closeBrowserInstanceIfLaunched(session)
.then(async () => await this.#launchBrowserInstance(connection, sessionOptions))
.catch((e) => {
debugInfo('Error while creating session', e);
connection.close(500, 'cannot create browser instance');
throw e;
});
}
connection.on('message', async (message) => {
// If type is not text, return error.
if (message.type !== 'utf8') {
this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `not supported type (${message.type})`);
return;
}
const plainCommandData = message.utf8Data;
if (debugRecv.enabled) {
try {
debugRecv(JSON.parse(plainCommandData));
}
catch {
debugRecv(plainCommandData);
}
}
// Try to parse the message to handle some of BiDi commands.
let parsedCommandData;
try {
parsedCommandData = JSON.parse(plainCommandData);
}
catch (error) {
this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `unable to parse BiDi command: ${error}`);
return;
}
// Handle creating new session.
if (parsedCommandData.method === 'session.new') {
if (session !== undefined) {
debugInfo('WS connection already have an associated session.');
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, 'WS connection already have an associated session.');
return;
}
try {
const sessionOptions = {
chromeOptions: this.#getChromeOptions(parsedCommandData.params?.capabilities),
verbose: this.#verbose,
sessionNewBody: plainCommandData,
};
const browserInstance = await this.#launchBrowserInstance(connection, sessionOptions, true);
const sessionId = uuidv4();
session = {
sessionId,
browserInstancePromise: Promise.resolve(browserInstance),
sessionOptions,
};
this.#sessions.set(sessionId, session);
}
catch (e) {
debugInfo('Error while creating session', e);
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, e?.message ?? 'Unknown error');
return;
}
return;
}
// Handle ending session. Close browser if open, remove session.
if (parsedCommandData.method === 'session.end') {
if (session === undefined) {
debugInfo('WS connection does not have an associated session.');
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, 'WS connection does not have an associated session.');
return;
}
try {
await this.#closeBrowserInstanceIfLaunched(session);
this.#sessions.delete(session.sessionId);
}
catch (e) {
debugInfo('Error while closing session', e);
this.#respondWithError(connection, plainCommandData, "unknown error" /* ErrorCode.UnknownError */, `Session cannot be closed. Error: ${e?.message}`);
return;
}
this.#sendClientMessage({
id: parsedCommandData.id,
type: 'success',
result: {},
}, connection);
return;
}
if (session === undefined) {
debugInfo('Session is not yet initialized.');
this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Session is not yet initialized.');
return;
}
if (session.browserInstancePromise === undefined) {
debugInfo('Browser instance is not launched.');
this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Browser instance is not launched.');
return;
}
const browserInstance = await session.browserInstancePromise;
// Handle `browser.close` command.
if (parsedCommandData.method === 'browser.close') {
await browserInstance.close();
this.#sendClientMessage({
id: parsedCommandData.id,
type: 'success',
result: {},
}, connection);
return;
}
// Forward all other commands to BiDi Mapper.
await browserInstance.bidiSession().sendCommand(plainCommandData);
});
connection.on('close', async () => {
debugInternal(`Peer ${connection.remoteAddress} disconnected.`);
// TODO: don't close Browser instance to allow re-connecting to the session.
await this.#closeBrowserInstanceIfLaunched(session);
});
}
async #closeBrowserInstanceIfLaunched(session) {
if (session === undefined || session.browserInstancePromise === undefined) {
return;
}
const browserInstance = await session.browserInstancePromise;
session.browserInstancePromise = undefined;
void browserInstance.close();
}
#getChromeOptions(capabilities) {
const chromeCapabilities = capabilities?.alwaysMatch?.['goog:chromeOptions'];
return {
chromeArgs: chromeCapabilities?.args ?? [],
chromeBinary: chromeCapabilities?.binary ?? undefined,
};
}
async #launchBrowserInstance(connection, sessionOptions, passSessionNewThrough = false) {
debugInfo('Scheduling browser launch...');
const browserInstance = await BrowserInstance.run(sessionOptions.chromeOptions, sessionOptions.verbose);
const body = JSON.parse(sessionOptions.sessionNewBody);
const id = body.id;
const sessionCreated = new Deferred();
const sessionResponseListener = (message) => {
const jsonMessage = JSON.parse(message);
if (jsonMessage['id'] === id) {
debugInfo('Receiving session.new response from mapper', message);
sessionCreated.resolve();
if (passSessionNewThrough) {
this.#sendClientMessageString(message, connection);
}
}
};
browserInstance.bidiSession().on('message', sessionResponseListener);
debugInfo('Sending session.new to mapper', sessionOptions.sessionNewBody);
await browserInstance
.bidiSession()
.sendCommand(sessionOptions.sessionNewBody);
await sessionCreated;
browserInstance.bidiSession().off('message', sessionResponseListener);
// Forward messages from BiDi Mapper to the client unconditionally.
browserInstance.bidiSession().on('message', (message) => {
this.#sendClientMessageString(message, connection);
});
debugInfo('Browser is launched!');
return browserInstance;
}
#sendClientMessageString(message, connection) {
if (debugSend.enabled) {
try {
debugSend(JSON.parse(message));
}
catch {
debugSend(message);
}
}
connection.sendUTF(message);
}
#sendClientMessage(object, connection) {
const json = JSON.stringify(object);
return this.#sendClientMessageString(json, connection);
}
#respondWithError(connection, plainCommandData, errorCode, errorMessage) {
const errorResponse = this.#getErrorResponse(plainCommandData, errorCode, errorMessage);
void this.#sendClientMessage(errorResponse, connection);
}
#getErrorResponse(plainCommandData, errorCode, errorMessage) {
// XXX: this is bizarre per spec. We reparse the payload and
// extract the ID, regardless of what kind of value it was.
let commandId;
try {
const commandData = JSON.parse(plainCommandData);
if ('id' in commandData) {
commandId = commandData.id;
}
}
catch { }
return {
type: 'error',
id: commandId,
error: errorCode,
message: errorMessage,
// XXX: optional stacktrace field.
};
}
#getHttpRequestPayload(request) {
return new Promise((resolve, reject) => {
let data = '';
request.on('data', (chunk) => {
data += chunk;
});
request.on('end', () => {
resolve(data);
});
request.on('error', (error) => {
reject(error);
});
});
}
}
//# sourceMappingURL=WebSocketServer.js.map
File diff suppressed because one or more lines are too long
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export declare function parseCommandLineArgs(): {
[x: string]: unknown;
port: number;
verbose: boolean;
_: (string | number)[];
$0: string;
};
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { debugInfo, WebSocketServer } from './WebSocketServer.js';
export function parseCommandLineArgs() {
return yargs(hideBin(process.argv))
.usage(`$0`, `[PORT=8080] [VERBOSE=8080]`)
.option('port', {
alias: 'p',
describe: 'Port that BiDi server should listen to. Default is 8080.',
type: 'number',
default: process.env['PORT'] ? Number(process.env['PORT']) : 8080,
})
.option('verbose', {
alias: 'v',
describe: 'If present, the Mapper debug log, including CDP commands and events will be logged into the server output.',
type: 'boolean',
default: process.env['VERBOSE'] === 'true' || false,
})
.parseSync();
}
(() => {
try {
const argv = parseCommandLineArgs();
const { port, verbose } = argv;
debugInfo('Launching BiDi server...');
new WebSocketServer(port, verbose);
debugInfo('BiDi server launched');
}
catch (e) {
debugInfo('Error launching BiDi server', e);
}
})();
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bidiServer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAEtC,OAAO,EAAC,SAAS,EAAE,eAAe,EAAC,MAAM,sBAAsB,CAAC;AAEhE,MAAM,UAAU,oBAAoB;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;SAChC,KAAK,CAAC,IAAI,EAAE,4BAA4B,CAAC;SACzC,MAAM,CAAC,MAAM,EAAE;QACd,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,0DAA0D;QACpE,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;KAClE,CAAC;SACD,MAAM,CAAC,SAAS,EAAE;QACjB,KAAK,EAAE,GAAG;QACV,QAAQ,EACN,4GAA4G;QAC9G,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,MAAM,IAAI,KAAK;KACpD,CAAC;SACD,SAAS,EAAE,CAAC;AACjB,CAAC;AAED,CAAC,GAAG,EAAE;IACJ,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAC;QAEpC,MAAM,EAAC,IAAI,EAAE,OAAO,EAAC,GAAG,IAAI,CAAC;QAE7B,SAAS,CAAC,0BAA0B,CAAC,CAAC;QAEtC,IAAI,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACnC,SAAS,CAAC,sBAAsB,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,SAAS,CAAC,6BAA6B,EAAE,CAAC,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC,CAAC,EAAE,CAAC"}
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export declare function getMapperTabSource(): Promise<string>;
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs/promises';
import path from 'path';
export async function getMapperTabSource() {
return await fs.readFile(path.join(import.meta.dirname, '../iife/mapperTab.js'), 'utf8');
}
//# sourceMappingURL=reader.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/bidiServer/reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,OAAO,MAAM,EAAE,CAAC,QAAQ,CACtB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,sBAAsB,CAAC,EACtD,MAAM,CACP,CAAC;AACJ,CAAC"}
+94
View File
@@ -0,0 +1,94 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BidiCommandParameterParser } from '../bidiMapper/BidiMapper.js';
import type { Bluetooth, Browser, BrowsingContext, Cdp, Emulation, Input, Network, Permissions, Script, Session, Storage, WebExtension, UAClientHints } from '../protocol/protocol.js';
export declare class BidiParser implements BidiCommandParameterParser {
parseDisableSimulationParameters(params: unknown): Bluetooth.DisableSimulationParameters;
parseHandleRequestDevicePromptParams(params: unknown): Bluetooth.HandleRequestDevicePromptParameters;
parseSimulateAdapterParameters(params: unknown): Bluetooth.SimulateAdapterParameters;
parseSimulateAdvertisementParameters(params: unknown): Bluetooth.SimulateAdvertisementParameters;
parseSimulateCharacteristicParameters(params: unknown): Bluetooth.SimulateCharacteristicParameters;
parseSimulateCharacteristicResponseParameters(params: unknown): Bluetooth.SimulateCharacteristicResponseParameters;
parseSimulateDescriptorParameters(params: unknown): Bluetooth.SimulateDescriptorParameters;
parseSimulateDescriptorResponseParameters(params: unknown): Bluetooth.SimulateDescriptorResponseParameters;
parseSimulateGattConnectionResponseParameters(params: unknown): Bluetooth.SimulateGattConnectionResponseParameters;
parseSimulateGattDisconnectionParameters(params: unknown): Bluetooth.SimulateGattDisconnectionParameters;
parseSimulatePreconnectedPeripheralParameters(params: unknown): Bluetooth.SimulatePreconnectedPeripheralParameters;
parseSimulateServiceParameters(params: unknown): Bluetooth.SimulateServiceParameters;
parseCreateUserContextParameters(params: unknown): Browser.CreateUserContextParameters;
parseRemoveUserContextParameters(params: unknown): Browser.RemoveUserContextParameters;
parseSetClientWindowStateParameters(params: unknown): Browser.SetClientWindowStateParameters;
parseSetDownloadBehaviorParameters(params: unknown): Browser.SetDownloadBehaviorParameters;
parseActivateParams(params: unknown): BrowsingContext.ActivateParameters;
parseCaptureScreenshotParams(params: unknown): BrowsingContext.CaptureScreenshotParameters;
parseCloseParams(params: unknown): BrowsingContext.CloseParameters;
parseCreateParams(params: unknown): BrowsingContext.CreateParameters;
parseGetTreeParams(params: unknown): BrowsingContext.GetTreeParameters;
parseHandleUserPromptParams(params: unknown): BrowsingContext.HandleUserPromptParameters;
parseLocateNodesParams(params: unknown): BrowsingContext.LocateNodesParameters;
parseNavigateParams(params: unknown): BrowsingContext.NavigateParameters;
parsePrintParams(params: unknown): BrowsingContext.PrintParameters;
parseReloadParams(params: unknown): BrowsingContext.ReloadParameters;
parseSetBypassCspParams(params: unknown): BrowsingContext.SetBypassCspParameters;
parseSetViewportParams(params: unknown): BrowsingContext.SetViewportParameters;
parseTraverseHistoryParams(params: unknown): BrowsingContext.TraverseHistoryParameters;
parseGetSessionParams(params: unknown): Cdp.GetSessionParameters;
parseResolveRealmParams(params: unknown): Cdp.ResolveRealmParameters;
parseSendCommandParams(params: unknown): Cdp.SendCommandParameters;
parseSetClientHintsOverrideParams(params: unknown): UAClientHints.UserAgentClientHints.SetClientHintsOverrideCommand['params'];
parseSetForcedColorsModeThemeOverrideParams(params: unknown): Emulation.SetForcedColorsModeThemeOverrideParameters;
parseSetGeolocationOverrideParams(params: unknown): Emulation.SetGeolocationOverrideParameters;
parseSetLocaleOverrideParams(params: unknown): Emulation.SetLocaleOverrideParameters;
parseSetNetworkConditionsParams(params: unknown): Emulation.SetNetworkConditionsParameters;
parseSetScreenOrientationOverrideParams(params: unknown): Emulation.SetScreenOrientationOverrideParameters;
parseSetScreenSettingsOverrideParams(params: unknown): Emulation.SetScreenSettingsOverrideParameters;
parseSetScriptingEnabledParams(params: unknown): Emulation.SetScriptingEnabledParameters;
parseSetScrollbarTypeOverrideParams(params: unknown): Emulation.SetScrollbarTypeOverrideParameters;
parseSetTimezoneOverrideParams(params: unknown): Emulation.SetTimezoneOverrideParameters;
parseSetTouchOverrideParams(params: unknown): Emulation.SetTouchOverrideParameters;
parseSetUserAgentOverrideParams(params: unknown): Emulation.SetUserAgentOverrideParameters;
parsePerformActionsParams(params: unknown): Input.PerformActionsParameters;
parseReleaseActionsParams(params: unknown): Input.ReleaseActionsParameters;
parseSetFilesParams(params: unknown): Input.SetFilesParameters;
parseAddDataCollectorParams(params: unknown): Network.AddDataCollectorParameters;
parseAddInterceptParams(params: unknown): Network.AddInterceptParameters;
parseContinueRequestParams(params: unknown): Network.ContinueRequestParameters;
parseContinueResponseParams(params: unknown): Network.ContinueResponseParameters;
parseContinueWithAuthParams(params: unknown): Network.ContinueWithAuthParameters;
parseDisownDataParams(params: unknown): Network.DisownDataParameters;
parseFailRequestParams(params: unknown): Network.FailRequestParameters;
parseGetDataParams(params: unknown): Network.GetDataParameters;
parseProvideResponseParams(params: unknown): Network.ProvideResponseParameters;
parseRemoveDataCollectorParams(params: unknown): Network.RemoveDataCollectorParameters;
parseRemoveInterceptParams(params: unknown): Network.RemoveInterceptParameters;
parseSetCacheBehaviorParams(params: unknown): Network.SetCacheBehaviorParameters;
parseSetExtraHeadersParams(params: unknown): Network.SetExtraHeadersParameters;
parseSetPermissionsParams(params: unknown): Permissions.SetPermissionParameters;
parseAddPreloadScriptParams(params: unknown): Script.AddPreloadScriptParameters;
parseCallFunctionParams(params: unknown): Script.CallFunctionParameters;
parseDisownParams(params: unknown): Script.DisownParameters;
parseEvaluateParams(params: unknown): Script.EvaluateParameters;
parseGetRealmsParams(params: unknown): Script.GetRealmsParameters;
parseRemovePreloadScriptParams(params: unknown): Script.RemovePreloadScriptParameters;
parseSubscribeParams(params: unknown): Session.SubscribeParameters;
parseUnsubscribeParams(params: unknown): Session.UnsubscribeParameters;
parseDeleteCookiesParams(params: unknown): Storage.DeleteCookiesParameters;
parseGetCookiesParams(params: unknown): Storage.GetCookiesParameters;
parseSetCookieParams(params: unknown): Storage.SetCookieParameters;
parseInstallParams(params: unknown): WebExtension.InstallParameters;
parseUninstallParams(params: unknown): WebExtension.UninstallParameters;
}
+263
View File
@@ -0,0 +1,263 @@
import * as Parser from '../protocol-parser/protocol-parser.js';
export class BidiParser {
// Bluetooth module
// keep-sorted start block=yes
parseDisableSimulationParameters(params) {
return Parser.Bluetooth.parseDisableSimulationParameters(params);
}
parseHandleRequestDevicePromptParams(params) {
return Parser.Bluetooth.parseHandleRequestDevicePromptParams(params);
}
parseSimulateAdapterParameters(params) {
return Parser.Bluetooth.parseSimulateAdapterParams(params);
}
parseSimulateAdvertisementParameters(params) {
return Parser.Bluetooth.parseSimulateAdvertisementParams(params);
}
parseSimulateCharacteristicParameters(params) {
return Parser.Bluetooth.parseSimulateCharacteristicParams(params);
}
parseSimulateCharacteristicResponseParameters(params) {
return Parser.Bluetooth.parseSimulateCharacteristicResponseParams(params);
}
parseSimulateDescriptorParameters(params) {
return Parser.Bluetooth.parseSimulateDescriptorParams(params);
}
parseSimulateDescriptorResponseParameters(params) {
return Parser.Bluetooth.parseSimulateDescriptorResponseParams(params);
}
parseSimulateGattConnectionResponseParameters(params) {
return Parser.Bluetooth.parseSimulateGattConnectionResponseParams(params);
}
parseSimulateGattDisconnectionParameters(params) {
return Parser.Bluetooth.parseSimulateGattDisconnectionParams(params);
}
parseSimulatePreconnectedPeripheralParameters(params) {
return Parser.Bluetooth.parseSimulatePreconnectedPeripheralParams(params);
}
parseSimulateServiceParameters(params) {
return Parser.Bluetooth.parseSimulateServiceParams(params);
}
// keep-sorted end
// Browser module
// keep-sorted start block=yes
parseCreateUserContextParameters(params) {
// Validate the params, but return the original one, as there can be `goog:` options.
Parser.Browser.parseCreateUserContextParameters(params);
return params;
}
parseRemoveUserContextParameters(params) {
return Parser.Browser.parseRemoveUserContextParameters(params);
}
parseSetClientWindowStateParameters(params) {
return Parser.Browser.parseSetClientWindowStateParameters(params);
}
parseSetDownloadBehaviorParameters(params) {
return Parser.Browser.parseSetDownloadBehaviorParameters(params);
}
// keep-sorted end
// Browsing Context module
// keep-sorted start block=yes
parseActivateParams(params) {
return Parser.BrowsingContext.parseActivateParams(params);
}
parseCaptureScreenshotParams(params) {
return Parser.BrowsingContext.parseCaptureScreenshotParams(params);
}
parseCloseParams(params) {
return Parser.BrowsingContext.parseCloseParams(params);
}
parseCreateParams(params) {
return Parser.BrowsingContext.parseCreateParams(params);
}
parseGetTreeParams(params) {
return Parser.BrowsingContext.parseGetTreeParams(params);
}
parseHandleUserPromptParams(params) {
return Parser.BrowsingContext.parseHandleUserPromptParameters(params);
}
parseLocateNodesParams(params) {
return Parser.BrowsingContext.parseLocateNodesParams(params);
}
parseNavigateParams(params) {
return Parser.BrowsingContext.parseNavigateParams(params);
}
parsePrintParams(params) {
return Parser.BrowsingContext.parsePrintParams(params);
}
parseReloadParams(params) {
return Parser.BrowsingContext.parseReloadParams(params);
}
parseSetBypassCspParams(params) {
return Parser.BrowsingContext.parseSetBypassCspParams(params);
}
parseSetViewportParams(params) {
return Parser.BrowsingContext.parseSetViewportParams(params);
}
parseTraverseHistoryParams(params) {
return Parser.BrowsingContext.parseTraverseHistoryParams(params);
}
// keep-sorted end
// CDP module
// keep-sorted start block=yes
parseGetSessionParams(params) {
return Parser.Cdp.parseGetSessionRequest(params);
}
parseResolveRealmParams(params) {
return Parser.Cdp.parseResolveRealmRequest(params);
}
parseSendCommandParams(params) {
return Parser.Cdp.parseSendCommandRequest(params);
}
// keep-sorted end
// Emulation module
// keep-sorted start block=yes
parseSetClientHintsOverrideParams(params) {
return Parser.Emulation.parseSetClientHintsOverrideParams(params);
}
parseSetForcedColorsModeThemeOverrideParams(params) {
return Parser.Emulation.parseSetForcedColorsModeThemeOverrideParams(params);
}
parseSetGeolocationOverrideParams(params) {
return Parser.Emulation.parseSetGeolocationOverrideParams(params);
}
parseSetLocaleOverrideParams(params) {
return Parser.Emulation.parseSetLocaleOverrideParams(params);
}
parseSetNetworkConditionsParams(params) {
return Parser.Emulation.parseSetNetworkConditionsParams(params);
}
parseSetScreenOrientationOverrideParams(params) {
return Parser.Emulation.parseSetScreenOrientationOverrideParams(params);
}
parseSetScreenSettingsOverrideParams(params) {
return Parser.Emulation.parseSetScreenSettingsOverrideParams(params);
}
parseSetScriptingEnabledParams(params) {
return Parser.Emulation.parseSetScriptingEnabledParams(params);
}
parseSetScrollbarTypeOverrideParams(params) {
return Parser.Emulation.parseSetScrollbarTypeOverrideParams(params);
}
parseSetTimezoneOverrideParams(params) {
return Parser.Emulation.parseSetTimezoneOverrideParams(params);
}
parseSetTouchOverrideParams(params) {
return Parser.Emulation.parseSetTouchOverrideParams(params);
}
parseSetUserAgentOverrideParams(params) {
return Parser.Emulation.parseSetUserAgentOverrideParams(params);
}
// keep-sorted end
// Input module
// keep-sorted start block=yes
parsePerformActionsParams(params) {
return Parser.Input.parsePerformActionsParams(params);
}
parseReleaseActionsParams(params) {
return Parser.Input.parseReleaseActionsParams(params);
}
parseSetFilesParams(params) {
return Parser.Input.parseSetFilesParams(params);
}
// keep-sorted end
// Network module
// keep-sorted start block=yes
parseAddDataCollectorParams(params) {
return Parser.Network.parseAddDataCollectorParameters(params);
}
parseAddInterceptParams(params) {
return Parser.Network.parseAddInterceptParameters(params);
}
parseContinueRequestParams(params) {
return Parser.Network.parseContinueRequestParameters(params);
}
parseContinueResponseParams(params) {
return Parser.Network.parseContinueResponseParameters(params);
}
parseContinueWithAuthParams(params) {
return Parser.Network.parseContinueWithAuthParameters(params);
}
parseDisownDataParams(params) {
return Parser.Network.parseDisownDataParameters(params);
}
parseFailRequestParams(params) {
return Parser.Network.parseFailRequestParameters(params);
}
parseGetDataParams(params) {
return Parser.Network.parseGetDataParameters(params);
}
parseProvideResponseParams(params) {
return Parser.Network.parseProvideResponseParameters(params);
}
parseRemoveDataCollectorParams(params) {
return Parser.Network.parseRemoveDataCollectorParameters(params);
}
parseRemoveInterceptParams(params) {
return Parser.Network.parseRemoveInterceptParameters(params);
}
parseSetCacheBehaviorParams(params) {
return Parser.Network.parseSetCacheBehaviorParameters(params);
}
parseSetExtraHeadersParams(params) {
return Parser.Network.parseSetExtraHeadersParameters(params);
}
// keep-sorted end
// Permissions module
// keep-sorted start block=yes
parseSetPermissionsParams(params) {
return Parser.Permissions.parseSetPermissionsParams(params);
}
// keep-sorted end
// Script module
// keep-sorted start block=yes
parseAddPreloadScriptParams(params) {
return Parser.Script.parseAddPreloadScriptParams(params);
}
parseCallFunctionParams(params) {
return Parser.Script.parseCallFunctionParams(params);
}
parseDisownParams(params) {
return Parser.Script.parseDisownParams(params);
}
parseEvaluateParams(params) {
return Parser.Script.parseEvaluateParams(params);
}
parseGetRealmsParams(params) {
return Parser.Script.parseGetRealmsParams(params);
}
parseRemovePreloadScriptParams(params) {
return Parser.Script.parseRemovePreloadScriptParams(params);
}
// keep-sorted end
// Session module
// keep-sorted start block=yes
parseSubscribeParams(params) {
return Parser.Session.parseSubscribeParams(params);
}
parseUnsubscribeParams(params) {
return Parser.Session.parseUnsubscribeParams(params);
}
// keep-sorted end
// Storage module
// keep-sorted start block=yes
parseDeleteCookiesParams(params) {
return Parser.Storage.parseDeleteCookiesParams(params);
}
parseGetCookiesParams(params) {
return Parser.Storage.parseGetCookiesParams(params);
}
parseSetCookieParams(params) {
return Parser.Storage.parseSetCookieParams(params);
}
// keep-sorted end
// WebExtenstion module
// keep-sorted start block=yes
parseInstallParams(params) {
return Parser.WebModule.parseInstallParams(params);
}
parseUninstallParams(params) {
return Parser.WebModule.parseUninstallParams(params);
}
}
//# sourceMappingURL=BidiParser.js.map
File diff suppressed because one or more lines are too long
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright 2023 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License. *
*/
import type { BidiTransport } from '../bidiMapper/BidiMapper.js';
import { type ChromiumBidi } from '../protocol/protocol.js';
import { LogType } from '../utils/log.js';
import type { Transport } from '../utils/transport.js';
export declare class WindowBidiTransport implements BidiTransport {
#private;
static readonly LOGGER_PREFIX_RECV: "bidi:RECV ◂";
static readonly LOGGER_PREFIX_SEND: "bidi:SEND ▸";
static readonly LOGGER_PREFIX_WARN = LogType.debugWarn;
constructor();
setOnMessage(onMessage: Parameters<BidiTransport['setOnMessage']>[0]): void;
sendMessage(message: ChromiumBidi.Message): void;
close(): void;
}
export declare class WindowCdpTransport implements Transport {
#private;
constructor();
setOnMessage(onMessage: Parameters<Transport['setOnMessage']>[0]): void;
sendMessage(message: string): void;
close(): void;
}
+145
View File
@@ -0,0 +1,145 @@
var _a;
import { LogType } from '../utils/log.js';
import { log } from './mapperTabPage.js';
export class WindowBidiTransport {
static LOGGER_PREFIX_RECV = `${LogType.bidi}:RECV ◂`;
static LOGGER_PREFIX_SEND = `${LogType.bidi}:SEND ▸`;
static LOGGER_PREFIX_WARN = LogType.debugWarn;
#onMessage = null;
constructor() {
window.onBidiMessage = (message) => {
log(_a.LOGGER_PREFIX_RECV, message);
try {
const command = _a.#parseBidiMessage(message);
this.#onMessage?.call(null, command);
}
catch (e) {
const error = e instanceof Error ? e : new Error(e);
// Transport-level error does not provide goog:channel.
this.#respondWithError(message, "invalid argument" /* ErrorCode.InvalidArgument */, error, null);
}
};
}
setOnMessage(onMessage) {
this.#onMessage = onMessage;
}
sendMessage(message) {
log(_a.LOGGER_PREFIX_SEND, message);
const json = JSON.stringify(message);
window.sendBidiResponse(json);
}
close() {
this.#onMessage = null;
window.onBidiMessage = null;
}
#respondWithError(plainCommandData, errorCode, error, googChannel) {
const errorResponse = _a.#getErrorResponse(plainCommandData, errorCode, error);
if (googChannel) {
this.sendMessage({
...errorResponse,
'goog:channel': googChannel,
});
}
else {
this.sendMessage(errorResponse);
}
}
static #getJsonType(value) {
if (value === null) {
return 'null';
}
if (Array.isArray(value)) {
return 'array';
}
return typeof value;
}
static #getErrorResponse(message, errorCode, error) {
// XXX: this is bizarre per spec. We reparse the payload and
// extract the ID, regardless of what kind of value it was.
let messageId;
try {
const command = JSON.parse(message);
if (_a.#getJsonType(command) === 'object' &&
'id' in command) {
messageId = command.id;
}
}
catch { }
return {
type: 'error',
id: messageId,
error: errorCode,
message: error.message,
};
}
static #parseBidiMessage(message) {
let command;
try {
command = JSON.parse(message);
}
catch {
throw new Error('Cannot parse data as JSON');
}
const type = _a.#getJsonType(command);
if (type !== 'object') {
throw new Error(`Expected JSON object but got ${type}`);
}
// Extract and validate id, method and params.
const { id, method, params } = command;
const idType = _a.#getJsonType(id);
if (idType !== 'number' || !Number.isInteger(id) || id < 0) {
// TODO: should uint64_t be the upper limit?
// https://tools.ietf.org/html/rfc7049#section-2.1
throw new Error(`Expected unsigned integer but got ${idType}`);
}
const methodType = _a.#getJsonType(method);
if (methodType !== 'string') {
throw new Error(`Expected string method but got ${methodType}`);
}
const paramsType = _a.#getJsonType(params);
if (paramsType !== 'object') {
throw new Error(`Expected object params but got ${paramsType}`);
}
let googChannel = command['goog:channel'];
if (googChannel !== undefined) {
const googChannelType = _a.#getJsonType(googChannel);
if (googChannelType !== 'string') {
throw new Error(`Expected string channel but got ${googChannelType}`);
}
// Empty string goog:channel is considered as no goog:channel provided.
if (googChannel === '') {
googChannel = undefined;
}
}
return {
id,
method,
params,
'goog:channel': googChannel,
};
}
}
_a = WindowBidiTransport;
export class WindowCdpTransport {
#onMessage = null;
#cdpSend;
constructor() {
this.#cdpSend = window.cdp.send;
// @ts-expect-error removing cdp
window.cdp.send = undefined;
window.cdp.onmessage = (message) => {
this.#onMessage?.call(null, message);
};
}
setOnMessage(onMessage) {
this.#onMessage = onMessage;
}
sendMessage(message) {
this.#cdpSend(message);
}
close() {
this.#onMessage = null;
window.cdp.onmessage = null;
}
}
//# sourceMappingURL=Transport.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Transport.js","sourceRoot":"","sources":["../../src/bidiTab/Transport.ts"],"names":[],"mappings":"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"}
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @license
*/
declare global {
interface Window {
runMapperInstance: ((...args: any) => Promise<void>) | null;
cdp: {
send: (message: string) => void;
onmessage: ((message: string) => void) | null;
};
sendBidiResponse: (response: string) => void;
onBidiMessage: ((message: string) => void) | null;
sendDebugMessage?: ((message: string) => void) | null;
onbeforeunload: ((this: WindowEventHandlers, ev: BeforeUnloadEvent) => any) | null;
}
}
export {};
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @license
*/
import { BidiServer } from '../bidiMapper/BidiMapper.js';
import { MapperCdpConnection } from '../cdp/CdpConnection.js';
import { LogType } from '../utils/log.js';
import { BidiParser } from './BidiParser.js';
import { generatePage, log } from './mapperTabPage.js';
import { WindowBidiTransport, WindowCdpTransport } from './Transport.js';
generatePage();
const mapperTabToServerTransport = new WindowBidiTransport();
const cdpTransport = new WindowCdpTransport();
/**
* A CdpTransport implementation that uses the window.cdp bindings
* injected by Target.exposeDevToolsProtocol.
*/
const cdpConnection = new MapperCdpConnection(cdpTransport, log);
/**
* Launches the BiDi mapper instance.
* @param {string} selfTargetId
* @param options Mapper options. E.g. `acceptInsecureCerts`.
*/
async function runMapperInstance(selfTargetId) {
// eslint-disable-next-line no-console
console.log('Launching Mapper instance with selfTargetId:', selfTargetId);
const bidiServer = await BidiServer.createAndStart(mapperTabToServerTransport, cdpConnection,
/**
* Create a Browser CDP Session per Mapper instance.
*/
await cdpConnection.createBrowserSession(), selfTargetId, new BidiParser(), log);
log(LogType.debugInfo, 'Mapper instance has been launched');
return bidiServer;
}
/**
* Set `window.runMapper` to a function which launches the BiDi mapper instance.
* @param selfTargetId Needed to filter out info related to BiDi target.
*/
window.runMapperInstance = async (selfTargetId) => {
await runMapperInstance(selfTargetId);
};
//# sourceMappingURL=bidiTab.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"bidiTab.js","sourceRoot":"","sources":["../../src/bidiTab/bidiTab.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAC,UAAU,EAAC,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAC,mBAAmB,EAAC,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAExC,OAAO,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAC,YAAY,EAAE,GAAG,EAAC,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAC,mBAAmB,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAgCvE,YAAY,EAAE,CAAC;AACf,MAAM,0BAA0B,GAAG,IAAI,mBAAmB,EAAE,CAAC;AAC7D,MAAM,YAAY,GAAG,IAAI,kBAAkB,EAAE,CAAC;AAC9C;;;GAGG;AACH,MAAM,aAAa,GAAG,IAAI,mBAAmB,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;AAEjE;;;;GAIG;AACH,KAAK,UAAU,iBAAiB,CAAC,YAAoB;IACnD,sCAAsC;IACtC,OAAO,CAAC,GAAG,CAAC,8CAA8C,EAAE,YAAY,CAAC,CAAC;IAE1E,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,cAAc,CAChD,0BAA0B,EAC1B,aAAa;IACb;;OAEG;IACH,MAAM,aAAa,CAAC,oBAAoB,EAAE,EAC1C,YAAY,EACZ,IAAI,UAAU,EAAE,EAChB,GAAG,CACJ,CAAC;IAEF,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,mCAAmC,CAAC,CAAC;IAE5D,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,iBAAiB,GAAG,KAAK,EAAE,YAAY,EAAE,EAAE;IAChD,MAAM,iBAAiB,CAAC,YAAY,CAAC,CAAC;AACxC,CAAC,CAAC"}
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type LogPrefix } from '../utils/log.js';
export declare function generatePage(): void;
export declare function log(logPrefix: LogPrefix, ...messages: unknown[]): void;
+59
View File
@@ -0,0 +1,59 @@
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LogType } from '../utils/log.js';
/** HTML source code for the user-facing Mapper tab. */
const mapperPageSource = '<!DOCTYPE html><title>BiDi-CDP Mapper</title><style>body{font-family: Roboto,serif;font-size:13px;color:#202124;}.log{padding: 10px;font-family:Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;font-size:11px;line-height:180%;background: #f1f3f4;border-radius:4px;}.pre{overflow-wrap: break-word; margin:10px;}.card{margin:60px auto;padding:2px 0;max-width:900px;box-shadow:0 1px 4px rgba(0,0,0,0.15),0 1px 6px rgba(0,0,0,0.2);border-radius:8px;}.divider{height:1px;background:#f0f0f0;}.item{padding:16px 20px;}</style><div class="card"><div class="item"><h1>BiDi-CDP Mapper is controlling this tab</h1><p>Closing or reloading it will stop the BiDi process. <a target="_blank" title="BiDi-CDP Mapper GitHub Repository" href="https://github.com/GoogleChromeLabs/chromium-bidi">Details.</a></p></div><div class="item"><div id="logs" class="log"></div></div></div></div>';
export function generatePage() {
// If run not in browser (e.g. unit test), do nothing.
if (!globalThis.document.documentElement) {
return;
}
globalThis.document.documentElement.innerHTML = mapperPageSource;
// Show a confirmation dialog when the user tries to leave the Mapper tab.
globalThis.window.onbeforeunload = () => 'Closing or reloading this tab will stop the BiDi process. Are you sure you want to leave?';
}
function stringify(message) {
if (typeof message === 'object') {
return JSON.stringify(message, null, 2);
}
return message;
}
export function log(logPrefix, ...messages) {
// If run not in browser (e.g. unit test), do nothing.
if (!globalThis.document.documentElement) {
return;
}
// Skip sending BiDi logs as they are logged once by `bidi:server:*`
if (!logPrefix.startsWith(LogType.bidi)) {
// If `sendDebugMessage` is defined, send the log message there.
globalThis.window?.sendDebugMessage?.(JSON.stringify({ logType: logPrefix, messages }, null, 2));
}
const debugContainer = document.getElementById('logs');
if (!debugContainer) {
return;
}
// This piece of HTML should be added:
// <div class="pre">...log message...</div>
const lineElement = document.createElement('div');
lineElement.className = 'pre';
lineElement.textContent = [logPrefix, ...messages].map(stringify).join(' ');
debugContainer.appendChild(lineElement);
if (debugContainer.childNodes.length > 400) {
debugContainer.removeChild(debugContainer.childNodes[0]);
}
}
//# sourceMappingURL=mapperTabPage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"mapperTabPage.js","sourceRoot":"","sources":["../../src/bidiTab/mapperTabPage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAiB,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAExD,uDAAuD;AACvD,MAAM,gBAAgB,GACpB,u3BAAu3B,CAAC;AAE13B,MAAM,UAAU,YAAY;IAC1B,sDAAsD;IACtD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC;QACzC,OAAO;IACT,CAAC;IACD,UAAU,CAAC,QAAQ,CAAC,eAAe,CAAC,SAAS,GAAG,gBAAgB,CAAC;IAEjE,0EAA0E;IAC1E,UAAU,CAAC,MAAM,CAAC,cAAc,GAAG,GAAG,EAAE,CACtC,2FAA2F,CAAC;AAChG,CAAC;AAED,SAAS,SAAS,CAAC,OAAgB;IACjC,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,SAAoB,EAAE,GAAG,QAAmB;IAC9D,sDAAsD;IACtD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC;QACzC,OAAO;IACT,CAAC;IAED,oEAAoE;IACpE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,gEAAgE;QAChE,UAAU,CAAC,MAAM,EAAE,gBAAgB,EAAE,CACnC,IAAI,CAAC,SAAS,CAAC,EAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CACxD,CAAC;IACJ,CAAC;IAED,MAAM,cAAc,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,OAAO;IACT,CAAC;IAED,sCAAsC;IACtC,2CAA2C;IAC3C,MAAM,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAClD,WAAW,CAAC,SAAS,GAAG,KAAK,CAAC;IAE9B,WAAW,CAAC,WAAW,GAAG,CAAC,SAAS,EAAE,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5E,cAAc,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,cAAc,CAAC,UAAU,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC3C,cAAc,CAAC,WAAW,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAE,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC"}
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Protocol } from 'devtools-protocol';
import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js';
import { EventEmitter } from '../utils/EventEmitter.js';
import type { MapperCdpConnection } from './CdpConnection.js';
export type CdpEvents = {
[Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0];
};
/** An error that will be thrown if/when the connection is closed. */
export declare class CloseError extends Error {
}
export interface CdpClient extends EventEmitter<CdpEvents> {
/** Unique session identifier. */
sessionId: Protocol.Target.SessionID | undefined;
/**
* Provides an unique way to detect if an error was caused by the closure of a
* Target or Session.
*
* @example During the creation of a subframe we navigate the main frame.
* The subframe Target is closed while initialized commands are in-flight.
* In this case we want to swallow the thrown error.
*/
isCloseError(error: unknown): boolean;
/**
* Returns a command promise, which will be resolved with the command result
* after receiving the result from CDP.
* @param method Name of the CDP command to call.
* @param params Parameters to pass to the CDP command.
*/
sendCommand<CdpMethod extends keyof ProtocolMapping.Commands>(method: CdpMethod, params?: ProtocolMapping.Commands[CdpMethod]['paramsType'][0]): Promise<ProtocolMapping.Commands[CdpMethod]['returnType']>;
}
/** Represents a high-level CDP connection to the browser. */
export declare class MapperCdpClient extends EventEmitter<CdpEvents> implements CdpClient {
#private;
constructor(cdpConnection: MapperCdpConnection, sessionId?: Protocol.Target.SessionID);
get sessionId(): Protocol.Target.SessionID | undefined;
sendCommand<CdpMethod extends keyof ProtocolMapping.Commands>(method: CdpMethod, ...params: ProtocolMapping.Commands[CdpMethod]['paramsType']): Promise<ProtocolMapping.Commands[CdpMethod]['returnType']>;
isCloseError(error: unknown): boolean;
}
+40
View File
@@ -0,0 +1,40 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from '../utils/EventEmitter.js';
/** An error that will be thrown if/when the connection is closed. */
export class CloseError extends Error {
}
/** Represents a high-level CDP connection to the browser. */
export class MapperCdpClient extends EventEmitter {
#cdpConnection;
#sessionId;
constructor(cdpConnection, sessionId) {
super();
this.#cdpConnection = cdpConnection;
this.#sessionId = sessionId;
}
get sessionId() {
return this.#sessionId;
}
sendCommand(method, ...params) {
return this.#cdpConnection.sendCommand(method, params[0], this.#sessionId);
}
isCloseError(error) {
return error instanceof CloseError;
}
}
//# sourceMappingURL=CdpClient.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"CdpClient.js","sourceRoot":"","sources":["../../src/cdp/CdpClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAQtD,qEAAqE;AACrE,MAAM,OAAO,UAAW,SAAQ,KAAK;CAAG;AA4BxC,6DAA6D;AAC7D,MAAM,OAAO,eACX,SAAQ,YAAuB;IAG/B,cAAc,CAAsB;IACpC,UAAU,CAA6B;IAEvC,YACE,aAAkC,EAClC,SAAqC;QAErC,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;QACpC,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,WAAW,CACT,MAAiB,EACjB,GAAG,MAAyD;QAE5D,OAAO,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7E,CAAC;IAED,YAAY,CAAC,KAAc;QACzB,OAAO,KAAK,YAAY,UAAU,CAAC;IACrC,CAAC;CACF"}
+45
View File
@@ -0,0 +1,45 @@
/**
* Copyright 2021 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Protocol } from 'devtools-protocol';
import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js';
import type { LoggerFn } from '../utils/log.js';
import type { Transport } from '../utils/transport.js';
import { MapperCdpClient, type CdpClient } from './CdpClient.js';
export interface CdpConnection {
getCdpClient(sessionId: Protocol.Target.SessionID): CdpClient;
}
/**
* Represents a high-level CDP connection to the browser backend.
*
* Manages all CdpClients (each backed by a Session ID) instance for each active
* CDP session.
*/
export declare class MapperCdpConnection implements CdpConnection {
#private;
static readonly LOGGER_PREFIX_RECV: "cdp:RECV ◂";
static readonly LOGGER_PREFIX_SEND: "cdp:SEND ▸";
constructor(transport: Transport, logger?: LoggerFn);
/** Closes the connection to the browser. */
close(): void;
createBrowserSession(): Promise<MapperCdpClient>;
/**
* Gets a CdpClient instance attached to the given session ID,
* or null if the session is not attached.
*/
getCdpClient(sessionId: Protocol.Target.SessionID): MapperCdpClient;
sendCommand<CdpMethod extends keyof ProtocolMapping.Commands>(method: CdpMethod, params?: ProtocolMapping.Commands[CdpMethod]['paramsType'][0], sessionId?: Protocol.Target.SessionID): Promise<object>;
}

Some files were not shown because too many files have changed in this diff Show More