summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/.gitignore2
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md140
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs51
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/package.json15
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts29
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts62
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts130
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts105
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts10
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts211
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts14
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts158
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts245
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts187
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/CDPSession.ts103
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts59
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts58
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts32
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts72
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts35
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts363
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts669
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts126
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts84
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts341
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts34
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts155
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts846
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts344
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts123
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts110
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts173
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/WebWorker.ts48
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts4
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts71
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts129
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts40
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts75
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts119
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts33
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts28
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts69
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts119
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts43
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts12
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts24
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts2
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts2
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts10
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts4
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts11
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts14
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts53
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts7
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts2
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts31
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts4
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts18
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts2
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Cookie.ts186
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts7
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts50
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts8
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts36
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts14
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts2
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts14
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts113
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts24
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts10
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts13
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts6
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts4
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts5
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts48
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts66
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts7
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts29
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json4
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/tsconfig.json4
88 files changed, 3471 insertions, 3279 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/.gitignore b/remote/test/puppeteer/packages/puppeteer-core/.gitignore
index 42061c01a1..3d32a7ba82 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/.gitignore
+++ b/remote/test/puppeteer/packages/puppeteer-core/.gitignore
@@ -1 +1 @@
-README.md \ No newline at end of file
+/README.md \ No newline at end of file
diff --git a/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md b/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md
index 341d706fb4..5076077c9f 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md
+++ b/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md
@@ -20,6 +20,146 @@ All notable changes to this project will be documented in this file. See [standa
* dependencies
* @puppeteer/browsers bumped from 1.5.1 to 1.6.0
+## [22.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.3.0...puppeteer-core-v22.4.0) (2024-03-05)
+
+
+### Features
+
+* implement ElementHandle.uploadFile for WebDriver BiDi ([#11963](https://github.com/puppeteer/puppeteer/issues/11963)) ([accf2b6](https://github.com/puppeteer/puppeteer/commit/accf2b6ca84c93bc700277b4e3382d894fb45a76))
+* **webdriver:** support `Page.deleteCookie()` for WebDriver BiDi ([#12031](https://github.com/puppeteer/puppeteer/issues/12031)) ([7fe22b5](https://github.com/puppeteer/puppeteer/commit/7fe22b533dc96104f28696eb4ff96b2543fd8e5b))
+
+
+### Bug Fixes
+
+* roll to Chrome 122.0.6261.94 (r1250580) ([#12012](https://github.com/puppeteer/puppeteer/issues/12012)) ([7ba5529](https://github.com/puppeteer/puppeteer/commit/7ba5529f8d6f8ed085968b7a9bc6f25f8d91abd5))
+* **webdriver:** wait for response if the response has not completed once navigation has finished ([#12018](https://github.com/puppeteer/puppeteer/issues/12018)) ([6d8831a](https://github.com/puppeteer/puppeteer/commit/6d8831a9c398230f2543c3862d3fe5fc7cd2b940))
+
+## [22.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.2.0...puppeteer-core-v22.3.0) (2024-02-25)
+
+
+### Features
+
+* implement permissions for WebDriver BiDi ([#11979](https://github.com/puppeteer/puppeteer/issues/11979)) ([3a467c3](https://github.com/puppeteer/puppeteer/commit/3a467c39cb60de4237081ee201bd86051887c2f2))
+
+
+### Bug Fixes
+
+* roll to Chrome 122.0.6261.69 (r1250580) ([#11991](https://github.com/puppeteer/puppeteer/issues/11991)) ([eb2c334](https://github.com/puppeteer/puppeteer/commit/eb2c33485ec473e085c6b76b45554758764349d6))
+* supress viewport errors for pages that do not support changing it ([#11970](https://github.com/puppeteer/puppeteer/issues/11970)) ([753a954](https://github.com/puppeteer/puppeteer/commit/753a954456699fc06adf67837225f306711af856))
+
+## [22.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.1.0...puppeteer-core-v22.2.0) (2024-02-21)
+
+
+### Features
+
+* roll to Chrome 122.0.6261.57 (r1250580) ([#11958](https://github.com/puppeteer/puppeteer/issues/11958)) ([70ad3b2](https://github.com/puppeteer/puppeteer/commit/70ad3b244826ca102737e93cd2316e451ea310e8))
+
+
+### Bug Fixes
+
+* deprecate isIncognito ([#11962](https://github.com/puppeteer/puppeteer/issues/11962)) ([ceab7a9](https://github.com/puppeteer/puppeteer/commit/ceab7a9042fe5fc3f71875e75327bb370f1c43a5))
+* roll to Chrome 121.0.6167.184 (r1233107) ([#11948](https://github.com/puppeteer/puppeteer/issues/11948)) ([03ef7a6](https://github.com/puppeteer/puppeteer/commit/03ef7a62c23f2339e4d508d9abfe0894bd790cdd))
+* update touchscreen tests ([#11960](https://github.com/puppeteer/puppeteer/issues/11960)) ([013bd0b](https://github.com/puppeteer/puppeteer/commit/013bd0b12d3a69f9d62fffe7911a327ad26d33d8))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 2.0.1 to 2.1.0
+
+## [22.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.0.0...puppeteer-core-v22.1.0) (2024-02-17)
+
+
+### Features
+
+* support closing workers ([#11870](https://github.com/puppeteer/puppeteer/issues/11870)) ([1bdae40](https://github.com/puppeteer/puppeteer/commit/1bdae40ec865326fcb365320939869a6efb18c8a))
+
+
+### Bug Fixes
+
+* Chrome for Testing downloads have a new URL ([#11923](https://github.com/puppeteer/puppeteer/issues/11923)) ([f00a94a](https://github.com/puppeteer/puppeteer/commit/f00a94a809d38ee1c2c8cfc8597c66db9f3d243d))
+* deprecate `Page.prototype.target` ([#11872](https://github.com/puppeteer/puppeteer/issues/11872)) ([15c986c](https://github.com/puppeteer/puppeteer/commit/15c986c2bc5f5005a738187674cd6c44bcb3df3d))
+* frameElement should work for framesets ([#11842](https://github.com/puppeteer/puppeteer/issues/11842)) ([c5cee0e](https://github.com/puppeteer/puppeteer/commit/c5cee0e37dec8b90a17bf13400ede7ebdf453ac8))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 2.0.0 to 2.0.1
+
+## [22.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.11.0...puppeteer-core-v22.0.0) (2024-02-05)
+
+
+### ⚠ BREAKING CHANGES
+
+* rename createIncognitoBrowserContext to createBrowserContext ([#11834](https://github.com/puppeteer/puppeteer/issues/11834))
+* enable the new-headless mode by default ([#11815](https://github.com/puppeteer/puppeteer/issues/11815))
+* remove networkConditions in favor of PredefinedNetworkConditions ([#11806](https://github.com/puppeteer/puppeteer/issues/11806))
+* use ReadableStreams ([#11805](https://github.com/puppeteer/puppeteer/issues/11805))
+* remove duplicate type names ([#11803](https://github.com/puppeteer/puppeteer/issues/11803))
+* remove add/removeEventListener in favor of on/off ([#11792](https://github.com/puppeteer/puppeteer/issues/11792))
+* make console warn level compatible with WebDriver BiDi ([#11790](https://github.com/puppeteer/puppeteer/issues/11790))
+* remove InterceptResolutionStrategy ([#11788](https://github.com/puppeteer/puppeteer/issues/11788))
+* remove devices in favor of KnownDevices ([#11787](https://github.com/puppeteer/puppeteer/issues/11787))
+* remove `$x` and `waitForXpath` ([#11782](https://github.com/puppeteer/puppeteer/issues/11782))
+* remove waitForTimeout ([#11780](https://github.com/puppeteer/puppeteer/issues/11780))
+* generate accessible PDFs by default ([#11778](https://github.com/puppeteer/puppeteer/issues/11778))
+* remove `error` const, change CustomError to PuppeteerError ([#11777](https://github.com/puppeteer/puppeteer/issues/11777))
+* remove viewport resizing from ElementHandle.screenshot ([#11774](https://github.com/puppeteer/puppeteer/issues/11774))
+* remove PUPPETEER_DOWNLOAD_PATH in favor of PUPPETEER_CACHE_DIR ([#11605](https://github.com/puppeteer/puppeteer/issues/11605))
+* BiDi cookies ([#11532](https://github.com/puppeteer/puppeteer/issues/11532))
+* drop support for node16 ([#10912](https://github.com/puppeteer/puppeteer/issues/10912))
+
+### Features
+
+* BiDi cookies ([#11532](https://github.com/puppeteer/puppeteer/issues/11532)) ([9cb1fde](https://github.com/puppeteer/puppeteer/commit/9cb1fde58949811532644decb79b691318031d8c))
+* drop support for node16 ([#10912](https://github.com/puppeteer/puppeteer/issues/10912)) ([953f420](https://github.com/puppeteer/puppeteer/commit/953f4207b17210fa7231225e6f29a826f77e0832))
+* generate accessible PDFs by default ([#11778](https://github.com/puppeteer/puppeteer/issues/11778)) ([4fc1402](https://github.com/puppeteer/puppeteer/commit/4fc14026e9bfffeedf317e9b61c7cda8509091ba))
+* remove PUPPETEER_DOWNLOAD_PATH in favor of PUPPETEER_CACHE_DIR ([#11605](https://github.com/puppeteer/puppeteer/issues/11605)) ([4677281](https://github.com/puppeteer/puppeteer/commit/467728187737283191f6528676e50d53dae6e5ef))
+
+
+### Bug Fixes
+
+* make console warn level compatible with WebDriver BiDi ([#11790](https://github.com/puppeteer/puppeteer/issues/11790)) ([d4e9d8d](https://github.com/puppeteer/puppeteer/commit/d4e9d8d591e4fb1e2a33fe3a586a8beaccf263e8))
+* remove viewport resizing from ElementHandle.screenshot ([#11774](https://github.com/puppeteer/puppeteer/issues/11774)) ([ced2235](https://github.com/puppeteer/puppeteer/commit/ced2235ada95ad67227df0ce579070ccb501a47b))
+
+
+### Code Refactoring
+
+* enable the new-headless mode by default ([#11815](https://github.com/puppeteer/puppeteer/issues/11815)) ([75c9e11](https://github.com/puppeteer/puppeteer/commit/75c9e117f1bf0d7a4de82c79201d70bf3cee2b6f))
+* remove `$x` and `waitForXpath` ([#11782](https://github.com/puppeteer/puppeteer/issues/11782)) ([53c9134](https://github.com/puppeteer/puppeteer/commit/53c91348094dc0bce59086c98986c5d06a949d08))
+* remove `error` const, change CustomError to PuppeteerError ([#11777](https://github.com/puppeteer/puppeteer/issues/11777)) ([b3bfdd2](https://github.com/puppeteer/puppeteer/commit/b3bfdd2024097be1974e28b3766419189b4a9fe0))
+* remove add/removeEventListener in favor of on/off ([#11792](https://github.com/puppeteer/puppeteer/issues/11792)) ([f160874](https://github.com/puppeteer/puppeteer/commit/f1608743c83e8ce7b56aec98ccdddacc91b86179))
+* remove devices in favor of KnownDevices ([#11787](https://github.com/puppeteer/puppeteer/issues/11787)) ([eb360e3](https://github.com/puppeteer/puppeteer/commit/eb360e3a762d9232a4972d4ec877b7d57a5b60c7))
+* remove duplicate type names ([#11803](https://github.com/puppeteer/puppeteer/issues/11803)) ([514e2d5](https://github.com/puppeteer/puppeteer/commit/514e2d5241dc3a9027c96d739cfc99efc5a02783))
+* remove InterceptResolutionStrategy ([#11788](https://github.com/puppeteer/puppeteer/issues/11788)) ([f18d447](https://github.com/puppeteer/puppeteer/commit/f18d44761cd1acc2e6b867e5eb2ebd753854e9ea))
+* remove networkConditions in favor of PredefinedNetworkConditions ([#11806](https://github.com/puppeteer/puppeteer/issues/11806)) ([7564dfa](https://github.com/puppeteer/puppeteer/commit/7564dfa9110e44b1f50f5fb1543c5c7d8529c182))
+* remove waitForTimeout ([#11780](https://github.com/puppeteer/puppeteer/issues/11780)) ([1900fa9](https://github.com/puppeteer/puppeteer/commit/1900fa94183e0a8654633a91f82b372ad068da71))
+* rename createIncognitoBrowserContext to createBrowserContext ([#11834](https://github.com/puppeteer/puppeteer/issues/11834)) ([46a3ef2](https://github.com/puppeteer/puppeteer/commit/46a3ef2681456d604e775f578fa447a094200610))
+* use ReadableStreams ([#11805](https://github.com/puppeteer/puppeteer/issues/11805)) ([84d9a94](https://github.com/puppeteer/puppeteer/commit/84d9a94d6228800e9f80914472ff2e5a4ee71b18))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.9.1 to 2.0.0
+
+## [21.11.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.10.0...puppeteer-core-v21.11.0) (2024-02-02)
+
+
+### Features
+
+* add outline to PDF generation ([#11779](https://github.com/puppeteer/puppeteer/issues/11779)) ([b99d478](https://github.com/puppeteer/puppeteer/commit/b99d478cd48adc261878836e04eac55ecc2890f2))
+* **bidi:** implement UserContexts ([#11784](https://github.com/puppeteer/puppeteer/issues/11784)) ([2930a70](https://github.com/puppeteer/puppeteer/commit/2930a70c884ce6835ec6bcff27b32f7d273c8af0))
+
+
+### Bug Fixes
+
+* use shareReplay for inflight requests ([#11810](https://github.com/puppeteer/puppeteer/issues/11810)) ([0f0813d](https://github.com/puppeteer/puppeteer/commit/0f0813db38aa0eb14d7514d725852d0cb66f4f0e))
+
## [21.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.9.0...puppeteer-core-v21.10.0) (2024-01-29)
diff --git a/remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs b/remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs
index 723fa2868a..972a080ba0 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs
+++ b/remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs
@@ -1,10 +1,19 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
import {mkdir, readFile, readdir, writeFile} from 'fs/promises';
-import {join} from 'path/posix';
+import Module from 'node:module';
+import path from 'path';
+import posixPath from 'path/posix';
import esbuild from 'esbuild';
import {execa} from 'execa';
import {task} from 'hereby';
+const require = Module.createRequire(import.meta.url);
+
export const generateVersionTask = task({
name: 'generate:version',
run: async () => {
@@ -91,20 +100,52 @@ export const buildTask = task({
});
const builders = [];
for (const format of formats) {
- const folder = join('lib', format, 'third_party');
+ const folder = posixPath.join('lib', format, 'third_party');
for (const name of packages) {
- const path = join(folder, name, `${name}.js`);
+ const entrypoint = posixPath.join(folder, name, `${name}.js`);
builders.push(
await esbuild.build({
- entryPoints: [path],
- outfile: path,
+ entryPoints: [entrypoint],
+ outfile: entrypoint,
bundle: true,
allowOverwrite: true,
format,
target: 'node16',
minify: true,
+ legalComments: 'inline',
})
);
+ let license = '';
+ switch (name) {
+ case 'rxjs':
+ license = await readFile(
+ path.join(
+ path.dirname(require.resolve('rxjs')),
+ '..',
+ '..',
+ 'LICENSE.txt'
+ ),
+ 'utf-8'
+ );
+ break;
+ case 'mitt':
+ license = await readFile(
+ path.join(path.dirname(require.resolve('mitt')), '..', 'LICENSE'),
+ 'utf-8'
+ );
+ break;
+ default:
+ throw new Error(`Add license handling for ${path}`);
+ }
+ const content = await readFile(entrypoint, 'utf-8');
+ await writeFile(
+ entrypoint,
+ `/**
+${license}
+*/
+${content}`,
+ 'utf-8'
+ );
}
}
await Promise.all(builders);
diff --git a/remote/test/puppeteer/packages/puppeteer-core/package.json b/remote/test/puppeteer/packages/puppeteer-core/package.json
index 2f1943bd2f..1d4d564c4f 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/package.json
+++ b/remote/test/puppeteer/packages/puppeteer-core/package.json
@@ -1,6 +1,6 @@
{
"name": "puppeteer-core",
- "version": "21.10.0",
+ "version": "22.4.0",
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
"keywords": [
"puppeteer",
@@ -31,13 +31,13 @@
"url": "https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core"
},
"engines": {
- "node": ">=16.13.2"
+ "node": ">=18"
},
"scripts": {
"build:docs": "wireit",
"build": "wireit",
"check": "tsx tools/ensure-correct-devtools-protocol-package",
- "clean": "../../tools/clean.js",
+ "clean": "../../tools/clean.mjs",
"prepack": "wireit",
"unit": "wireit"
},
@@ -77,7 +77,8 @@
"files": [
"{src,third_party}/**",
"../../versions.js",
- "!src/generated"
+ "!src/generated",
+ "Herebyfile.mjs"
],
"output": [
"lib/{cjs,esm}/**"
@@ -118,11 +119,11 @@
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
- "@puppeteer/browsers": "1.9.1",
- "chromium-bidi": "0.5.6",
+ "@puppeteer/browsers": "2.1.0",
+ "chromium-bidi": "0.5.12",
"cross-fetch": "4.0.0",
"debug": "4.3.4",
- "devtools-protocol": "0.0.1232444",
+ "devtools-protocol": "0.0.1249869",
"ws": "8.16.0"
},
"devDependencies": {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts
index e3b465c80e..6d7ea19d49 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts
@@ -9,7 +9,6 @@ import type {ChildProcess} from 'child_process';
import type {Protocol} from 'devtools-protocol';
import {
- filterAsync,
firstValueFrom,
from,
merge,
@@ -17,7 +16,12 @@ import {
} from '../../third_party/rxjs/rxjs.js';
import type {ProtocolType} from '../common/ConnectOptions.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
-import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
+import {
+ debugError,
+ fromEmitterEvent,
+ filterAsync,
+ timeout,
+} from '../common/util.js';
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
import type {BrowserContext} from './BrowserContext.js';
@@ -136,7 +140,7 @@ export const enum BrowserEvent {
* Emitted when the URL of a target changes. Contains a {@link Target}
* instance.
*
- * @remarks Note that this includes target changes in incognito browser
+ * @remarks Note that this includes target changes in all browser
* contexts.
*/
TargetChanged = 'targetchanged',
@@ -147,7 +151,7 @@ export const enum BrowserEvent {
*
* Contains a {@link Target} instance.
*
- * @remarks Note that this includes target creations in incognito browser
+ * @remarks Note that this includes target creations in all browser
* contexts.
*/
TargetCreated = 'targetcreated',
@@ -155,7 +159,7 @@ export const enum BrowserEvent {
* Emitted when a target is destroyed, for example when a page is closed.
* Contains a {@link Target} instance.
*
- * @remarks Note that this includes target destructions in incognito browser
+ * @remarks Note that this includes target destructions in all browser
* contexts.
*/
TargetDestroyed = 'targetdestroyed',
@@ -165,13 +169,6 @@ export const enum BrowserEvent {
TargetDiscovered = 'targetdiscovered',
}
-export {
- /**
- * @deprecated Use {@link BrowserEvent}.
- */
- BrowserEvent as BrowserEmittedEvents,
-};
-
/**
* @public
*/
@@ -251,7 +248,7 @@ export abstract class Browser extends EventEmitter<BrowserEvents> {
abstract process(): ChildProcess | null;
/**
- * Creates a new incognito {@link BrowserContext | browser context}.
+ * Creates a new {@link BrowserContext | browser context}.
*
* This won't share cookies/cache with other {@link BrowserContext | browser contexts}.
*
@@ -261,15 +258,15 @@ export abstract class Browser extends EventEmitter<BrowserEvents> {
* import puppeteer from 'puppeteer';
*
* const browser = await puppeteer.launch();
- * // Create a new incognito browser context.
- * const context = await browser.createIncognitoBrowserContext();
+ * // Create a new browser context.
+ * const context = await browser.createBrowserContext();
* // Create a new page in a pristine context.
* const page = await context.newPage();
* // Do stuff
* await page.goto('https://example.com');
* ```
*/
- abstract createIncognitoBrowserContext(
+ abstract createBrowserContext(
options?: BrowserContextOptions
): Promise<BrowserContext>;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts
index 79335eb9ed..5e6a5d5d5c 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts
@@ -4,8 +4,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import {
+ firstValueFrom,
+ from,
+ merge,
+ raceWith,
+} from '../../third_party/rxjs/rxjs.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
-import {debugError} from '../common/util.js';
+import {
+ debugError,
+ fromEmitterEvent,
+ filterAsync,
+ timeout,
+} from '../common/util.js';
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
import type {Browser, Permission, WaitForTargetOptions} from './Browser.js';
@@ -38,13 +49,6 @@ export const enum BrowserContextEvent {
TargetDestroyed = 'targetdestroyed',
}
-export {
- /**
- * @deprecated Use {@link BrowserContextEvent}
- */
- BrowserContextEvent as BrowserContextEmittedEvents,
-};
-
/**
* @public
*/
@@ -55,12 +59,13 @@ export interface BrowserContextEvents extends Record<EventType, unknown> {
}
/**
- * {@link BrowserContext} represents individual sessions within a
+ * {@link BrowserContext} represents individual user contexts within a
* {@link Browser | browser}.
*
* When a {@link Browser | browser} is launched, it has a single
* {@link BrowserContext | browser context} by default. Others can be created
- * using {@link Browser.createIncognitoBrowserContext}.
+ * using {@link Browser.createBrowserContext}. Each context has isolated storage
+ * (cookies/localStorage/etc.)
*
* {@link BrowserContext} {@link EventEmitter | emits} various events which are
* documented in the {@link BrowserContextEvent} enum.
@@ -69,11 +74,11 @@ export interface BrowserContextEvents extends Record<EventType, unknown> {
* `window.open`, the popup will belong to the parent {@link Page.browserContext
* | page's browser context}.
*
- * @example Creating an incognito {@link BrowserContext | browser context}:
+ * @example Creating a new {@link BrowserContext | browser context}:
*
* ```ts
- * // Create a new incognito browser context
- * const context = await browser.createIncognitoBrowserContext();
+ * // Create a new browser context
+ * const context = await browser.createBrowserContext();
* // Create a new page inside context.
* const page = await context.newPage();
* // ... do stuff with page ...
@@ -114,10 +119,19 @@ export abstract class BrowserContext extends EventEmitter<BrowserContextEvents>
* );
* ```
*/
- abstract waitForTarget(
+ async waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
- options?: WaitForTargetOptions
- ): Promise<Target>;
+ options: WaitForTargetOptions = {}
+ ): Promise<Target> {
+ const {timeout: ms = 30000} = options;
+ return await firstValueFrom(
+ merge(
+ fromEmitterEvent(this, BrowserContextEvent.TargetCreated),
+ fromEmitterEvent(this, BrowserContextEvent.TargetChanged),
+ from(this.targets())
+ ).pipe(filterAsync(predicate), raceWith(timeout(ms)))
+ );
+ }
/**
* Gets a list of all open {@link Page | pages} inside this
@@ -131,8 +145,20 @@ export abstract class BrowserContext extends EventEmitter<BrowserContextEvents>
/**
* Whether this {@link BrowserContext | browser context} is incognito.
*
- * The {@link Browser.defaultBrowserContext | default browser context} is the
- * only non-incognito browser context.
+ * In Chrome, the
+ * {@link Browser.defaultBrowserContext | default browser context} is the only
+ * non-incognito browser context.
+ *
+ * @deprecated In Chrome, the
+ * {@link Browser.defaultBrowserContext | default browser context} can also be
+ * "icognito" if configured via the arguments and in such cases this getter
+ * returns wrong results (see
+ * https://github.com/puppeteer/puppeteer/issues/8836). Also, the term
+ * "incognito" is not applicable to other browsers. To migrate, check the
+ * {@link Browser.defaultBrowserContext | default browser context} instead: in
+ * Chrome all non-default contexts are incognito, and the default context
+ * might be incognito if you provide the `--incognito` argument when launching
+ * the browser.
*/
abstract isIncognito(): boolean;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts
index 8bdf96f954..3a1fdf1e24 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts
@@ -1,3 +1,8 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import type {Connection} from '../cdp/Connection.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts
index 43fec58e37..9b1326f998 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts
@@ -17,15 +17,10 @@ import type {
NodeFor,
} from '../common/types.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
-import {
- debugError,
- isString,
- withSourcePuppeteerURLIfNone,
-} from '../common/util.js';
+import {isString, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {throwIfDisposed} from '../util/decorators.js';
-import {AsyncDisposableStack} from '../util/disposable.js';
import {_isElementHandle} from './ElementHandleSymbol.js';
import type {
@@ -482,27 +477,6 @@ export abstract class ElementHandle<
}
/**
- * @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix.
- *
- * Example: `await elementHandle.$$('xpath/' + xpathExpression)`
- *
- * The method evaluates the XPath expression relative to the elementHandle.
- * If `xpath` starts with `//` instead of `.//`, the dot will be appended
- * automatically.
- *
- * If there are no such elements, the method will resolve to an empty array.
- * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
- */
- @throwIfDisposed()
- @ElementHandle.bindIsolatedHandle
- async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
- if (expression.startsWith('//')) {
- expression = `.${expression}`;
- }
- return await this.$$(`xpath/${expression}`);
- }
-
- /**
* Wait for an element matching the given selector to appear in the current
* element.
*
@@ -587,84 +561,6 @@ export abstract class ElementHandle<
}
/**
- * @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath`
- * prefix.
- *
- * Example: `await elementHandle.waitForSelector('xpath/' + xpathExpression)`
- *
- * The method evaluates the XPath expression relative to the elementHandle.
- *
- * Wait for the `xpath` within the element. If at the moment of calling the
- * method the `xpath` already exists, the method will return immediately. If
- * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
- * function will throw.
- *
- * If `xpath` starts with `//` instead of `.//`, the dot will be appended
- * automatically.
- *
- * @example
- * This method works across navigation.
- *
- * ```ts
- * import puppeteer from 'puppeteer';
- * (async () => {
- * const browser = await puppeteer.launch();
- * const page = await browser.newPage();
- * let currentURL;
- * page
- * .waitForXPath('//img')
- * .then(() => console.log('First URL with image: ' + currentURL));
- * for (currentURL of [
- * 'https://example.com',
- * 'https://google.com',
- * 'https://bbc.com',
- * ]) {
- * await page.goto(currentURL);
- * }
- * await browser.close();
- * })();
- * ```
- *
- * @param xpath - A
- * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an
- * element to wait for
- * @param options - Optional waiting parameters
- * @returns Promise which resolves when element specified by xpath string is
- * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
- * not found in DOM, otherwise resolves to `ElementHandle`.
- * @remarks
- * The optional Argument `options` have properties:
- *
- * - `visible`: A boolean to wait for element to be present in DOM and to be
- * visible, i.e. to not have `display: none` or `visibility: hidden` CSS
- * properties. Defaults to `false`.
- *
- * - `hidden`: A boolean wait for element to not be found in the DOM or to be
- * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
- * Defaults to `false`.
- *
- * - `timeout`: A number which is maximum time to wait for in milliseconds.
- * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The
- * default value can be changed by using the {@link Page.setDefaultTimeout}
- * method.
- */
- @throwIfDisposed()
- @ElementHandle.bindIsolatedHandle
- async waitForXPath(
- xpath: string,
- options: {
- visible?: boolean;
- hidden?: boolean;
- timeout?: number;
- } = {}
- ): Promise<ElementHandle<Node> | null> {
- if (xpath.startsWith('//')) {
- xpath = `.${xpath}`;
- }
- return await this.waitForSelector(`xpath/${xpath}`, options);
- }
-
- /**
* Converts the current handle to the given element type.
*
* @example
@@ -1346,30 +1242,6 @@ export abstract class ElementHandle<
const page = this.frame.page();
- // If the element is larger than the viewport, `captureBeyondViewport` will
- // _not_ affect element rendering, so we need to adjust the viewport to
- // properly render the element.
- const viewport = page.viewport() ?? {
- width: clip.width,
- height: clip.height,
- };
- await using stack = new AsyncDisposableStack();
- if (clip.width > viewport.width || clip.height > viewport.height) {
- await this.frame.page().setViewport({
- ...viewport,
- width: Math.max(viewport.width, Math.ceil(clip.width)),
- height: Math.max(viewport.height, Math.ceil(clip.height)),
- });
-
- stack.defer(async () => {
- try {
- await this.frame.page().setViewport(viewport);
- } catch (error) {
- debugError(error);
- }
- });
- }
-
// Only scroll the element into view if the user wants it.
if (scrollIntoView) {
await this.scrollIntoViewIfNeeded();
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts
index 757ec872c6..19b5eb7fa0 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts
@@ -14,7 +14,6 @@ import type {
WaitTimeoutOptions,
} from '../api/Page.js';
import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
-import type {IsolatedWorldChart} from '../cdp/IsolatedWorld.js';
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
@@ -38,8 +37,8 @@ import type {CDPSession} from './CDPSession.js';
import type {KeyboardTypeOptions} from './Input.js';
import {
FunctionLocator,
- type Locator,
NodeLocator,
+ type Locator,
} from './locators/locators.js';
import type {Realm} from './Realm.js';
@@ -273,11 +272,6 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
/**
* @internal
*/
- worlds!: IsolatedWorldChart;
-
- /**
- * @internal
- */
_name?: string;
/**
@@ -339,12 +333,7 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
*/
abstract goto(
url: string,
- options?: {
- referer?: string;
- referrerPolicy?: string;
- timeout?: number;
- waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
- }
+ options?: GoToOptions
): Promise<HTTPResponse | null>;
/**
@@ -425,12 +414,12 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
return null;
}
using list = await parentFrame.isolatedRealm().evaluateHandle(() => {
- return document.querySelectorAll('iframe');
+ return document.querySelectorAll('iframe,frame');
});
for await (using iframe of transposeIterableHandle(list)) {
const frame = await iframe.contentFrame();
- if (frame._id === this._id) {
- return iframe.move();
+ if (frame?._id === this._id) {
+ return (iframe as HandleFor<HTMLIFrameElement>).move();
}
}
return null;
@@ -624,23 +613,6 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
}
/**
- * @deprecated Use {@link Frame.$$} with the `xpath` prefix.
- *
- * Example: `await frame.$$('xpath/' + xpathExpression)`
- *
- * This method evaluates the given XPath expression and returns the results.
- * If `xpath` starts with `//` instead of `.//`, the dot will be appended
- * automatically.
- * @param expression - the XPath expression to evaluate.
- */
- @throwIfDetached
- async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
- // eslint-disable-next-line rulesdir/use-using -- This is cached.
- const document = await this.#document();
- return await document.$x(expression);
- }
-
- /**
* Waits for an element matching the given selector to appear in the frame.
*
* This method works across navigations.
@@ -690,39 +662,6 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
}
/**
- * @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix.
- *
- * Example: `await frame.waitForSelector('xpath/' + xpathExpression)`
- *
- * The method evaluates the XPath expression relative to the Frame.
- * If `xpath` starts with `//` instead of `.//`, the dot will be appended
- * automatically.
- *
- * Wait for the `xpath` to appear in page. If at the moment of calling the
- * method the `xpath` already exists, the method will return immediately. If
- * the xpath doesn't appear after the `timeout` milliseconds of waiting, the
- * function will throw.
- *
- * For a code example, see the example for {@link Frame.waitForSelector}. That
- * function behaves identically other than taking a CSS selector rather than
- * an XPath.
- *
- * @param xpath - the XPath expression to wait for.
- * @param options - options to configure the visibility of the element and how
- * long to wait before timing out.
- */
- @throwIfDetached
- async waitForXPath(
- xpath: string,
- options: WaitForSelectorOptions = {}
- ): Promise<ElementHandle<Node> | null> {
- if (xpath.startsWith('//')) {
- xpath = `.${xpath}`;
- }
- return await this.waitForSelector(`xpath/${xpath}`, options);
- }
-
- /**
* @example
* The `waitForFunction` can be used to observe viewport size change:
*
@@ -799,13 +738,7 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
* @param options - Options to configure how long before timing out and at
* what point to consider the content setting successful.
*/
- abstract setContent(
- html: string,
- options?: {
- timeout?: number;
- waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
- }
- ): Promise<void>;
+ abstract setContent(html: string, options?: WaitForOptions): Promise<void>;
/**
* @internal
@@ -1152,32 +1085,6 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
}
/**
- * @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`.
- *
- * Causes your script to wait for the given number of milliseconds.
- *
- * @remarks
- * It's generally recommended to not wait for a number of seconds, but instead
- * use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or
- * {@link Frame.waitForFunction} to wait for exactly the conditions you want.
- *
- * @example
- *
- * Wait for 1 second:
- *
- * ```ts
- * await frame.waitForTimeout(1000);
- * ```
- *
- * @param milliseconds - the number of milliseconds to wait.
- */
- async waitForTimeout(milliseconds: number): Promise<void> {
- return await new Promise(resolve => {
- setTimeout(resolve, milliseconds);
- });
- }
-
- /**
* The frame's title.
*/
@throwIfDetached
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts
index 3c952371ee..d72f088686 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts
@@ -94,7 +94,8 @@ export abstract class HTTPRequest {
/**
* @internal
*/
- _requestId = '';
+ abstract get id(): string;
+
/**
* @internal
*/
@@ -395,13 +396,6 @@ export enum InterceptResolutionAction {
/**
* @public
- *
- * @deprecated please use {@link InterceptResolutionAction} instead.
- */
-export type InterceptResolutionStrategy = InterceptResolutionAction;
-
-/**
- * @public
*/
export type ErrorCode =
| 'aborted'
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts
index deb04628fd..b094d14b2f 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts
@@ -4,26 +4,25 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {Readable} from 'stream';
-
import type {Protocol} from 'devtools-protocol';
import {
concat,
EMPTY,
filter,
- filterAsync,
first,
firstValueFrom,
from,
map,
merge,
mergeMap,
+ mergeScan,
of,
- race,
raceWith,
+ ReplaySubject,
startWith,
switchMap,
+ take,
takeUntil,
timer,
type Observable,
@@ -36,6 +35,11 @@ import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
import type {Credentials, NetworkConditions} from '../cdp/NetworkManager.js';
import type {Tracing} from '../cdp/Tracing.js';
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
+import type {
+ Cookie,
+ CookieParam,
+ DeleteCookiesRequest,
+} from '../common/Cookie.js';
import type {Device} from '../common/Device.js';
import {TargetCloseError} from '../common/Errors.js';
import {
@@ -58,6 +62,7 @@ import type {
import {
debugError,
fromEmitterEvent,
+ filterAsync,
importFSPromises,
isString,
NETWORK_IDLE_TIME,
@@ -477,15 +482,6 @@ export const enum PageEvent {
WorkerDestroyed = 'workerdestroyed',
}
-export {
- /**
- * All the events that a page instance may emit.
- *
- * @deprecated Use {@link PageEvent}.
- */
- PageEvent as PageEmittedEvents,
-};
-
/**
* Denotes the objects received by callback functions for page events.
*
@@ -516,13 +512,6 @@ export interface PageEvents extends Record<EventType, unknown> {
[PageEvent.WorkerDestroyed]: WebWorker;
}
-export type {
- /**
- * @deprecated Use {@link PageEvents}.
- */
- PageEvents as PageEventObject,
-};
-
/**
* @public
*/
@@ -604,8 +593,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
#requestHandlers = new WeakMap<Handler<HTTPRequest>, Handler<HTTPRequest>>();
- #requestsInFlight = 0;
- #inflight$: Observable<number>;
+ #inflight$ = new ReplaySubject<number>(1);
/**
* @internal
@@ -613,39 +601,37 @@ export abstract class Page extends EventEmitter<PageEvents> {
constructor() {
super();
- this.#inflight$ = fromEmitterEvent(this, PageEvent.Request).pipe(
- takeUntil(fromEmitterEvent(this, PageEvent.Close)),
- mergeMap(request => {
- return concat(
- of(1),
- race(
- fromEmitterEvent(this, PageEvent.Response).pipe(
- filter(response => {
- return response.request()._requestId === request._requestId;
- })
- ),
- fromEmitterEvent(this, PageEvent.RequestFailed).pipe(
- filter(failure => {
- return failure._requestId === request._requestId;
- })
- ),
- fromEmitterEvent(this, PageEvent.RequestFinished).pipe(
- filter(success => {
- return success._requestId === request._requestId;
+ fromEmitterEvent(this, PageEvent.Request)
+ .pipe(
+ mergeMap(originalRequest => {
+ return concat(
+ of(1),
+ merge(
+ fromEmitterEvent(this, PageEvent.RequestFailed),
+ fromEmitterEvent(this, PageEvent.RequestFinished),
+ fromEmitterEvent(this, PageEvent.Response).pipe(
+ map(response => {
+ return response.request();
+ })
+ )
+ ).pipe(
+ filter(request => {
+ return request.id === originalRequest.id;
+ }),
+ take(1),
+ map(() => {
+ return -1;
})
)
- ).pipe(
- map(() => {
- return -1;
- })
- )
- );
- })
- );
-
- this.#inflight$.subscribe(count => {
- this.#requestsInFlight += count;
- });
+ );
+ }),
+ mergeScan((acc, addend) => {
+ return of(acc + addend);
+ }, 0),
+ takeUntil(fromEmitterEvent(this, PageEvent.Close)),
+ startWith(0)
+ )
+ .subscribe(this.#inflight$);
}
/**
@@ -771,6 +757,8 @@ export abstract class Page extends EventEmitter<PageEvents> {
/**
* A target this page was created from.
+ *
+ * @deprecated Use {@link Page.createCDPSession} directly.
*/
abstract target(): Target;
@@ -1287,28 +1275,12 @@ export abstract class Page extends EventEmitter<PageEvents> {
}
/**
- * The method evaluates the XPath expression relative to the page document as
- * its context node. If there are no such elements, the method resolves to an
- * empty array.
- *
- * @remarks
- * Shortcut for {@link Frame.$x | Page.mainFrame().$x(expression) }.
- *
- * @param expression - Expression to evaluate
- */
- async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
- return await this.mainFrame().$x(expression);
- }
-
- /**
* If no URLs are specified, this method returns cookies for the current page
* URL. If URLs are specified, only cookies for those URLs are returned.
*/
- abstract cookies(...urls: string[]): Promise<Protocol.Network.Cookie[]>;
+ abstract cookies(...urls: string[]): Promise<Cookie[]>;
- abstract deleteCookie(
- ...cookies: Protocol.Network.DeleteCookiesRequest[]
- ): Promise<void>;
+ abstract deleteCookie(...cookies: DeleteCookiesRequest[]): Promise<void>;
/**
* @example
@@ -1317,7 +1289,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
* await page.setCookie(cookieObject1, cookieObject2);
* ```
*/
- abstract setCookie(...cookies: Protocol.Network.CookieParam[]): Promise<void>;
+ abstract setCookie(...cookies: CookieParam[]): Promise<void>;
/**
* Adds a `<script>` tag into the page with the desired URL or content.
@@ -1776,13 +1748,11 @@ export abstract class Page extends EventEmitter<PageEvents> {
} = options;
return this.#inflight$.pipe(
- startWith(this.#requestsInFlight),
- switchMap(() => {
- if (this.#requestsInFlight > concurrency) {
+ switchMap(inflight => {
+ if (inflight > concurrency) {
return EMPTY;
- } else {
- return timer(idleTime);
}
+ return timer(idleTime);
}),
map(() => {}),
raceWith(
@@ -2604,7 +2574,9 @@ export abstract class Page extends EventEmitter<PageEvents> {
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-print-color-adjust | `-webkit-print-color-adjust`}
* property to force rendering of exact colors.
*/
- abstract createPDFStream(options?: PDFOptions): Promise<Readable>;
+ abstract createPDFStream(
+ options?: PDFOptions
+ ): Promise<ReadableStream<Uint8Array>>;
/**
* {@inheritDoc Page.createPDFStream}
@@ -2785,31 +2757,6 @@ export abstract class Page extends EventEmitter<PageEvents> {
}
/**
- * @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`.
- *
- * Causes your script to wait for the given number of milliseconds.
- *
- * @remarks
- *
- * It's generally recommended to not wait for a number of seconds, but instead
- * use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or
- * {@link Frame.waitForFunction} to wait for exactly the conditions you want.
- *
- * @example
- *
- * Wait for 1 second:
- *
- * ```ts
- * await page.waitForTimeout(1000);
- * ```
- *
- * @param milliseconds - the number of milliseconds to wait.
- */
- waitForTimeout(milliseconds: number): Promise<void> {
- return this.mainFrame().waitForTimeout(milliseconds);
- }
-
- /**
* Wait for the `selector` to appear in page. If at the moment of calling the
* method the `selector` already exists, the method will return immediately. If
* the `selector` doesn't appear after the `timeout` milliseconds of waiting, the
@@ -2869,64 +2816,6 @@ export abstract class Page extends EventEmitter<PageEvents> {
}
/**
- * Wait for the `xpath` to appear in page. If at the moment of calling the
- * method the `xpath` already exists, the method will return immediately. If
- * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
- * function will throw.
- *
- * @example
- * This method works across navigation
- *
- * ```ts
- * import puppeteer from 'puppeteer';
- * (async () => {
- * const browser = await puppeteer.launch();
- * const page = await browser.newPage();
- * let currentURL;
- * page
- * .waitForXPath('//img')
- * .then(() => console.log('First URL with image: ' + currentURL));
- * for (currentURL of [
- * 'https://example.com',
- * 'https://google.com',
- * 'https://bbc.com',
- * ]) {
- * await page.goto(currentURL);
- * }
- * await browser.close();
- * })();
- * ```
- *
- * @param xpath - A
- * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an
- * element to wait for
- * @param options - Optional waiting parameters
- * @returns Promise which resolves when element specified by xpath string is
- * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
- * not found in DOM, otherwise resolves to `ElementHandle`.
- * @remarks
- * The optional Argument `options` have properties:
- *
- * - `visible`: A boolean to wait for element to be present in DOM and to be
- * visible, i.e. to not have `display: none` or `visibility: hidden` CSS
- * properties. Defaults to `false`.
- *
- * - `hidden`: A boolean wait for element to not be found in the DOM or to be
- * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
- * Defaults to `false`.
- *
- * - `timeout`: A number which is maximum time to wait for in milliseconds.
- * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default
- * value can be changed by using the {@link Page.setDefaultTimeout} method.
- */
- waitForXPath(
- xpath: string,
- options?: WaitForSelectorOptions
- ): Promise<ElementHandle<Node> | null> {
- return this.mainFrame().waitForXPath(xpath, options);
- }
-
- /**
* Waits for the provided function, `pageFunction`, to return a truthy value when
* evaluated in the page's context.
*
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts
index 4de287f146..b65452b650 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import {UnsupportedOperation} from '../common/Errors.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
@@ -131,4 +132,8 @@ export abstract class WebWorker extends EventEmitter<
func = withSourcePuppeteerURLIfNone(this.evaluateHandle.name, func);
return await this.mainRealm().evaluateHandle(func, ...args);
}
+
+ async close(): Promise<void> {
+ throw new UnsupportedOperation('WebWorker.close() is not supported');
+ }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts
index 7bec11e38e..d88cc0a17d 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts
@@ -109,24 +109,14 @@ export enum LocatorEvent {
*/
Action = 'action',
}
-export {
- /**
- * @deprecated Use {@link LocatorEvent}.
- */
- LocatorEvent as LocatorEmittedEvents,
-};
+
/**
* @public
*/
export interface LocatorEvents extends Record<EventType, unknown> {
[LocatorEvent.Action]: undefined;
}
-export type {
- /**
- * @deprecated Use {@link LocatorEvents}.
- */
- LocatorEvents as LocatorEventObject,
-};
+
/**
* Locators describe a strategy of locating objects and performing an action on
* them. If the action fails because the object is not ready for the action, the
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts
index 42979790c9..8798d8325d 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts
@@ -8,6 +8,7 @@ import type {ChildProcess} from 'child_process';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type {BrowserEvents} from '../api/Browser.js';
import {
Browser,
BrowserEvent,
@@ -19,22 +20,17 @@ import {BrowserContextEvent} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
-import type {Handler} from '../common/EventEmitter.js';
+import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
+import {bubble} from '../util/decorators.js';
import {BidiBrowserContext} from './BrowserContext.js';
-import {BrowsingContext, BrowsingContextEvent} from './BrowsingContext.js';
import type {BidiConnection} from './Connection.js';
import type {Browser as BrowserCore} from './core/Browser.js';
import {Session} from './core/Session.js';
import type {UserContext} from './core/UserContext.js';
-import {
- BiDiBrowserTarget,
- BiDiBrowsingContextTarget,
- BiDiPageTarget,
- type BidiTarget,
-} from './Target.js';
+import {BidiBrowserTarget} from './Target.js';
/**
* @internal
@@ -89,28 +85,18 @@ export class BidiBrowser extends Browser {
const browser = new BidiBrowser(session.browser, opts);
browser.#initialize();
- await browser.#getTree();
return browser;
}
+ @bubble()
+ accessor #trustedEmitter = new EventEmitter<BrowserEvents>();
+
#process?: ChildProcess;
#closeCallback?: BrowserCloseCallback;
#browserCore: BrowserCore;
#defaultViewport: Viewport | null;
- #targets = new Map<string, BidiTarget>();
#browserContexts = new WeakMap<UserContext, BidiBrowserContext>();
- #browserTarget: BiDiBrowserTarget;
-
- #connectionEventHandlers = new Map<
- Bidi.BrowsingContextEvent['method'],
- Handler<any>
- >([
- ['browsingContext.contextCreated', this.#onContextCreated.bind(this)],
- ['browsingContext.contextDestroyed', this.#onContextDestroyed.bind(this)],
- ['browsingContext.domContentLoaded', this.#onContextDomLoaded.bind(this)],
- ['browsingContext.fragmentNavigated', this.#onContextNavigation.bind(this)],
- ['browsingContext.navigationStarted', this.#onContextNavigation.bind(this)],
- ]);
+ #target = new BidiBrowserTarget(this);
private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) {
super();
@@ -118,22 +104,22 @@ export class BidiBrowser extends Browser {
this.#closeCallback = opts.closeCallback;
this.#browserCore = browserCore;
this.#defaultViewport = opts.defaultViewport;
- this.#browserTarget = new BiDiBrowserTarget(this);
- this.#createBrowserContext(this.#browserCore.defaultUserContext);
}
#initialize() {
+ // Initializing existing contexts.
+ for (const userContext of this.#browserCore.userContexts) {
+ this.#createBrowserContext(userContext);
+ }
+
this.#browserCore.once('disconnected', () => {
- this.emit(BrowserEvent.Disconnected, undefined);
+ this.#trustedEmitter.emit(BrowserEvent.Disconnected, undefined);
+ this.#trustedEmitter.removeAllListeners();
});
this.#process?.once('close', () => {
this.#browserCore.dispose('Browser process exited.', true);
this.connection.dispose();
});
-
- for (const [eventName, handler] of this.#connectionEventHandlers) {
- this.connection.on(eventName, handler);
- }
}
get #browserName() {
@@ -143,82 +129,40 @@ export class BidiBrowser extends Browser {
return this.#browserCore.session.capabilities.browserVersion;
}
+ get cdpSupported(): boolean {
+ return !this.#browserName.toLocaleLowerCase().includes('firefox');
+ }
+
override userAgent(): never {
throw new UnsupportedOperation();
}
#createBrowserContext(userContext: UserContext) {
- const browserContext = new BidiBrowserContext(this, userContext, {
+ const browserContext = BidiBrowserContext.from(this, userContext, {
defaultViewport: this.#defaultViewport,
});
this.#browserContexts.set(userContext, browserContext);
- return browserContext;
- }
-
- #onContextDomLoaded(event: Bidi.BrowsingContext.Info) {
- const target = this.#targets.get(event.context);
- if (target) {
- this.emit(BrowserEvent.TargetChanged, target);
- }
- }
-
- #onContextNavigation(event: Bidi.BrowsingContext.NavigationInfo) {
- const target = this.#targets.get(event.context);
- if (target) {
- this.emit(BrowserEvent.TargetChanged, target);
- target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
- }
- }
- #onContextCreated(event: Bidi.BrowsingContext.ContextCreated['params']) {
- const context = new BrowsingContext(
- this.connection,
- event,
- this.#browserName
+ browserContext.trustedEmitter.on(
+ BrowserContextEvent.TargetCreated,
+ target => {
+ this.#trustedEmitter.emit(BrowserEvent.TargetCreated, target);
+ }
+ );
+ browserContext.trustedEmitter.on(
+ BrowserContextEvent.TargetChanged,
+ target => {
+ this.#trustedEmitter.emit(BrowserEvent.TargetChanged, target);
+ }
+ );
+ browserContext.trustedEmitter.on(
+ BrowserContextEvent.TargetDestroyed,
+ target => {
+ this.#trustedEmitter.emit(BrowserEvent.TargetDestroyed, target);
+ }
);
- this.connection.registerBrowsingContexts(context);
- // TODO: once more browsing context types are supported, this should be
- // updated to support those. Currently, all top-level contexts are treated
- // as pages.
- const browserContext = this.browserContexts().at(-1);
- if (!browserContext) {
- throw new Error('Missing browser contexts');
- }
- const target = !context.parent
- ? new BiDiPageTarget(browserContext, context)
- : new BiDiBrowsingContextTarget(browserContext, context);
- this.#targets.set(event.context, target);
-
- this.emit(BrowserEvent.TargetCreated, target);
- target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
-
- if (context.parent) {
- const topLevel = this.connection.getTopLevelContext(context.parent);
- topLevel.emit(BrowsingContextEvent.Created, context);
- }
- }
-
- async #getTree(): Promise<void> {
- const {result} = await this.connection.send('browsingContext.getTree', {});
- for (const context of result.contexts) {
- this.#onContextCreated(context);
- }
- }
- async #onContextDestroyed(
- event: Bidi.BrowsingContext.ContextDestroyed['params']
- ) {
- const context = this.connection.getBrowsingContext(event.context);
- const topLevelContext = this.connection.getTopLevelContext(event.context);
- topLevelContext.emit(BrowsingContextEvent.Destroyed, context);
- const target = this.#targets.get(event.context);
- const page = await target?.page();
- await page?.close().catch(debugError);
- this.#targets.delete(event.context);
- if (target) {
- this.emit(BrowserEvent.TargetDestroyed, target);
- target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
- }
+ return browserContext;
}
get connection(): BidiConnection {
@@ -231,9 +175,6 @@ export class BidiBrowser extends Browser {
}
override async close(): Promise<void> {
- for (const [eventName, handler] of this.#connectionEventHandlers) {
- this.connection.off(eventName, handler);
- }
if (this.connection.closed) {
return;
}
@@ -250,14 +191,14 @@ export class BidiBrowser extends Browser {
}
override get connected(): boolean {
- return !this.#browserCore.disposed;
+ return !this.#browserCore.disconnected;
}
override process(): ChildProcess | null {
return this.#process ?? null;
}
- override async createIncognitoBrowserContext(
+ override async createBrowserContext(
_options?: BrowserContextOptions
): Promise<BidiBrowserContext> {
const userContext = await this.#browserCore.createUserContext();
@@ -283,19 +224,16 @@ export class BidiBrowser extends Browser {
}
override targets(): Target[] {
- return [this.#browserTarget, ...Array.from(this.#targets.values())];
- }
-
- _getTargetById(id: string): BidiTarget {
- const target = this.#targets.get(id);
- if (!target) {
- throw new Error('Target not found');
- }
- return target;
+ return [
+ this.#target,
+ ...this.browserContexts().flatMap(context => {
+ return context.targets();
+ }),
+ ];
}
- override target(): Target {
- return this.#browserTarget;
+ override target(): BidiBrowserTarget {
+ return this.#target;
}
override async disconnect(): Promise<void> {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts
index feb5e9951d..9976e4cc6a 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts
@@ -6,18 +6,25 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import type {WaitForTargetOptions} from '../api/Browser.js';
-import {BrowserContext} from '../api/BrowserContext.js';
-import type {Page} from '../api/Page.js';
+import type {Permission} from '../api/Browser.js';
+import {WEB_PERMISSION_TO_PROTOCOL_PERMISSION} from '../api/Browser.js';
+import type {BrowserContextEvents} from '../api/BrowserContext.js';
+import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
+import {PageEvent, type Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
-import {UnsupportedOperation} from '../common/Errors.js';
+import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
+import {bubble} from '../util/decorators.js';
import type {BidiBrowser} from './Browser.js';
-import type {BidiConnection} from './Connection.js';
+import type {BrowsingContext} from './core/BrowsingContext.js';
import {UserContext} from './core/UserContext.js';
-import type {BidiPage} from './Page.js';
+import type {BidiFrame} from './Frame.js';
+import {BidiPage} from './Page.js';
+import {BidiWorkerTarget} from './Target.js';
+import {BidiFrameTarget, BidiPageTarget} from './Target.js';
+import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
@@ -30,56 +37,134 @@ export interface BidiBrowserContextOptions {
* @internal
*/
export class BidiBrowserContext extends BrowserContext {
- #browser: BidiBrowser;
- #connection: BidiConnection;
- #defaultViewport: Viewport | null;
- #userContext: UserContext;
+ static from(
+ browser: BidiBrowser,
+ userContext: UserContext,
+ options: BidiBrowserContextOptions
+ ): BidiBrowserContext {
+ const context = new BidiBrowserContext(browser, userContext, options);
+ context.#initialize();
+ return context;
+ }
+
+ @bubble()
+ accessor trustedEmitter = new EventEmitter<BrowserContextEvents>();
+
+ readonly #browser: BidiBrowser;
+ readonly #defaultViewport: Viewport | null;
+ // This is public because of cookies.
+ readonly userContext: UserContext;
+ readonly #pages = new WeakMap<BrowsingContext, BidiPage>();
+ readonly #targets = new Map<
+ BidiPage,
+ [
+ BidiPageTarget,
+ Map<BidiFrame | BidiWebWorker, BidiFrameTarget | BidiWorkerTarget>,
+ ]
+ >();
- constructor(
+ #overrides: Array<{origin: string; permission: Permission}> = [];
+
+ private constructor(
browser: BidiBrowser,
userContext: UserContext,
options: BidiBrowserContextOptions
) {
super();
this.#browser = browser;
- this.#userContext = userContext;
- this.#connection = this.#browser.connection;
+ this.userContext = userContext;
this.#defaultViewport = options.defaultViewport;
}
- override targets(): Target[] {
- return this.#browser.targets().filter(target => {
- return target.browserContext() === this;
+ #initialize() {
+ // Create targets for existing browsing contexts.
+ for (const browsingContext of this.userContext.browsingContexts) {
+ this.#createPage(browsingContext);
+ }
+
+ this.userContext.on('browsingcontext', ({browsingContext}) => {
+ this.#createPage(browsingContext);
+ });
+ this.userContext.on('closed', () => {
+ this.trustedEmitter.removeAllListeners();
});
}
- override waitForTarget(
- predicate: (x: Target) => boolean | Promise<boolean>,
- options: WaitForTargetOptions = {}
- ): Promise<Target> {
- return this.#browser.waitForTarget(target => {
- return target.browserContext() === this && predicate(target);
- }, options);
- }
+ #createPage(browsingContext: BrowsingContext): BidiPage {
+ const page = BidiPage.from(this, browsingContext);
+ this.#pages.set(browsingContext, page);
+ page.trustedEmitter.on(PageEvent.Close, () => {
+ this.#pages.delete(browsingContext);
+ });
- get connection(): BidiConnection {
- return this.#connection;
- }
+ // -- Target stuff starts here --
+ const pageTarget = new BidiPageTarget(page);
+ const pageTargets = new Map();
+ this.#targets.set(page, [pageTarget, pageTargets]);
- override async newPage(): Promise<Page> {
- const {result} = await this.#connection.send('browsingContext.create', {
- type: Bidi.BrowsingContext.CreateType.Tab,
+ page.trustedEmitter.on(PageEvent.FrameAttached, frame => {
+ const bidiFrame = frame as BidiFrame;
+ const target = new BidiFrameTarget(bidiFrame);
+ pageTargets.set(bidiFrame, target);
+ this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
+ });
+ page.trustedEmitter.on(PageEvent.FrameNavigated, frame => {
+ const bidiFrame = frame as BidiFrame;
+ const target = pageTargets.get(bidiFrame);
+ // If there is no target, then this is the page's frame.
+ if (target === undefined) {
+ this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget);
+ } else {
+ this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, target);
+ }
+ });
+ page.trustedEmitter.on(PageEvent.FrameDetached, frame => {
+ const bidiFrame = frame as BidiFrame;
+ const target = pageTargets.get(bidiFrame);
+ if (target === undefined) {
+ return;
+ }
+ pageTargets.delete(bidiFrame);
+ this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
+ });
+
+ page.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
+ const bidiWorker = worker as BidiWebWorker;
+ const target = new BidiWorkerTarget(bidiWorker);
+ pageTargets.set(bidiWorker, target);
+ this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
+ });
+ page.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
+ const bidiWorker = worker as BidiWebWorker;
+ const target = pageTargets.get(bidiWorker);
+ if (target === undefined) {
+ return;
+ }
+ pageTargets.delete(worker);
+ this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
});
- const target = this.#browser._getTargetById(result.context);
- // TODO: once BiDi has some concept matching BrowserContext, the newly
- // created contexts should get automatically assigned to the right
- // BrowserContext. For now, we assume that only explicitly created pages go
- // to the current BrowserContext. Otherwise, the contexts get assigned to
- // the default BrowserContext by the Browser.
- target._setBrowserContext(this);
+ page.trustedEmitter.on(PageEvent.Close, () => {
+ this.#targets.delete(page);
+ this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget);
+ });
+ this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, pageTarget);
+ // -- Target stuff ends here --
+
+ return page;
+ }
+
+ override targets(): Target[] {
+ return [...this.#targets.values()].flatMap(([target, frames]) => {
+ return [target, ...frames.values()];
+ });
+ }
- const page = await target.page();
+ override async newPage(): Promise<Page> {
+ const context = await this.userContext.createBrowsingContext(
+ Bidi.BrowsingContext.CreateType.Tab
+ );
+ const page = this.#pages.get(context)!;
if (!page) {
throw new Error('Page is not found');
}
@@ -99,18 +184,8 @@ export class BidiBrowserContext extends BrowserContext {
throw new Error('Default context cannot be closed!');
}
- // TODO: Remove once we have adopted the new browsing contexts.
- for (const target of this.targets()) {
- const page = await target?.page();
- try {
- await page?.close();
- } catch (error) {
- debugError(error);
- }
- }
-
try {
- await this.#userContext.remove();
+ await this.userContext.remove();
} catch (error) {
debugError(error);
}
@@ -121,25 +196,73 @@ export class BidiBrowserContext extends BrowserContext {
}
override async pages(): Promise<BidiPage[]> {
- const results = await Promise.all(
- [...this.targets()].map(t => {
- return t.page();
- })
- );
- return results.filter((p): p is BidiPage => {
- return p !== null;
+ return [...this.userContext.browsingContexts].map(context => {
+ return this.#pages.get(context)!;
});
}
override isIncognito(): boolean {
- return this.#userContext.id !== UserContext.DEFAULT;
+ return this.userContext.id !== UserContext.DEFAULT;
}
- override overridePermissions(): never {
- throw new UnsupportedOperation();
+ override async overridePermissions(
+ origin: string,
+ permissions: Permission[]
+ ): Promise<void> {
+ const permissionsSet = new Set(
+ permissions.map(permission => {
+ const protocolPermission =
+ WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
+ if (!protocolPermission) {
+ throw new Error('Unknown permission: ' + permission);
+ }
+ return permission;
+ })
+ );
+ await Promise.all(
+ Array.from(WEB_PERMISSION_TO_PROTOCOL_PERMISSION.keys()).map(
+ permission => {
+ const result = this.userContext.setPermissions(
+ origin,
+ {
+ name: permission,
+ },
+ permissionsSet.has(permission)
+ ? Bidi.Permissions.PermissionState.Granted
+ : Bidi.Permissions.PermissionState.Denied
+ );
+ this.#overrides.push({origin, permission});
+ // TODO: some permissions are outdated and setting them to denied does
+ // not work.
+ if (!permissionsSet.has(permission)) {
+ return result.catch(debugError);
+ }
+ return result;
+ }
+ )
+ );
}
- override clearPermissionOverrides(): never {
- throw new UnsupportedOperation();
+ override async clearPermissionOverrides(): Promise<void> {
+ const promises = this.#overrides.map(({permission, origin}) => {
+ return this.userContext
+ .setPermissions(
+ origin,
+ {
+ name: permission,
+ },
+ Bidi.Permissions.PermissionState.Prompt
+ )
+ .catch(debugError);
+ });
+ this.#overrides = [];
+ await Promise.all(promises);
+ }
+
+ override get id(): string | undefined {
+ if (this.userContext.id === UserContext.DEFAULT) {
+ return undefined;
+ }
+ return this.userContext.id;
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts
deleted file mode 100644
index 0804628c06..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
-
-import {CDPSession} from '../api/CDPSession.js';
-import type {Connection as CdpConnection} from '../cdp/Connection.js';
-import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
-import type {EventType} from '../common/EventEmitter.js';
-import {debugError} from '../common/util.js';
-import {Deferred} from '../util/Deferred.js';
-
-import type {BidiConnection} from './Connection.js';
-import {BidiRealm} from './Realm.js';
-
-/**
- * @internal
- */
-export const cdpSessions = new Map<string, CdpSessionWrapper>();
-
-/**
- * @internal
- */
-export class CdpSessionWrapper extends CDPSession {
- #context: BrowsingContext;
- #sessionId = Deferred.create<string>();
- #detached = false;
-
- constructor(context: BrowsingContext, sessionId?: string) {
- super();
- this.#context = context;
- if (!this.#context.supportsCdp()) {
- return;
- }
- if (sessionId) {
- this.#sessionId.resolve(sessionId);
- cdpSessions.set(sessionId, this);
- } else {
- context.connection
- .send('cdp.getSession', {
- context: context.id,
- })
- .then(session => {
- this.#sessionId.resolve(session.result.session!);
- cdpSessions.set(session.result.session!, this);
- })
- .catch(err => {
- this.#sessionId.reject(err);
- });
- }
- }
-
- override connection(): CdpConnection | undefined {
- return undefined;
- }
-
- override async send<T extends keyof ProtocolMapping.Commands>(
- method: T,
- ...paramArgs: ProtocolMapping.Commands[T]['paramsType']
- ): Promise<ProtocolMapping.Commands[T]['returnType']> {
- if (!this.#context.supportsCdp()) {
- throw new UnsupportedOperation(
- 'CDP support is required for this feature. The current browser does not support CDP.'
- );
- }
- if (this.#detached) {
- throw new TargetCloseError(
- `Protocol error (${method}): Session closed. Most likely the page has been closed.`
- );
- }
- const session = await this.#sessionId.valueOrThrow();
- const {result} = await this.#context.connection.send('cdp.sendCommand', {
- method: method,
- params: paramArgs[0],
- session,
- });
- return result.result;
- }
-
- override async detach(): Promise<void> {
- cdpSessions.delete(this.id());
- if (!this.#detached && this.#context.supportsCdp()) {
- await this.#context.cdpSession.send('Target.detachFromTarget', {
- sessionId: this.id(),
- });
- }
- this.#detached = true;
- }
-
- override id(): string {
- const val = this.#sessionId.value();
- return val instanceof Error || val === undefined ? '' : val;
- }
-}
-
-/**
- * Internal events that the BrowsingContext class emits.
- *
- * @internal
- */
-// eslint-disable-next-line @typescript-eslint/no-namespace
-export namespace BrowsingContextEvent {
- /**
- * Emitted on the top-level context, when a descendant context is created.
- */
- export const Created = Symbol('BrowsingContext.created');
- /**
- * Emitted on the top-level context, when a descendant context or the
- * top-level context itself is destroyed.
- */
- export const Destroyed = Symbol('BrowsingContext.destroyed');
-}
-
-/**
- * @internal
- */
-export interface BrowsingContextEvents extends Record<EventType, unknown> {
- [BrowsingContextEvent.Created]: BrowsingContext;
- [BrowsingContextEvent.Destroyed]: BrowsingContext;
-}
-
-/**
- * @internal
- */
-export class BrowsingContext extends BidiRealm {
- #id: string;
- #url: string;
- #cdpSession: CDPSession;
- #parent?: string | null;
- #browserName = '';
-
- constructor(
- connection: BidiConnection,
- info: Bidi.BrowsingContext.Info,
- browserName: string
- ) {
- super(connection);
- this.#id = info.context;
- this.#url = info.url;
- this.#parent = info.parent;
- this.#browserName = browserName;
- this.#cdpSession = new CdpSessionWrapper(this, undefined);
-
- this.on('browsingContext.domContentLoaded', this.#updateUrl.bind(this));
- this.on('browsingContext.fragmentNavigated', this.#updateUrl.bind(this));
- this.on('browsingContext.load', this.#updateUrl.bind(this));
- }
-
- supportsCdp(): boolean {
- return !this.#browserName.toLowerCase().includes('firefox');
- }
-
- #updateUrl(info: Bidi.BrowsingContext.NavigationInfo) {
- this.#url = info.url;
- }
-
- createRealmForSandbox(): BidiRealm {
- return new BidiRealm(this.connection);
- }
-
- get url(): string {
- return this.#url;
- }
-
- get id(): string {
- return this.#id;
- }
-
- get parent(): string | undefined | null {
- return this.#parent;
- }
-
- get cdpSession(): CDPSession {
- return this.#cdpSession;
- }
-
- async sendCdpCommand<T extends keyof ProtocolMapping.Commands>(
- method: T,
- ...paramArgs: ProtocolMapping.Commands[T]['paramsType']
- ): Promise<ProtocolMapping.Commands[T]['returnType']> {
- return await this.#cdpSession.send(method, ...paramArgs);
- }
-
- dispose(): void {
- this.removeAllListeners();
- this.connection.unregisterBrowsingContexts(this.#id);
- void this.#cdpSession.detach().catch(debugError);
- }
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/CDPSession.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/CDPSession.ts
new file mode 100644
index 0000000000..1e0c503498
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/CDPSession.ts
@@ -0,0 +1,103 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
+
+import {CDPSession} from '../api/CDPSession.js';
+import type {Connection as CdpConnection} from '../cdp/Connection.js';
+import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
+import {Deferred} from '../util/Deferred.js';
+
+import type {BidiConnection} from './Connection.js';
+import type {BidiFrame} from './Frame.js';
+
+/**
+ * @internal
+ */
+export class BidiCdpSession extends CDPSession {
+ static sessions = new Map<string, BidiCdpSession>();
+
+ #detached = false;
+ readonly #connection: BidiConnection | undefined = undefined;
+ readonly #sessionId = Deferred.create<string>();
+ readonly frame: BidiFrame;
+
+ constructor(frame: BidiFrame, sessionId?: string) {
+ super();
+ this.frame = frame;
+ if (!this.frame.page().browser().cdpSupported) {
+ return;
+ }
+
+ const connection = this.frame.page().browser().connection;
+ this.#connection = connection;
+
+ if (sessionId) {
+ this.#sessionId.resolve(sessionId);
+ BidiCdpSession.sessions.set(sessionId, this);
+ } else {
+ (async () => {
+ try {
+ const session = await connection.send('cdp.getSession', {
+ context: frame._id,
+ });
+ this.#sessionId.resolve(session.result.session!);
+ BidiCdpSession.sessions.set(session.result.session!, this);
+ } catch (error) {
+ this.#sessionId.reject(error as Error);
+ }
+ })();
+ }
+
+ // SAFETY: We never throw #sessionId.
+ BidiCdpSession.sessions.set(this.#sessionId.value() as string, this);
+ }
+
+ override connection(): CdpConnection | undefined {
+ return undefined;
+ }
+
+ override async send<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ params?: ProtocolMapping.Commands[T]['paramsType'][0]
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ if (this.#connection === undefined) {
+ throw new UnsupportedOperation(
+ 'CDP support is required for this feature. The current browser does not support CDP.'
+ );
+ }
+ if (this.#detached) {
+ throw new TargetCloseError(
+ `Protocol error (${method}): Session closed. Most likely the page has been closed.`
+ );
+ }
+ const session = await this.#sessionId.valueOrThrow();
+ const {result} = await this.#connection.send('cdp.sendCommand', {
+ method: method,
+ params: params,
+ session,
+ });
+ return result.result;
+ }
+
+ override async detach(): Promise<void> {
+ if (this.#connection === undefined || this.#detached) {
+ return;
+ }
+ try {
+ await this.frame.client.send('Target.detachFromTarget', {
+ sessionId: this.id(),
+ });
+ } finally {
+ BidiCdpSession.sessions.delete(this.id());
+ this.#detached = true;
+ }
+ }
+
+ override id(): string {
+ const value = this.#sessionId.value();
+ return typeof value === 'string' ? value : '';
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts
index bce952ba39..dd688c309a 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts
@@ -14,10 +14,10 @@ import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
-import {cdpSessions, type BrowsingContext} from './BrowsingContext.js';
+import {BidiCdpSession} from './CDPSession.js';
import type {
- BidiEvents,
Commands as BidiCommands,
+ BidiEvents,
Connection,
} from './core/Connection.js';
@@ -36,6 +36,10 @@ export interface Commands extends BidiCommands {
params: Bidi.Cdp.GetSessionParameters;
returnType: Bidi.Cdp.GetSessionResult;
};
+ 'cdp.resolveRealm': {
+ params: Bidi.Cdp.ResolveRealmParameters;
+ returnType: Bidi.Cdp.ResolveRealmResult;
+ };
}
/**
@@ -51,7 +55,6 @@ export class BidiConnection
#timeout? = 0;
#closed = false;
#callbacks = new CallbackRegistry();
- #browsingContexts = new Map<string, BrowsingContext>();
#emitters: Array<EventEmitter<any>> = [];
constructor(
@@ -137,12 +140,11 @@ export class BidiConnection
return;
case 'event':
if (isCdpEvent(object)) {
- cdpSessions
+ BidiCdpSession.sessions
.get(object.params.session)
?.emit(object.params.event, object.params.params);
return;
}
- this.#maybeEmitOnContext(object);
// SAFETY: We know the method and parameter still match here.
this.emit(
object.method,
@@ -163,52 +165,6 @@ export class BidiConnection
debugError(object);
}
- #maybeEmitOnContext(event: Bidi.ChromiumBidi.Event) {
- let context: BrowsingContext | undefined;
- // Context specific events
- if ('context' in event.params && event.params.context !== null) {
- context = this.#browsingContexts.get(event.params.context);
- // `log.entryAdded` specific context
- } else if (
- 'source' in event.params &&
- event.params.source.context !== undefined
- ) {
- context = this.#browsingContexts.get(event.params.source.context);
- }
- context?.emit(event.method, event.params);
- }
-
- registerBrowsingContexts(context: BrowsingContext): void {
- this.#browsingContexts.set(context.id, context);
- }
-
- getBrowsingContext(contextId: string): BrowsingContext {
- const currentContext = this.#browsingContexts.get(contextId);
- if (!currentContext) {
- throw new Error(`BrowsingContext ${contextId} does not exist.`);
- }
- return currentContext;
- }
-
- getTopLevelContext(contextId: string): BrowsingContext {
- let currentContext = this.#browsingContexts.get(contextId);
- if (!currentContext) {
- throw new Error(`BrowsingContext ${contextId} does not exist.`);
- }
- while (currentContext.parent) {
- contextId = currentContext.parent;
- currentContext = this.#browsingContexts.get(contextId);
- if (!currentContext) {
- throw new Error(`BrowsingContext ${contextId} does not exist.`);
- }
- }
- return currentContext;
- }
-
- unregisterBrowsingContexts(id: string): void {
- this.#browsingContexts.delete(id);
- }
-
/**
* Unbinds the connection, but keeps the transport open. Useful when the transport will
* be reused by other connection e.g. with different protocol.
@@ -223,7 +179,6 @@ export class BidiConnection
this.#transport.onmessage = () => {};
this.#transport.onclose = () => {};
- this.#browsingContexts.clear();
this.#callbacks.clear();
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts
index 14b87d403b..20dc8d9fc9 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts
@@ -12,40 +12,30 @@ import {debugError} from '../common/util.js';
* @internal
*/
export class BidiDeserializer {
- static deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
- switch (value) {
- case '-0':
- return -0;
- case 'NaN':
- return NaN;
- case 'Infinity':
- return Infinity;
- case '-Infinity':
- return -Infinity;
- default:
- return value;
+ static deserialize(result: Bidi.Script.RemoteValue): any {
+ if (!result) {
+ debugError('Service did not produce a result.');
+ return undefined;
}
- }
- static deserializeLocalValue(result: Bidi.Script.RemoteValue): unknown {
switch (result.type) {
case 'array':
return result.value?.map(value => {
- return BidiDeserializer.deserializeLocalValue(value);
+ return this.deserialize(value);
});
case 'set':
return result.value?.reduce((acc: Set<unknown>, value) => {
- return acc.add(BidiDeserializer.deserializeLocalValue(value));
+ return acc.add(this.deserialize(value));
}, new Set());
case 'object':
return result.value?.reduce((acc: Record<any, unknown>, tuple) => {
- const {key, value} = BidiDeserializer.deserializeTuple(tuple);
+ const {key, value} = this.#deserializeTuple(tuple);
acc[key as any] = value;
return acc;
}, {});
case 'map':
return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
- const {key, value} = BidiDeserializer.deserializeTuple(tuple);
+ const {key, value} = this.#deserializeTuple(tuple);
return acc.set(key, value);
}, new Map());
case 'promise':
@@ -59,7 +49,7 @@ export class BidiDeserializer {
case 'null':
return null;
case 'number':
- return BidiDeserializer.deserializeNumber(result.value);
+ return this.#deserializeNumber(result.value);
case 'bigint':
return BigInt(result.value);
case 'boolean':
@@ -72,25 +62,31 @@ export class BidiDeserializer {
return undefined;
}
- static deserializeTuple([serializedKey, serializedValue]: [
+ static #deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
+ switch (value) {
+ case '-0':
+ return -0;
+ case 'NaN':
+ return NaN;
+ case 'Infinity':
+ return Infinity;
+ case '-Infinity':
+ return -Infinity;
+ default:
+ return value;
+ }
+ }
+
+ static #deserializeTuple([serializedKey, serializedValue]: [
Bidi.Script.RemoteValue | string,
Bidi.Script.RemoteValue,
]): {key: unknown; value: unknown} {
const key =
typeof serializedKey === 'string'
? serializedKey
- : BidiDeserializer.deserializeLocalValue(serializedKey);
- const value = BidiDeserializer.deserializeLocalValue(serializedValue);
+ : this.deserialize(serializedKey);
+ const value = this.deserialize(serializedValue);
return {key, value};
}
-
- static deserialize(result: Bidi.Script.RemoteValue): any {
- if (!result) {
- debugError('Service did not produce a result.');
- return undefined;
- }
-
- return BidiDeserializer.deserializeLocalValue(result);
- }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts
index ce22223461..1774a29f6b 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts
@@ -4,40 +4,26 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-
import {Dialog} from '../api/Dialog.js';
-import type {BrowsingContext} from './BrowsingContext.js';
+import type {UserPrompt} from './core/UserPrompt.js';
-/**
- * @internal
- */
export class BidiDialog extends Dialog {
- #context: BrowsingContext;
+ static from(prompt: UserPrompt): BidiDialog {
+ return new BidiDialog(prompt);
+ }
- /**
- * @internal
- */
- constructor(
- context: BrowsingContext,
- type: Bidi.BrowsingContext.UserPromptOpenedParameters['type'],
- message: string,
- defaultValue?: string
- ) {
- super(type, message, defaultValue);
- this.#context = context;
+ #prompt: UserPrompt;
+ private constructor(prompt: UserPrompt) {
+ super(prompt.info.type, prompt.info.message, prompt.info.defaultValue);
+ this.#prompt = prompt;
}
- /**
- * @internal
- */
override async handle(options: {
accept: boolean;
text?: string;
}): Promise<void> {
- await this.#context.connection.send('browsingContext.handleUserPrompt', {
- context: this.#context.id,
+ await this.#prompt.handle({
accept: options.accept,
userText: options.text,
});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts
index fd886e8c26..4263697671 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts
@@ -6,14 +6,12 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import {type AutofillData, ElementHandle} from '../api/ElementHandle.js';
-import {UnsupportedOperation} from '../common/Errors.js';
+import {ElementHandle, type AutofillData} from '../api/ElementHandle.js';
import {throwIfDisposed} from '../util/decorators.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
-import type {BidiRealm} from './Realm.js';
-import type {Sandbox} from './Sandbox.js';
+import type {BidiFrameRealm} from './Realm.js';
/**
* @internal
@@ -21,28 +19,28 @@ import type {Sandbox} from './Sandbox.js';
export class BidiElementHandle<
ElementType extends Node = Element,
> extends ElementHandle<ElementType> {
+ static from<ElementType extends Node = Element>(
+ value: Bidi.Script.RemoteValue,
+ realm: BidiFrameRealm
+ ): BidiElementHandle<ElementType> {
+ return new BidiElementHandle(value, realm);
+ }
+
declare handle: BidiJSHandle<ElementType>;
- constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
- super(new BidiJSHandle(sandbox, remoteValue));
+ constructor(value: Bidi.Script.RemoteValue, realm: BidiFrameRealm) {
+ super(BidiJSHandle.from(value, realm));
}
- override get realm(): Sandbox {
- return this.handle.realm;
+ override get realm(): BidiFrameRealm {
+ // SAFETY: See the super call in the constructor.
+ return this.handle.realm as BidiFrameRealm;
}
override get frame(): BidiFrame {
return this.realm.environment;
}
- context(): BidiRealm {
- return this.handle.context();
- }
-
- get isPrimitiveValue(): boolean {
- return this.handle.isPrimitiveValue;
- }
-
remoteValue(): Bidi.Script.RemoteValue {
return this.handle.remoteValue();
}
@@ -69,19 +67,53 @@ export class BidiElementHandle<
@ElementHandle.bindIsolatedHandle
override async contentFrame(): Promise<BidiFrame | null> {
using handle = (await this.evaluateHandle(element => {
- if (element instanceof HTMLIFrameElement) {
+ if (
+ element instanceof HTMLIFrameElement ||
+ element instanceof HTMLFrameElement
+ ) {
return element.contentWindow;
}
return;
})) as BidiJSHandle;
const value = handle.remoteValue();
if (value.type === 'window') {
- return this.frame.page().frame(value.value.context);
+ return (
+ this.frame
+ .page()
+ .frames()
+ .find(frame => {
+ return frame._id === value.value.context;
+ }) ?? null
+ );
}
return null;
}
- override uploadFile(this: ElementHandle<HTMLInputElement>): never {
- throw new UnsupportedOperation();
+ override async uploadFile(
+ this: BidiElementHandle<HTMLInputElement>,
+ ...files: string[]
+ ): Promise<void> {
+ // Locate all files and confirm that they exist.
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
+ let path: typeof import('path');
+ try {
+ path = await import('path');
+ } catch (error) {
+ if (error instanceof TypeError) {
+ throw new Error(
+ `JSHandle#uploadFile can only be used in Node-like environments.`
+ );
+ }
+ throw error;
+ }
+
+ files = files.map(file => {
+ if (path.win32.isAbsolute(file) || path.posix.isAbsolute(file)) {
+ return file;
+ } else {
+ return path.resolve(file);
+ }
+ });
+ await this.frame.setFiles(this, files);
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts
deleted file mode 100644
index de95695785..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-import type {Viewport} from '../common/Viewport.js';
-
-import type {BrowsingContext} from './BrowsingContext.js';
-
-/**
- * @internal
- */
-export class EmulationManager {
- #browsingContext: BrowsingContext;
-
- constructor(browsingContext: BrowsingContext) {
- this.#browsingContext = browsingContext;
- }
-
- async emulateViewport(viewport: Viewport): Promise<void> {
- await this.#browsingContext.connection.send('browsingContext.setViewport', {
- context: this.#browsingContext.id,
- viewport:
- viewport.width && viewport.height
- ? {
- width: viewport.width,
- height: viewport.height,
- }
- : null,
- devicePixelRatio: viewport.deviceScaleFactor
- ? viewport.deviceScaleFactor
- : null,
- });
- }
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
index 62c6b5e37e..f6e1304a55 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
@@ -6,97 +6,91 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import {EventEmitter} from '../common/EventEmitter.js';
import type {Awaitable, FlattenHandle} from '../common/types.js';
import {debugError} from '../common/util.js';
-import {assert} from '../util/assert.js';
-import {Deferred} from '../util/Deferred.js';
+import {DisposableStack} from '../util/disposable.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
-import type {BidiConnection} from './Connection.js';
-import {BidiDeserializer} from './Deserializer.js';
+import type {Connection} from './core/Connection.js';
+import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
-import {BidiSerializer} from './Serializer.js';
-
-type SendArgsChannel<Args> = (value: [id: number, args: Args]) => void;
-type SendResolveChannel<Ret> = (
- value: [id: number, resolve: (ret: FlattenHandle<Awaited<Ret>>) => void]
-) => void;
-type SendRejectChannel = (
- value: [id: number, reject: (error: unknown) => void]
+import {BidiJSHandle} from './JSHandle.js';
+
+type CallbackChannel<Args, Ret> = (
+ value: [
+ resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
+ reject: (error: unknown) => void,
+ args: Args,
+ ]
) => void;
-interface RemotePromiseCallbacks {
- resolve: Deferred<Bidi.Script.RemoteValue>;
- reject: Deferred<Bidi.Script.RemoteValue>;
-}
-
/**
* @internal
*/
export class ExposeableFunction<Args extends unknown[], Ret> {
+ static async from<Args extends unknown[], Ret>(
+ frame: BidiFrame,
+ name: string,
+ apply: (...args: Args) => Awaitable<Ret>,
+ isolate = false
+ ): Promise<ExposeableFunction<Args, Ret>> {
+ const func = new ExposeableFunction(frame, name, apply, isolate);
+ await func.#initialize();
+ return func;
+ }
+
readonly #frame;
readonly name;
readonly #apply;
+ readonly #isolate;
- readonly #channels;
- readonly #callerInfos = new Map<
- string,
- Map<number, RemotePromiseCallbacks>
- >();
+ readonly #channel;
- #preloadScriptId?: Bidi.Script.PreloadScript;
+ #scripts: Array<[BidiFrame, Bidi.Script.PreloadScript]> = [];
+ #disposables = new DisposableStack();
constructor(
frame: BidiFrame,
name: string,
- apply: (...args: Args) => Awaitable<Ret>
+ apply: (...args: Args) => Awaitable<Ret>,
+ isolate = false
) {
this.#frame = frame;
this.name = name;
this.#apply = apply;
+ this.#isolate = isolate;
- this.#channels = {
- args: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_args`,
- resolve: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_resolve`,
- reject: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_reject`,
- };
+ this.#channel = `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}`;
}
- async expose(): Promise<void> {
+ async #initialize() {
const connection = this.#connection;
- const channelArguments = this.#channelArguments;
+ const channel = {
+ type: 'channel' as const,
+ value: {
+ channel: this.#channel,
+ ownership: Bidi.Script.ResultOwnership.Root,
+ },
+ };
- // TODO(jrandolf): Implement cleanup with removePreloadScript.
- connection.on(
- Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleArgumentsMessage
+ const connectionEmitter = this.#disposables.use(
+ new EventEmitter(connection)
);
- connection.on(
+ connectionEmitter.on(
Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleResolveMessage
- );
- connection.on(
- Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleRejectMessage
+ this.#handleMessage
);
const functionDeclaration = stringifyFunction(
interpolateFunction(
- (
- sendArgs: SendArgsChannel<Args>,
- sendResolve: SendResolveChannel<Ret>,
- sendReject: SendRejectChannel
- ) => {
- let id = 0;
+ (callback: CallbackChannel<Args, Ret>) => {
Object.assign(globalThis, {
[PLACEHOLDER('name') as string]: function (...args: Args) {
return new Promise<FlattenHandle<Awaited<Ret>>>(
(resolve, reject) => {
- sendArgs([id, args]);
- sendResolve([id, resolve]);
- sendReject([id, reject]);
- ++id;
+ callback([resolve, reject, args]);
}
);
},
@@ -106,179 +100,133 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
)
);
- const {result} = await connection.send('script.addPreloadScript', {
- functionDeclaration,
- arguments: channelArguments,
- contexts: [this.#frame.page().mainFrame()._id],
- });
- this.#preloadScriptId = result.script;
+ const frames = [this.#frame];
+ for (const frame of frames) {
+ frames.push(...frame.childFrames());
+ }
await Promise.all(
- this.#frame
- .page()
- .frames()
- .map(async frame => {
- return await connection.send('script.callFunction', {
- functionDeclaration,
- arguments: channelArguments,
- awaitPromise: false,
- target: frame.mainRealm().realm.target,
- });
- })
+ frames.map(async frame => {
+ const realm = this.#isolate ? frame.isolatedRealm() : frame.mainRealm();
+ try {
+ const [script] = await Promise.all([
+ frame.browsingContext.addPreloadScript(functionDeclaration, {
+ arguments: [channel],
+ sandbox: realm.sandbox,
+ }),
+ realm.realm.callFunction(functionDeclaration, false, {
+ arguments: [channel],
+ }),
+ ]);
+ this.#scripts.push([frame, script]);
+ } catch (error) {
+ // If it errors, the frame probably doesn't support call function. We
+ // fail gracefully.
+ debugError(error);
+ }
+ })
);
}
- #handleArgumentsMessage = async (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.args) {
+ get #connection(): Connection {
+ return this.#frame.page().browser().connection;
+ }
+
+ #handleMessage = async (params: Bidi.Script.MessageParameters) => {
+ if (params.channel !== this.#channel) {
return;
}
- const connection = this.#connection;
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- const args = remoteValue.value?.[1];
- assert(args);
+ const realm = this.#getRealm(params.source);
+ if (!realm) {
+ // Unrelated message.
+ return;
+ }
+
+ using dataHandle = BidiJSHandle.from<
+ [
+ resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
+ reject: (error: unknown) => void,
+ args: Args,
+ ]
+ >(params.data, realm);
+
+ using argsHandle = await dataHandle.evaluateHandle(([, , args]) => {
+ return args;
+ });
+
+ using stack = new DisposableStack();
+ const args = [];
+ for (const [index, handle] of await argsHandle.getProperties()) {
+ stack.use(handle);
+
+ // Element handles are passed as is.
+ if (handle instanceof BidiElementHandle) {
+ args[+index] = handle;
+ stack.use(handle);
+ continue;
+ }
+
+ // Everything else is passed as the JS value.
+ args[+index] = handle.jsonValue();
+ }
+
+ let result;
try {
- const result = await this.#apply(...BidiDeserializer.deserialize(args));
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(([_, resolve]: any, result) => {
- resolve(result);
- }),
- arguments: [
- (await callbacks.resolve.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(result),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
- },
- });
+ result = await this.#apply(...((await Promise.all(args)) as Args));
} catch (error) {
try {
if (error instanceof Error) {
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(
- (
- [_, reject]: [unknown, (error: Error) => void],
- name: string,
- message: string,
- stack?: string
- ) => {
- const error = new Error(message);
- error.name = name;
- if (stack) {
- error.stack = stack;
- }
- reject(error);
+ await dataHandle.evaluate(
+ ([, reject], name, message, stack) => {
+ const error = new Error(message);
+ error.name = name;
+ if (stack) {
+ error.stack = stack;
}
- ),
- arguments: [
- (await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(error.name),
- BidiSerializer.serializeRemoteValue(error.message),
- BidiSerializer.serializeRemoteValue(error.stack),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
+ reject(error);
},
- });
+ error.name,
+ error.message,
+ error.stack
+ );
} else {
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(
- (
- [_, reject]: [unknown, (error: unknown) => void],
- error: unknown
- ) => {
- reject(error);
- }
- ),
- arguments: [
- (await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(error),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
- },
- });
+ await dataHandle.evaluate(([, reject], error) => {
+ reject(error);
+ }, error);
}
} catch (error) {
debugError(error);
}
- }
- };
-
- get #connection(): BidiConnection {
- return this.#frame.context().connection;
- }
-
- get #channelArguments() {
- return [
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.args,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.resolve,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.reject,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- ];
- }
-
- #handleResolveMessage = (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.resolve) {
return;
}
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- callbacks.resolve.resolve(remoteValue);
- };
- #handleRejectMessage = (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.reject) {
- return;
+ try {
+ await dataHandle.evaluate(([resolve], result) => {
+ resolve(result);
+ }, result);
+ } catch (error) {
+ debugError(error);
}
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- callbacks.reject.resolve(remoteValue);
};
- #getCallbacksAndRemoteValue(params: Bidi.Script.MessageParameters) {
- const {data, source} = params;
- assert(data.type === 'array');
- assert(data.value);
-
- const callerIdRemote = data.value[0];
- assert(callerIdRemote);
- assert(callerIdRemote.type === 'number');
- assert(typeof callerIdRemote.value === 'number');
-
- let bindingMap = this.#callerInfos.get(source.realm);
- if (!bindingMap) {
- bindingMap = new Map();
- this.#callerInfos.set(source.realm, bindingMap);
+ #getRealm(source: Bidi.Script.Source) {
+ const frame = this.#findFrame(source.context as string);
+ if (!frame) {
+ // Unrelated message.
+ return;
}
+ return frame.realm(source.realm);
+ }
- const callerId = callerIdRemote.value;
- let callbacks = bindingMap.get(callerId);
- if (!callbacks) {
- callbacks = {
- resolve: new Deferred(),
- reject: new Deferred(),
- };
- bindingMap.set(callerId, callbacks);
+ #findFrame(id: string) {
+ const frames = [this.#frame];
+ for (const frame of frames) {
+ if (frame._id === id) {
+ return frame;
+ }
+ frames.push(...frame.childFrames());
}
- return {callbacks, remoteValue: data};
+ return;
}
[Symbol.dispose](): void {
@@ -286,10 +234,21 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
}
async [Symbol.asyncDispose](): Promise<void> {
- if (this.#preloadScriptId) {
- await this.#connection.send('script.removePreloadScript', {
- script: this.#preloadScriptId,
- });
- }
+ this.#disposables.dispose();
+ await Promise.all(
+ this.#scripts.map(async ([frame, script]) => {
+ const realm = this.#isolate ? frame.isolatedRealm() : frame.mainRealm();
+ try {
+ await Promise.all([
+ realm.evaluate(name => {
+ delete (globalThis as any)[name];
+ }, this.name),
+ frame.browsingContext.removePreloadScript(script),
+ ]);
+ } catch (error) {
+ debugError(error);
+ }
+ })
+ );
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts
index 1638c2cbdf..f2bfd5f64e 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts
@@ -6,15 +6,18 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type {Observable} from '../../third_party/rxjs/rxjs.js';
import {
+ combineLatest,
+ defer,
+ delayWhen,
+ filter,
first,
firstValueFrom,
- forkJoin,
- from,
map,
- merge,
+ of,
raceWith,
- zip,
+ switchMap,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
@@ -25,85 +28,228 @@ import {
type WaitForOptions,
} from '../api/Frame.js';
import type {WaitForSelectorOptions} from '../api/Page.js';
-import {UnsupportedOperation} from '../common/Errors.js';
+import {PageEvent} from '../api/Page.js';
+import {
+ ConsoleMessage,
+ type ConsoleMessageLocation,
+} from '../common/ConsoleMessage.js';
+import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {Awaitable, NodeFor} from '../common/types.js';
-import {
- fromEmitterEvent,
- NETWORK_IDLE_TIME,
- timeout,
- UTILITY_WORLD_NAME,
-} from '../common/util.js';
-import {Deferred} from '../util/Deferred.js';
-import {disposeSymbol} from '../util/disposable.js';
-
-import type {BrowsingContext} from './BrowsingContext.js';
+import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
+
+import {BidiCdpSession} from './CDPSession.js';
+import type {BrowsingContext} from './core/BrowsingContext.js';
+import type {Navigation} from './core/Navigation.js';
+import type {Request} from './core/Request.js';
+import {BidiDeserializer} from './Deserializer.js';
+import {BidiDialog} from './Dialog.js';
+import type {BidiElementHandle} from './ElementHandle.js';
import {ExposeableFunction} from './ExposedFunction.js';
+import {BidiHTTPRequest, requests} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
-import {
- getBiDiLifecycleEvent,
- getBiDiReadinessState,
- rewriteNavigationError,
-} from './lifecycle.js';
+import {BidiJSHandle} from './JSHandle.js';
import type {BidiPage} from './Page.js';
-import {
- MAIN_SANDBOX,
- PUPPETEER_SANDBOX,
- Sandbox,
- type SandboxChart,
-} from './Sandbox.js';
+import type {BidiRealm} from './Realm.js';
+import {BidiFrameRealm} from './Realm.js';
+import {rewriteNavigationError} from './util.js';
+import {BidiWebWorker} from './WebWorker.js';
-/**
- * Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation
- * @internal
- */
export class BidiFrame extends Frame {
- #page: BidiPage;
- #context: BrowsingContext;
- #timeoutSettings: TimeoutSettings;
- #abortDeferred = Deferred.create<never>();
- #disposed = false;
- sandboxes: SandboxChart;
- override _id: string;
-
- constructor(
- page: BidiPage,
- context: BrowsingContext,
- timeoutSettings: TimeoutSettings,
- parentId?: string | null
+ static from(
+ parent: BidiPage | BidiFrame,
+ browsingContext: BrowsingContext
+ ): BidiFrame {
+ const frame = new BidiFrame(parent, browsingContext);
+ frame.#initialize();
+ return frame;
+ }
+
+ readonly #parent: BidiPage | BidiFrame;
+ readonly browsingContext: BrowsingContext;
+ readonly #frames = new WeakMap<BrowsingContext, BidiFrame>();
+ readonly realms: {default: BidiFrameRealm; internal: BidiFrameRealm};
+
+ override readonly _id: string;
+ override readonly client: BidiCdpSession;
+
+ private constructor(
+ parent: BidiPage | BidiFrame,
+ browsingContext: BrowsingContext
) {
super();
- this.#page = page;
- this.#context = context;
- this.#timeoutSettings = timeoutSettings;
- this._id = this.#context.id;
- this._parentId = parentId ?? undefined;
-
- this.sandboxes = {
- [MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings),
- [PUPPETEER_SANDBOX]: new Sandbox(
- UTILITY_WORLD_NAME,
- this,
- context.createRealmForSandbox(),
- timeoutSettings
+ this.#parent = parent;
+ this.browsingContext = browsingContext;
+
+ this._id = browsingContext.id;
+ this.client = new BidiCdpSession(this);
+ this.realms = {
+ default: BidiFrameRealm.from(this.browsingContext.defaultRealm, this),
+ internal: BidiFrameRealm.from(
+ this.browsingContext.createWindowRealm(
+ `__puppeteer_internal_${Math.ceil(Math.random() * 10000)}`
+ ),
+ this
),
};
}
- override get client(): CDPSession {
- return this.context().cdpSession;
+ #initialize(): void {
+ for (const browsingContext of this.browsingContext.children) {
+ this.#createFrameTarget(browsingContext);
+ }
+
+ this.browsingContext.on('browsingcontext', ({browsingContext}) => {
+ this.#createFrameTarget(browsingContext);
+ });
+ this.browsingContext.on('closed', () => {
+ for (const session of BidiCdpSession.sessions.values()) {
+ if (session.frame === this) {
+ void session.detach().catch(debugError);
+ }
+ }
+ this.page().trustedEmitter.emit(PageEvent.FrameDetached, this);
+ });
+
+ this.browsingContext.on('request', ({request}) => {
+ const httpRequest = BidiHTTPRequest.from(request, this);
+ request.once('success', () => {
+ // SAFETY: BidiHTTPRequest will create this before here.
+ this.page().trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
+ });
+
+ request.once('error', () => {
+ this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
+ });
+ });
+
+ this.browsingContext.on('navigation', ({navigation}) => {
+ navigation.once('fragment', () => {
+ this.page().trustedEmitter.emit(PageEvent.FrameNavigated, this);
+ });
+ });
+ this.browsingContext.on('load', () => {
+ this.page().trustedEmitter.emit(PageEvent.Load, undefined);
+ });
+ this.browsingContext.on('DOMContentLoaded', () => {
+ this._hasStartedLoading = true;
+ this.page().trustedEmitter.emit(PageEvent.DOMContentLoaded, undefined);
+ this.page().trustedEmitter.emit(PageEvent.FrameNavigated, this);
+ });
+
+ this.browsingContext.on('userprompt', ({userPrompt}) => {
+ this.page().trustedEmitter.emit(
+ PageEvent.Dialog,
+ BidiDialog.from(userPrompt)
+ );
+ });
+
+ this.browsingContext.on('log', ({entry}) => {
+ if (this._id !== entry.source.context) {
+ return;
+ }
+ if (isConsoleLogEntry(entry)) {
+ const args = entry.args.map(arg => {
+ return this.mainRealm().createHandle(arg);
+ });
+
+ const text = args
+ .reduce((value, arg) => {
+ const parsedValue =
+ arg instanceof BidiJSHandle && arg.isPrimitiveValue
+ ? BidiDeserializer.deserialize(arg.remoteValue())
+ : arg.toString();
+ return `${value} ${parsedValue}`;
+ }, '')
+ .slice(1);
+
+ this.page().trustedEmitter.emit(
+ PageEvent.Console,
+ new ConsoleMessage(
+ entry.method as any,
+ text,
+ args,
+ getStackTraceLocations(entry.stackTrace)
+ )
+ );
+ } else if (isJavaScriptLogEntry(entry)) {
+ const error = new Error(entry.text ?? '');
+
+ const messageHeight = error.message.split('\n').length;
+ const messageLines = error.stack!.split('\n').splice(0, messageHeight);
+
+ const stackLines = [];
+ if (entry.stackTrace) {
+ for (const frame of entry.stackTrace.callFrames) {
+ // Note we need to add `1` because the values are 0-indexed.
+ stackLines.push(
+ ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
+ frame.lineNumber + 1
+ }:${frame.columnNumber + 1})`
+ );
+ if (stackLines.length >= Error.stackTraceLimit) {
+ break;
+ }
+ }
+ }
+
+ error.stack = [...messageLines, ...stackLines].join('\n');
+ this.page().trustedEmitter.emit(PageEvent.PageError, error);
+ } else {
+ debugError(
+ `Unhandled LogEntry with type "${entry.type}", text "${entry.text}" and level "${entry.level}"`
+ );
+ }
+ });
+
+ this.browsingContext.on('worker', ({realm}) => {
+ const worker = BidiWebWorker.from(this, realm);
+ realm.on('destroyed', () => {
+ this.page().trustedEmitter.emit(PageEvent.WorkerDestroyed, worker);
+ });
+ this.page().trustedEmitter.emit(PageEvent.WorkerCreated, worker);
+ });
+ }
+
+ #createFrameTarget(browsingContext: BrowsingContext) {
+ const frame = BidiFrame.from(this, browsingContext);
+ this.#frames.set(browsingContext, frame);
+ this.page().trustedEmitter.emit(PageEvent.FrameAttached, frame);
+
+ browsingContext.on('closed', () => {
+ this.#frames.delete(browsingContext);
+ });
+
+ return frame;
+ }
+
+ get timeoutSettings(): TimeoutSettings {
+ return this.page()._timeoutSettings;
}
- override mainRealm(): Sandbox {
- return this.sandboxes[MAIN_SANDBOX];
+ override mainRealm(): BidiFrameRealm {
+ return this.realms.default;
}
- override isolatedRealm(): Sandbox {
- return this.sandboxes[PUPPETEER_SANDBOX];
+ override isolatedRealm(): BidiFrameRealm {
+ return this.realms.internal;
+ }
+
+ realm(id: string): BidiRealm | undefined {
+ for (const realm of Object.values(this.realms)) {
+ if (realm.realm.id === id) {
+ return realm;
+ }
+ }
+ return;
}
override page(): BidiPage {
- return this.#page;
+ let parent = this.#parent;
+ while (parent instanceof BidiFrame) {
+ parent = parent.#parent;
+ }
+ return parent;
}
override isOOPFrame(): never {
@@ -111,15 +257,36 @@ export class BidiFrame extends Frame {
}
override url(): string {
- return this.#context.url;
+ return this.browsingContext.url;
}
override parentFrame(): BidiFrame | null {
- return this.#page.frame(this._parentId ?? '');
+ if (this.#parent instanceof BidiFrame) {
+ return this.#parent;
+ }
+ return null;
}
override childFrames(): BidiFrame[] {
- return this.#page.childFrames(this.#context.id);
+ return [...this.browsingContext.children].map(child => {
+ return this.#frames.get(child)!;
+ });
+ }
+
+ #detached$() {
+ return defer(() => {
+ if (this.detached) {
+ return of(this as Frame);
+ }
+ return fromEmitterEvent(
+ this.page().trustedEmitter,
+ PageEvent.FrameDetached
+ ).pipe(
+ filter(detachedFrame => {
+ return detachedFrame === this;
+ })
+ );
+ });
}
@throwIfDetached
@@ -127,40 +294,23 @@ export class BidiFrame extends Frame {
url: string,
options: GoToOptions = {}
): Promise<BidiHTTPResponse | null> {
- const {
- waitUntil = 'load',
- timeout: ms = this.#timeoutSettings.navigationTimeout(),
- } = options;
-
- const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
-
- const result$ = zip(
- from(
- this.#context.connection.send('browsingContext.navigate', {
- context: this.#context.id,
- url,
- wait: readiness,
- })
+ const [response] = await Promise.all([
+ this.waitForNavigation(options),
+ // Some implementations currently only report errors when the
+ // readiness=interactive.
+ //
+ // Related: https://bugzilla.mozilla.org/show_bug.cgi?id=1846601
+ this.browsingContext.navigate(
+ url,
+ Bidi.BrowsingContext.ReadinessState.Interactive
),
- ...(networkIdle !== null
- ? [
- this.#page.waitForNetworkIdle$({
- timeout: ms,
- concurrency: networkIdle === 'networkidle2' ? 2 : 0,
- idleTime: NETWORK_IDLE_TIME,
- }),
- ]
- : [])
- ).pipe(
- map(([{result}]) => {
- return result;
- }),
- raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
- rewriteNavigationError(url, ms)
+ ]).catch(
+ rewriteNavigationError(
+ url,
+ options.timeout ?? this.timeoutSettings.navigationTimeout()
+ )
);
-
- const result = await firstValueFrom(result$);
- return this.#page.getNavigationResponse(result.navigation);
+ return response;
}
@throwIfDetached
@@ -168,95 +318,105 @@ export class BidiFrame extends Frame {
html: string,
options: WaitForOptions = {}
): Promise<void> {
- const {
- waitUntil = 'load',
- timeout: ms = this.#timeoutSettings.navigationTimeout(),
- } = options;
-
- const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
-
- const result$ = zip(
- forkJoin([
- fromEmitterEvent(this.#context, waitEvent).pipe(first()),
- from(this.setFrameContent(html)),
- ]).pipe(
- map(() => {
- return null;
- })
+ await Promise.all([
+ this.setFrameContent(html),
+ firstValueFrom(
+ combineLatest([
+ this.#waitForLoad$(options),
+ this.#waitForNetworkIdle$(options),
+ ])
),
- ...(networkIdle !== null
- ? [
- this.#page.waitForNetworkIdle$({
- timeout: ms,
- concurrency: networkIdle === 'networkidle2' ? 2 : 0,
- idleTime: NETWORK_IDLE_TIME,
- }),
- ]
- : [])
- ).pipe(
- raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
- rewriteNavigationError('setContent', ms)
- );
-
- await firstValueFrom(result$);
- }
-
- context(): BrowsingContext {
- return this.#context;
+ ]);
}
@throwIfDetached
override async waitForNavigation(
options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> {
- const {
- waitUntil = 'load',
- timeout: ms = this.#timeoutSettings.navigationTimeout(),
- } = options;
-
- const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
-
- const navigation$ = merge(
- forkJoin([
- fromEmitterEvent(
- this.#context,
- Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted
- ).pipe(first()),
- fromEmitterEvent(this.#context, waitUntilEvent).pipe(first()),
- ]),
- fromEmitterEvent(
- this.#context,
- Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated
+ const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
+
+ const frames = this.childFrames().map(frame => {
+ return frame.#detached$();
+ });
+ return await firstValueFrom(
+ combineLatest([
+ fromEmitterEvent(this.browsingContext, 'navigation').pipe(
+ switchMap(({navigation}) => {
+ return this.#waitForLoad$(options).pipe(
+ delayWhen(() => {
+ if (frames.length === 0) {
+ return of(undefined);
+ }
+ return combineLatest(frames);
+ }),
+ raceWith(
+ fromEmitterEvent(navigation, 'fragment'),
+ fromEmitterEvent(navigation, 'failed').pipe(
+ map(({url}) => {
+ throw new Error(`Navigation failed: ${url}`);
+ })
+ ),
+ fromEmitterEvent(navigation, 'aborted').pipe(
+ map(({url}) => {
+ throw new Error(`Navigation aborted: ${url}`);
+ })
+ )
+ ),
+ switchMap(() => {
+ if (navigation.request) {
+ function requestFinished$(
+ request: Request
+ ): Observable<Navigation> {
+ // Reduces flakiness if the response events arrive after
+ // the load event.
+ // Usually, the response or error is already there at this point.
+ if (request.response || request.error) {
+ return of(navigation);
+ }
+ if (request.redirect) {
+ return requestFinished$(request.redirect);
+ }
+ return fromEmitterEvent(request, 'success')
+ .pipe(
+ raceWith(fromEmitterEvent(request, 'error')),
+ raceWith(fromEmitterEvent(request, 'redirect'))
+ )
+ .pipe(
+ switchMap(() => {
+ return requestFinished$(request);
+ })
+ );
+ }
+ return requestFinished$(navigation.request);
+ }
+ return of(navigation);
+ })
+ );
+ })
+ ),
+ this.#waitForNetworkIdle$(options),
+ ]).pipe(
+ map(([navigation]) => {
+ const request = navigation.request;
+ if (!request) {
+ return null;
+ }
+ const httpRequest = requests.get(request)!;
+ const lastRedirect = httpRequest.redirectChain().at(-1);
+ return (
+ lastRedirect !== undefined ? lastRedirect : httpRequest
+ ).response();
+ }),
+ raceWith(
+ timeout(ms),
+ this.#detached$().pipe(
+ map(() => {
+ throw new TargetCloseError('Frame detached.');
+ })
+ )
+ )
)
- ).pipe(
- map(result => {
- if (Array.isArray(result)) {
- return {result: result[1]};
- }
- return {result};
- })
);
-
- const result$ = zip(
- navigation$,
- ...(networkIdle !== null
- ? [
- this.#page.waitForNetworkIdle$({
- timeout: ms,
- concurrency: networkIdle === 'networkidle2' ? 2 : 0,
- idleTime: NETWORK_IDLE_TIME,
- }),
- ]
- : [])
- ).pipe(
- map(([{result}]) => {
- return result;
- }),
- raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))
- );
-
- const result = await firstValueFrom(result$);
- return this.#page.getNavigationResponse(result.navigation);
}
override waitForDevicePrompt(): never {
@@ -264,18 +424,7 @@ export class BidiFrame extends Frame {
}
override get detached(): boolean {
- return this.#disposed;
- }
-
- [disposeSymbol](): void {
- if (this.#disposed) {
- return;
- }
- this.#disposed = true;
- this.#abortDeferred.reject(new Error('Frame detached'));
- this.#context.dispose();
- this.sandboxes[MAIN_SANDBOX][disposeSymbol]();
- this.sandboxes[PUPPETEER_SANDBOX][disposeSymbol]();
+ return this.browsingContext.closed;
}
#exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>();
@@ -288,21 +437,27 @@ export class BidiFrame extends Frame {
`Failed to add page binding with name ${name}: globalThis['${name}'] already exists!`
);
}
- const exposeable = new ExposeableFunction(this, name, apply);
+ const exposeable = await ExposeableFunction.from(this, name, apply);
this.#exposedFunctions.set(name, exposeable);
- try {
- await exposeable.expose();
- } catch (error) {
- this.#exposedFunctions.delete(name);
- throw error;
+ }
+
+ async removeExposedFunction(name: string): Promise<void> {
+ const exposedFunction = this.#exposedFunctions.get(name);
+ if (!exposedFunction) {
+ throw new Error(
+ `Failed to remove page binding with name ${name}: window['${name}'] does not exists!`
+ );
}
+
+ this.#exposedFunctions.delete(name);
+ await exposedFunction[Symbol.asyncDispose]();
}
override waitForSelector<Selector extends string>(
selector: Selector,
options?: WaitForSelectorOptions
): Promise<ElementHandle<NodeFor<Selector>> | null> {
- if (selector.startsWith('aria')) {
+ if (selector.startsWith('aria') && !this.page().browser().cdpSupported) {
throw new UnsupportedOperation(
'ARIA selector is not supported for BiDi!'
);
@@ -310,4 +465,124 @@ export class BidiFrame extends Frame {
return super.waitForSelector(selector, options);
}
+
+ async createCDPSession(): Promise<CDPSession> {
+ const {sessionId} = await this.client.send('Target.attachToTarget', {
+ targetId: this._id,
+ flatten: true,
+ });
+ return new BidiCdpSession(this, sessionId);
+ }
+
+ @throwIfDetached
+ #waitForLoad$(options: WaitForOptions = {}): Observable<void> {
+ let {waitUntil = 'load'} = options;
+ const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
+
+ if (!Array.isArray(waitUntil)) {
+ waitUntil = [waitUntil];
+ }
+
+ const events = new Set<'load' | 'DOMContentLoaded'>();
+ for (const lifecycleEvent of waitUntil) {
+ switch (lifecycleEvent) {
+ case 'load': {
+ events.add('load');
+ break;
+ }
+ case 'domcontentloaded': {
+ events.add('DOMContentLoaded');
+ break;
+ }
+ }
+ }
+ if (events.size === 0) {
+ return of(undefined);
+ }
+
+ return combineLatest(
+ [...events].map(event => {
+ return fromEmitterEvent(this.browsingContext, event);
+ })
+ ).pipe(
+ map(() => {}),
+ first(),
+ raceWith(
+ timeout(ms),
+ this.#detached$().pipe(
+ map(() => {
+ throw new Error('Frame detached.');
+ })
+ )
+ )
+ );
+ }
+
+ @throwIfDetached
+ #waitForNetworkIdle$(options: WaitForOptions = {}): Observable<void> {
+ let {waitUntil = 'load'} = options;
+ if (!Array.isArray(waitUntil)) {
+ waitUntil = [waitUntil];
+ }
+
+ let concurrency = Infinity;
+ for (const event of waitUntil) {
+ switch (event) {
+ case 'networkidle0': {
+ concurrency = Math.min(0, concurrency);
+ break;
+ }
+ case 'networkidle2': {
+ concurrency = Math.min(2, concurrency);
+ break;
+ }
+ }
+ }
+ if (concurrency === Infinity) {
+ return of(undefined);
+ }
+
+ return this.page().waitForNetworkIdle$({
+ idleTime: 500,
+ timeout: options.timeout ?? this.timeoutSettings.timeout(),
+ concurrency,
+ });
+ }
+
+ @throwIfDetached
+ async setFiles(element: BidiElementHandle, files: string[]): Promise<void> {
+ await this.browsingContext.setFiles(
+ // SAFETY: ElementHandles are always remote references.
+ element.remoteValue() as Bidi.Script.SharedReference,
+ files
+ );
+ }
+}
+
+function isConsoleLogEntry(
+ event: Bidi.Log.Entry
+): event is Bidi.Log.ConsoleLogEntry {
+ return event.type === 'console';
+}
+
+function isJavaScriptLogEntry(
+ event: Bidi.Log.Entry
+): event is Bidi.Log.JavascriptLogEntry {
+ return event.type === 'javascript';
+}
+
+function getStackTraceLocations(
+ stackTrace?: Bidi.Script.StackTrace
+): ConsoleMessageLocation[] {
+ const stackTraceLocations: ConsoleMessageLocation[] = [];
+ if (stackTrace) {
+ for (const callFrame of stackTrace.callFrames) {
+ stackTraceLocations.push({
+ url: callFrame.url,
+ lineNumber: callFrame.lineNumber,
+ columnNumber: callFrame.columnNumber,
+ });
+ }
+ }
+ return stackTraceLocations;
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts
index 57cb801b8c..e75bb0cf3c 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts
@@ -5,106 +5,126 @@
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import type {Frame} from '../api/Frame.js';
+import type {CDPSession} from '../api/CDPSession.js';
import type {
ContinueRequestOverrides,
ResponseForRequest,
} from '../api/HTTPRequest.js';
import {HTTPRequest, type ResourceType} from '../api/HTTPRequest.js';
+import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
-import type {BidiHTTPResponse} from './HTTPResponse.js';
+import type {Request} from './core/Request.js';
+import type {BidiFrame} from './Frame.js';
+import {BidiHTTPResponse} from './HTTPResponse.js';
+
+export const requests = new WeakMap<Request, BidiHTTPRequest>();
/**
* @internal
*/
export class BidiHTTPRequest extends HTTPRequest {
- override _response: BidiHTTPResponse | null = null;
- override _redirectChain: BidiHTTPRequest[];
- _navigationId: string | null;
-
- #url: string;
- #resourceType: ResourceType;
-
- #method: string;
- #postData?: string;
- #headers: Record<string, string> = {};
- #initiator: Bidi.Network.Initiator;
- #frame: Frame | null;
-
- constructor(
- event: Bidi.Network.BeforeRequestSentParameters,
- frame: Frame | null,
- redirectChain: BidiHTTPRequest[] = []
- ) {
+ static from(
+ bidiRequest: Request,
+ frame: BidiFrame | undefined
+ ): BidiHTTPRequest {
+ const request = new BidiHTTPRequest(bidiRequest, frame);
+ request.#initialize();
+ return request;
+ }
+
+ #redirect: BidiHTTPRequest | undefined;
+ #response: BidiHTTPResponse | null = null;
+ override readonly id: string;
+ readonly #frame: BidiFrame | undefined;
+ readonly #request: Request;
+
+ private constructor(request: Request, frame: BidiFrame | undefined) {
super();
+ requests.set(request, this);
- this.#url = event.request.url;
- this.#resourceType = event.initiator.type.toLowerCase() as ResourceType;
- this.#method = event.request.method;
- this.#postData = undefined;
- this.#initiator = event.initiator;
+ this.#request = request;
this.#frame = frame;
-
- this._requestId = event.request.request;
- this._redirectChain = redirectChain;
- this._navigationId = event.navigation;
-
- for (const header of event.request.headers) {
- // TODO: How to handle Binary Headers
- // https://w3c.github.io/webdriver-bidi/#type-network-Header
- if (header.value.type === 'string') {
- this.#headers[header.name.toLowerCase()] = header.value.value;
- }
- }
+ this.id = request.id;
}
- override get client(): never {
+ override get client(): CDPSession {
throw new UnsupportedOperation();
}
+ #initialize() {
+ this.#request.on('redirect', request => {
+ this.#redirect = BidiHTTPRequest.from(request, this.#frame);
+ });
+ this.#request.once('success', data => {
+ this.#response = BidiHTTPResponse.from(data, this);
+ });
+
+ this.#frame?.page().trustedEmitter.emit(PageEvent.Request, this);
+ }
+
override url(): string {
- return this.#url;
+ return this.#request.url;
}
override resourceType(): ResourceType {
- return this.#resourceType;
+ return this.initiator().type.toLowerCase() as ResourceType;
}
override method(): string {
- return this.#method;
+ return this.#request.method;
}
override postData(): string | undefined {
- return this.#postData;
+ throw new UnsupportedOperation();
}
override hasPostData(): boolean {
- return this.#postData !== undefined;
+ throw new UnsupportedOperation();
}
override async fetchPostData(): Promise<string | undefined> {
- return this.#postData;
+ throw new UnsupportedOperation();
}
override headers(): Record<string, string> {
- return this.#headers;
+ const headers: Record<string, string> = {};
+ for (const header of this.#request.headers) {
+ headers[header.name.toLowerCase()] = header.value.value;
+ }
+ return headers;
}
override response(): BidiHTTPResponse | null {
- return this._response;
+ return this.#response;
+ }
+
+ override failure(): {errorText: string} | null {
+ if (this.#request.error === undefined) {
+ return null;
+ }
+ return {errorText: this.#request.error};
}
override isNavigationRequest(): boolean {
- return Boolean(this._navigationId);
+ return this.#request.navigation !== undefined;
}
override initiator(): Bidi.Network.Initiator {
- return this.#initiator;
+ return this.#request.initiator;
}
override redirectChain(): BidiHTTPRequest[] {
- return this._redirectChain.slice();
+ if (this.#redirect === undefined) {
+ return [];
+ }
+ const redirects = [this.#redirect];
+ for (const redirect of redirects) {
+ if (redirect.#redirect !== undefined) {
+ redirects.push(redirect.#redirect);
+ }
+ }
+ return redirects;
}
override enqueueInterceptAction(
@@ -114,8 +134,8 @@ export class BidiHTTPRequest extends HTTPRequest {
void pendingHandler();
}
- override frame(): Frame | null {
- return this.#frame;
+ override frame(): BidiFrame | null {
+ return this.#frame ?? null;
}
override continueRequestOverrides(): never {
@@ -156,8 +176,4 @@ export class BidiHTTPRequest extends HTTPRequest {
): never {
throw new UnsupportedOperation();
}
-
- override failure(): never {
- throw new UnsupportedOperation();
- }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts
index ce28820a65..bad44ff089 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts
@@ -7,11 +7,10 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol';
import type {Frame} from '../api/Frame.js';
-import {
- HTTPResponse as HTTPResponse,
- type RemoteAddress,
-} from '../api/HTTPResponse.js';
+import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
+import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
+import {invokeAtMostOnceForArguments} from '../util/decorators.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
@@ -19,62 +18,62 @@ import type {BidiHTTPRequest} from './HTTPRequest.js';
* @internal
*/
export class BidiHTTPResponse extends HTTPResponse {
+ static from(
+ data: Bidi.Network.ResponseData,
+ request: BidiHTTPRequest
+ ): BidiHTTPResponse {
+ const response = new BidiHTTPResponse(data, request);
+ response.#initialize();
+ return response;
+ }
+
+ #data: Bidi.Network.ResponseData;
#request: BidiHTTPRequest;
- #remoteAddress: RemoteAddress;
- #status: number;
- #statusText: string;
- #url: string;
- #fromCache: boolean;
- #headers: Record<string, string> = {};
- #timings: Record<string, string> | null;
-
- constructor(
- request: BidiHTTPRequest,
- {response}: Bidi.Network.ResponseCompletedParameters
+
+ private constructor(
+ data: Bidi.Network.ResponseData,
+ request: BidiHTTPRequest
) {
super();
+ this.#data = data;
this.#request = request;
+ }
- this.#remoteAddress = {
- ip: '',
- port: -1,
- };
-
- this.#url = response.url;
- this.#fromCache = response.fromCache;
- this.#status = response.status;
- this.#statusText = response.statusText;
- // TODO: File and issue with BiDi spec
- this.#timings = null;
-
- // TODO: Removed once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
- for (const header of response.headers || []) {
- // TODO: How to handle Binary Headers
- // https://w3c.github.io/webdriver-bidi/#type-network-Header
- if (header.value.type === 'string') {
- this.#headers[header.name.toLowerCase()] = header.value.value;
- }
- }
+ #initialize() {
+ this.#request.frame()?.page().trustedEmitter.emit(PageEvent.Response, this);
}
+ @invokeAtMostOnceForArguments
override remoteAddress(): RemoteAddress {
- return this.#remoteAddress;
+ return {
+ ip: '',
+ port: -1,
+ };
}
override url(): string {
- return this.#url;
+ return this.#data.url;
}
override status(): number {
- return this.#status;
+ return this.#data.status;
}
override statusText(): string {
- return this.#statusText;
+ return this.#data.statusText;
}
override headers(): Record<string, string> {
- return this.#headers;
+ const headers: Record<string, string> = {};
+ // TODO: Remove once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
+ for (const header of this.#data.headers || []) {
+ // TODO: How to handle Binary Headers
+ // https://w3c.github.io/webdriver-bidi/#type-network-Header
+ if (header.value.type === 'string') {
+ headers[header.name.toLowerCase()] = header.value.value;
+ }
+ }
+ return headers;
}
override request(): BidiHTTPRequest {
@@ -82,11 +81,12 @@ export class BidiHTTPResponse extends HTTPResponse {
}
override fromCache(): boolean {
- return this.#fromCache;
+ return this.#data.fromCache;
}
override timing(): Protocol.Network.ResourceTiming | null {
- return this.#timings as any;
+ // TODO: File and issue with BiDi spec
+ throw new UnsupportedOperation();
}
override frame(): Frame | null {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts
index 5406556d64..dc70850c12 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts
@@ -12,9 +12,9 @@ import {
Mouse,
MouseButton,
Touchscreen,
+ type KeyboardTypeOptions,
type KeyDownOptions,
type KeyPressOptions,
- type KeyboardTypeOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
@@ -23,7 +23,6 @@ import {
import {UnsupportedOperation} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
-import type {BrowsingContext} from './BrowsingContext.js';
import type {BidiPage} from './Page.js';
const enum InputId {
@@ -288,39 +287,33 @@ export class BidiKeyboard extends Keyboard {
key: KeyInput,
_options?: Readonly<KeyDownOptions>
): Promise<void> {
- await this.#page.connection.send('input.performActions', {
- context: this.#page.mainFrame()._id,
- actions: [
- {
- type: SourceActionsType.Key,
- id: InputId.Keyboard,
- actions: [
- {
- type: ActionType.KeyDown,
- value: getBidiKeyValue(key),
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions: [
+ {
+ type: ActionType.KeyDown,
+ value: getBidiKeyValue(key),
+ },
+ ],
+ },
+ ]);
}
override async up(key: KeyInput): Promise<void> {
- await this.#page.connection.send('input.performActions', {
- context: this.#page.mainFrame()._id,
- actions: [
- {
- type: SourceActionsType.Key,
- id: InputId.Keyboard,
- actions: [
- {
- type: ActionType.KeyUp,
- value: getBidiKeyValue(key),
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions: [
+ {
+ type: ActionType.KeyUp,
+ value: getBidiKeyValue(key),
+ },
+ ],
+ },
+ ]);
}
override async press(
@@ -344,16 +337,13 @@ export class BidiKeyboard extends Keyboard {
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
});
- await this.#page.connection.send('input.performActions', {
- context: this.#page.mainFrame()._id,
- actions: [
- {
- type: SourceActionsType.Key,
- id: InputId.Keyboard,
- actions,
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions,
+ },
+ ]);
}
override async type(
@@ -396,16 +386,13 @@ export class BidiKeyboard extends Keyboard {
);
}
}
- await this.#page.connection.send('input.performActions', {
- context: this.#page.mainFrame()._id,
- actions: [
- {
- type: SourceActionsType.Key,
- id: InputId.Keyboard,
- actions,
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions,
+ },
+ ]);
}
override async sendCharacter(char: string): Promise<void> {
@@ -460,19 +447,17 @@ const getBidiButton = (button: MouseButton) => {
* @internal
*/
export class BidiMouse extends Mouse {
- #context: BrowsingContext;
+ #page: BidiPage;
#lastMovePoint: Point = {x: 0, y: 0};
- constructor(context: BrowsingContext) {
+ constructor(page: BidiPage) {
super();
- this.#context = context;
+ this.#page = page;
}
override async reset(): Promise<void> {
this.#lastMovePoint = {x: 0, y: 0};
- await this.#context.connection.send('input.releaseActions', {
- context: this.#context.id,
- });
+ await this.#page.mainFrame().browsingContext.releaseActions();
}
override async move(
@@ -502,52 +487,43 @@ export class BidiMouse extends Mouse {
});
// https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
this.#lastMovePoint = to;
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Mouse,
- actions,
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions,
+ },
+ ]);
}
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Mouse,
- actions: [
- {
- type: ActionType.PointerDown,
- button: getBidiButton(options.button ?? MouseButton.Left),
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions: [
+ {
+ type: ActionType.PointerDown,
+ button: getBidiButton(options.button ?? MouseButton.Left),
+ },
+ ],
+ },
+ ]);
}
override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Mouse,
- actions: [
- {
- type: ActionType.PointerUp,
- button: getBidiButton(options.button ?? MouseButton.Left),
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions: [
+ {
+ type: ActionType.PointerUp,
+ button: getBidiButton(options.button ?? MouseButton.Left),
+ },
+ ],
+ },
+ ]);
}
override async click(
@@ -582,41 +558,35 @@ export class BidiMouse extends Mouse {
});
}
actions.push(pointerUpAction);
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Mouse,
- actions,
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions,
+ },
+ ]);
}
override async wheel(
options: Readonly<MouseWheelOptions> = {}
): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Wheel,
- id: InputId.Wheel,
- actions: [
- {
- type: ActionType.Scroll,
- ...(this.#lastMovePoint ?? {
- x: 0,
- y: 0,
- }),
- deltaX: options.deltaX ?? 0,
- deltaY: options.deltaY ?? 0,
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Wheel,
+ id: InputId.Wheel,
+ actions: [
+ {
+ type: ActionType.Scroll,
+ ...(this.#lastMovePoint ?? {
+ x: 0,
+ y: 0,
+ }),
+ deltaX: options.deltaX ?? 0,
+ deltaY: options.deltaY ?? 0,
+ },
+ ],
+ },
+ ]);
}
override drag(): never {
@@ -644,11 +614,11 @@ export class BidiMouse extends Mouse {
* @internal
*/
export class BidiTouchscreen extends Touchscreen {
- #context: BrowsingContext;
+ #page: BidiPage;
- constructor(context: BrowsingContext) {
+ constructor(page: BidiPage) {
super();
- this.#context = context;
+ this.#page = page;
}
override async touchStart(
@@ -656,30 +626,27 @@ export class BidiTouchscreen extends Touchscreen {
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Finger,
- parameters: {
- pointerType: Bidi.Input.PointerType.Touch,
- },
- actions: [
- {
- type: ActionType.PointerMove,
- x: Math.round(x),
- y: Math.round(y),
- origin: options.origin,
- },
- {
- type: ActionType.PointerDown,
- button: 0,
- },
- ],
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
},
- ],
- });
+ actions: [
+ {
+ type: ActionType.PointerMove,
+ x: Math.round(x),
+ y: Math.round(y),
+ origin: options.origin,
+ },
+ {
+ type: ActionType.PointerDown,
+ button: 0,
+ },
+ ],
+ },
+ ]);
}
override async touchMove(
@@ -687,46 +654,40 @@ export class BidiTouchscreen extends Touchscreen {
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Finger,
- parameters: {
- pointerType: Bidi.Input.PointerType.Touch,
- },
- actions: [
- {
- type: ActionType.PointerMove,
- x: Math.round(x),
- y: Math.round(y),
- origin: options.origin,
- },
- ],
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
},
- ],
- });
+ actions: [
+ {
+ type: ActionType.PointerMove,
+ x: Math.round(x),
+ y: Math.round(y),
+ origin: options.origin,
+ },
+ ],
+ },
+ ]);
}
override async touchEnd(): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Finger,
- parameters: {
- pointerType: Bidi.Input.PointerType.Touch,
- },
- actions: [
- {
- type: ActionType.PointerUp,
- button: 0,
- },
- ],
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
},
- ],
- });
+ actions: [
+ {
+ type: ActionType.PointerUp,
+ button: 0,
+ },
+ ],
+ },
+ ]);
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts
index 7104601553..10f564f78a 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts
@@ -12,29 +12,28 @@ import {UnsupportedOperation} from '../common/Errors.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiRealm} from './Realm.js';
-import type {Sandbox} from './Sandbox.js';
-import {releaseReference} from './util.js';
/**
* @internal
*/
export class BidiJSHandle<T = unknown> extends JSHandle<T> {
- #disposed = false;
- readonly #sandbox: Sandbox;
+ static from<T>(
+ value: Bidi.Script.RemoteValue,
+ realm: BidiRealm
+ ): BidiJSHandle<T> {
+ return new BidiJSHandle(value, realm);
+ }
+
readonly #remoteValue: Bidi.Script.RemoteValue;
- constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
- super();
- this.#sandbox = sandbox;
- this.#remoteValue = remoteValue;
- }
+ override readonly realm: BidiRealm;
- context(): BidiRealm {
- return this.realm.environment.context();
- }
+ #disposed = false;
- override get realm(): Sandbox {
- return this.#sandbox;
+ constructor(value: Bidi.Script.RemoteValue, realm: BidiRealm) {
+ super();
+ this.#remoteValue = value;
+ this.realm = realm;
}
override get disposed(): boolean {
@@ -56,12 +55,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
return;
}
this.#disposed = true;
- if ('handle' in this.#remoteValue) {
- await releaseReference(
- this.context(),
- this.#remoteValue as Bidi.Script.RemoteReference
- );
- }
+ await this.realm.destroyHandles([this]);
}
get isPrimitiveValue(): boolean {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts
deleted file mode 100644
index 2caaf0ad50..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-
-import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
-import {
- NetworkManagerEvent,
- type NetworkManagerEvents,
-} from '../common/NetworkManagerEvents.js';
-import {DisposableStack} from '../util/disposable.js';
-
-import type {BidiConnection} from './Connection.js';
-import type {BidiFrame} from './Frame.js';
-import {BidiHTTPRequest} from './HTTPRequest.js';
-import {BidiHTTPResponse} from './HTTPResponse.js';
-import type {BidiPage} from './Page.js';
-
-/**
- * @internal
- */
-export class BidiNetworkManager extends EventEmitter<NetworkManagerEvents> {
- #connection: BidiConnection;
- #page: BidiPage;
- #subscriptions = new DisposableStack();
-
- #requestMap = new Map<string, BidiHTTPRequest>();
- #navigationMap = new Map<string, BidiHTTPResponse>();
-
- constructor(connection: BidiConnection, page: BidiPage) {
- super();
- this.#connection = connection;
- this.#page = page;
-
- // TODO: Subscribe to the Frame individually
- this.#subscriptions.use(
- new EventSubscription(
- this.#connection,
- 'network.beforeRequestSent',
- this.#onBeforeRequestSent.bind(this)
- )
- );
- this.#subscriptions.use(
- new EventSubscription(
- this.#connection,
- 'network.responseStarted',
- this.#onResponseStarted.bind(this)
- )
- );
- this.#subscriptions.use(
- new EventSubscription(
- this.#connection,
- 'network.responseCompleted',
- this.#onResponseCompleted.bind(this)
- )
- );
- this.#subscriptions.use(
- new EventSubscription(
- this.#connection,
- 'network.fetchError',
- this.#onFetchError.bind(this)
- )
- );
- }
-
- #onBeforeRequestSent(event: Bidi.Network.BeforeRequestSentParameters): void {
- const frame = this.#page.frame(event.context ?? '');
- if (!frame) {
- return;
- }
- const request = this.#requestMap.get(event.request.request);
- let upsertRequest: BidiHTTPRequest;
- if (request) {
- request._redirectChain.push(request);
- upsertRequest = new BidiHTTPRequest(event, frame, request._redirectChain);
- } else {
- upsertRequest = new BidiHTTPRequest(event, frame, []);
- }
- this.#requestMap.set(event.request.request, upsertRequest);
- this.emit(NetworkManagerEvent.Request, upsertRequest);
- }
-
- #onResponseStarted(_event: Bidi.Network.ResponseStartedParameters) {}
-
- #onResponseCompleted(event: Bidi.Network.ResponseCompletedParameters): void {
- const request = this.#requestMap.get(event.request.request);
- if (!request) {
- return;
- }
- const response = new BidiHTTPResponse(request, event);
- request._response = response;
- if (event.navigation) {
- this.#navigationMap.set(event.navigation, response);
- }
- if (response.fromCache()) {
- this.emit(NetworkManagerEvent.RequestServedFromCache, request);
- }
- this.emit(NetworkManagerEvent.Response, response);
- this.emit(NetworkManagerEvent.RequestFinished, request);
- }
-
- #onFetchError(event: Bidi.Network.FetchErrorParameters) {
- const request = this.#requestMap.get(event.request.request);
- if (!request) {
- return;
- }
- request._failureText = event.errorText;
- this.emit(NetworkManagerEvent.RequestFailed, request);
- this.#requestMap.delete(event.request.request);
- }
-
- getNavigationResponse(navigationId?: string | null): BidiHTTPResponse | null {
- if (!navigationId) {
- return null;
- }
- const response = this.#navigationMap.get(navigationId);
-
- return response ?? null;
- }
-
- inFlightRequestsCount(): number {
- let inFlightRequestCounter = 0;
- for (const request of this.#requestMap.values()) {
- if (!request.response() || request._failureText) {
- inFlightRequestCounter++;
- }
- }
-
- return inFlightRequestCounter;
- }
-
- clearMapAfterFrameDispose(frame: BidiFrame): void {
- for (const [id, request] of this.#requestMap.entries()) {
- if (request.frame() === frame) {
- this.#requestMap.delete(id);
- }
- }
-
- for (const [id, response] of this.#navigationMap.entries()) {
- if (response.frame() === frame) {
- this.#navigationMap.delete(id);
- }
- }
- }
-
- dispose(): void {
- this.removeAllListeners();
- this.#requestMap.clear();
- this.#navigationMap.clear();
- this.#subscriptions.dispose();
- }
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
index 053d23b63a..c662496a18 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
@@ -4,210 +4,115 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {Readable} from 'stream';
-
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol';
-import {
- firstValueFrom,
- from,
- map,
- raceWith,
- zip,
-} from '../../third_party/rxjs/rxjs.js';
+import {firstValueFrom, from, raceWith} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {BoundingBox} from '../api/ElementHandle.js';
import type {WaitForOptions} from '../api/Frame.js';
import type {HTTPResponse} from '../api/HTTPResponse.js';
+import type {
+ MediaFeature,
+ GeolocationOptions,
+ PageEvents,
+} from '../api/Page.js';
import {
Page,
PageEvent,
- type GeolocationOptions,
- type MediaFeature,
type NewDocumentScriptEvaluation,
type ScreenshotOptions,
} from '../api/Page.js';
import {Accessibility} from '../cdp/Accessibility.js';
import {Coverage} from '../cdp/Coverage.js';
-import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js';
-import {FrameTree} from '../cdp/FrameTree.js';
+import {EmulationManager} from '../cdp/EmulationManager.js';
import {Tracing} from '../cdp/Tracing.js';
-import {
- ConsoleMessage,
- type ConsoleMessageLocation,
-} from '../common/ConsoleMessage.js';
-import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
-import type {Handler} from '../common/EventEmitter.js';
-import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
+import type {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js';
+import type {DeleteCookiesRequest} from '../common/Cookie.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import {EventEmitter} from '../common/EventEmitter.js';
import type {PDFOptions} from '../common/PDFOptions.js';
import type {Awaitable} from '../common/types.js';
-import {
- debugError,
- evaluationString,
- NETWORK_IDLE_TIME,
- parsePDFOptions,
- timeout,
- validateDialogType,
-} from '../common/util.js';
+import {evaluationString, parsePDFOptions, timeout} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
-import {Deferred} from '../util/Deferred.js';
-import {disposeSymbol} from '../util/disposable.js';
+import {bubble} from '../util/decorators.js';
import {isErrorLike} from '../util/ErrorLike.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
-import {
- BrowsingContextEvent,
- CdpSessionWrapper,
- type BrowsingContext,
-} from './BrowsingContext.js';
-import type {BidiConnection} from './Connection.js';
-import {BidiDeserializer} from './Deserializer.js';
-import {BidiDialog} from './Dialog.js';
+import type {BidiCdpSession} from './CDPSession.js';
+import type {BrowsingContext} from './core/BrowsingContext.js';
import {BidiElementHandle} from './ElementHandle.js';
-import {EmulationManager} from './EmulationManager.js';
import {BidiFrame} from './Frame.js';
-import type {BidiHTTPRequest} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
import type {BidiJSHandle} from './JSHandle.js';
-import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js';
-import {BidiNetworkManager} from './NetworkManager.js';
-import {createBidiHandle} from './Realm.js';
-import type {BiDiPageTarget} from './Target.js';
+import {rewriteNavigationError} from './util.js';
+import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export class BidiPage extends Page {
- #accessibility: Accessibility;
- #connection: BidiConnection;
- #frameTree = new FrameTree<BidiFrame>();
- #networkManager: BidiNetworkManager;
- #viewport: Viewport | null = null;
- #closedDeferred = Deferred.create<never, TargetCloseError>();
- #subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([
- ['log.entryAdded', this.#onLogEntryAdded.bind(this)],
- ['browsingContext.load', this.#onFrameLoaded.bind(this)],
- [
- 'browsingContext.fragmentNavigated',
- this.#onFrameFragmentNavigated.bind(this),
- ],
- [
- 'browsingContext.domContentLoaded',
- this.#onFrameDOMContentLoaded.bind(this),
- ],
- ['browsingContext.userPromptOpened', this.#onDialog.bind(this)],
- ]);
- readonly #networkManagerEvents = [
- [
- NetworkManagerEvent.Request,
- (request: BidiHTTPRequest) => {
- this.emit(PageEvent.Request, request);
- },
- ],
- [
- NetworkManagerEvent.RequestServedFromCache,
- (request: BidiHTTPRequest) => {
- this.emit(PageEvent.RequestServedFromCache, request);
- },
- ],
- [
- NetworkManagerEvent.RequestFailed,
- (request: BidiHTTPRequest) => {
- this.emit(PageEvent.RequestFailed, request);
- },
- ],
- [
- NetworkManagerEvent.RequestFinished,
- (request: BidiHTTPRequest) => {
- this.emit(PageEvent.RequestFinished, request);
- },
- ],
- [
- NetworkManagerEvent.Response,
- (response: BidiHTTPResponse) => {
- this.emit(PageEvent.Response, response);
- },
- ],
- ] as const;
-
- readonly #browsingContextEvents = new Map<symbol, Handler<any>>([
- [BrowsingContextEvent.Created, this.#onContextCreated.bind(this)],
- [BrowsingContextEvent.Destroyed, this.#onContextDestroyed.bind(this)],
- ]);
- #tracing: Tracing;
- #coverage: Coverage;
- #cdpEmulationManager: CdpEmulationManager;
- #emulationManager: EmulationManager;
- #mouse: BidiMouse;
- #touchscreen: BidiTouchscreen;
- #keyboard: BidiKeyboard;
- #browsingContext: BrowsingContext;
- #browserContext: BidiBrowserContext;
- #target: BiDiPageTarget;
-
- _client(): CDPSession {
- return this.mainFrame().context().cdpSession;
- }
-
- constructor(
- browsingContext: BrowsingContext,
+ static from(
browserContext: BidiBrowserContext,
- target: BiDiPageTarget
- ) {
- super();
- this.#browsingContext = browsingContext;
- this.#browserContext = browserContext;
- this.#target = target;
- this.#connection = browsingContext.connection;
+ browsingContext: BrowsingContext
+ ): BidiPage {
+ const page = new BidiPage(browserContext, browsingContext);
+ page.#initialize();
+ return page;
+ }
- for (const [event, subscriber] of this.#browsingContextEvents) {
- this.#browsingContext.on(event, subscriber);
- }
+ @bubble()
+ accessor trustedEmitter = new EventEmitter<PageEvents>();
- this.#networkManager = new BidiNetworkManager(this.#connection, this);
+ readonly #browserContext: BidiBrowserContext;
+ readonly #frame: BidiFrame;
+ #viewport: Viewport | null = null;
+ readonly #workers = new Set<BidiWebWorker>();
- for (const [event, subscriber] of this.#subscribedEvents) {
- this.#connection.on(event, subscriber);
- }
+ readonly keyboard: BidiKeyboard;
+ readonly mouse: BidiMouse;
+ readonly touchscreen: BidiTouchscreen;
+ readonly accessibility: Accessibility;
+ readonly tracing: Tracing;
+ readonly coverage: Coverage;
+ readonly #cdpEmulationManager: EmulationManager;
- for (const [event, subscriber] of this.#networkManagerEvents) {
- // TODO: remove any
- this.#networkManager.on(event, subscriber as any);
- }
+ _client(): BidiCdpSession {
+ return this.#frame.client;
+ }
- const frame = new BidiFrame(
- this,
- this.#browsingContext,
- this._timeoutSettings,
- this.#browsingContext.parent
- );
- this.#frameTree.addFrame(frame);
- this.emit(PageEvent.FrameAttached, frame);
+ private constructor(
+ browserContext: BidiBrowserContext,
+ browsingContext: BrowsingContext
+ ) {
+ super();
+ this.#browserContext = browserContext;
+ this.#frame = BidiFrame.from(this, browsingContext);
- // TODO: https://github.com/w3c/webdriver-bidi/issues/443
- this.#accessibility = new Accessibility(
- this.mainFrame().context().cdpSession
- );
- this.#tracing = new Tracing(this.mainFrame().context().cdpSession);
- this.#coverage = new Coverage(this.mainFrame().context().cdpSession);
- this.#cdpEmulationManager = new CdpEmulationManager(
- this.mainFrame().context().cdpSession
- );
- this.#emulationManager = new EmulationManager(browsingContext);
- this.#mouse = new BidiMouse(this.mainFrame().context());
- this.#touchscreen = new BidiTouchscreen(this.mainFrame().context());
- this.#keyboard = new BidiKeyboard(this);
+ this.#cdpEmulationManager = new EmulationManager(this.#frame.client);
+ this.accessibility = new Accessibility(this.#frame.client);
+ this.tracing = new Tracing(this.#frame.client);
+ this.coverage = new Coverage(this.#frame.client);
+ this.keyboard = new BidiKeyboard(this);
+ this.mouse = new BidiMouse(this);
+ this.touchscreen = new BidiTouchscreen(this);
}
- /**
- * @internal
- */
- get connection(): BidiConnection {
- return this.#connection;
+ #initialize() {
+ this.#frame.browsingContext.on('closed', () => {
+ this.trustedEmitter.emit(PageEvent.Close, undefined);
+ this.trustedEmitter.removeAllListeners();
+ });
+
+ this.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
+ this.#workers.add(worker as BidiWebWorker);
+ });
+ this.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
+ this.#workers.delete(worker as BidiWebWorker);
+ });
}
override async setUserAgent(
@@ -234,46 +139,15 @@ export class BidiPage extends Page {
prototypeHandle.id,
'Prototype JSHandle must not be referencing primitive value'
);
- const response = await this.mainFrame().client.send(
- 'Runtime.queryObjects',
- {
- prototypeObjectId: prototypeHandle.id,
- }
- );
- return createBidiHandle(this.mainFrame().mainRealm(), {
+ const response = await this.#frame.client.send('Runtime.queryObjects', {
+ prototypeObjectId: prototypeHandle.id,
+ });
+ return this.#frame.mainRealm().createHandle({
type: 'array',
handle: response.objects.objectId,
}) as BidiJSHandle<Prototype[]>;
}
- _setBrowserContext(browserContext: BidiBrowserContext): void {
- this.#browserContext = browserContext;
- }
-
- override get accessibility(): Accessibility {
- return this.#accessibility;
- }
-
- override get tracing(): Tracing {
- return this.#tracing;
- }
-
- override get coverage(): Coverage {
- return this.#coverage;
- }
-
- override get mouse(): BidiMouse {
- return this.#mouse;
- }
-
- override get touchscreen(): BidiTouchscreen {
- return this.#touchscreen;
- }
-
- override get keyboard(): BidiKeyboard {
- return this.#keyboard;
- }
-
override browser(): BidiBrowser {
return this.browserContext().browser();
}
@@ -283,14 +157,9 @@ export class BidiPage extends Page {
}
override mainFrame(): BidiFrame {
- const mainFrame = this.#frameTree.getMainFrame();
- assert(mainFrame, 'Requesting main frame too early!');
- return mainFrame;
+ return this.#frame;
}
- /**
- * @internal
- */
async focusedFrame(): Promise<BidiFrame> {
using frame = await this.mainFrame()
.isolatedRealm()
@@ -310,216 +179,38 @@ export class BidiPage extends Page {
}
override frames(): BidiFrame[] {
- return Array.from(this.#frameTree.frames());
- }
-
- frame(frameId?: string): BidiFrame | null {
- return this.#frameTree.getById(frameId ?? '') || null;
- }
-
- childFrames(frameId: string): BidiFrame[] {
- return this.#frameTree.childFrames(frameId);
- }
-
- #onFrameLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
- const frame = this.frame(info.context);
- if (frame && this.mainFrame() === frame) {
- this.emit(PageEvent.Load, undefined);
- }
- }
-
- #onFrameFragmentNavigated(info: Bidi.BrowsingContext.NavigationInfo): void {
- const frame = this.frame(info.context);
- if (frame) {
- this.emit(PageEvent.FrameNavigated, frame);
- }
- }
-
- #onFrameDOMContentLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
- const frame = this.frame(info.context);
- if (frame) {
- frame._hasStartedLoading = true;
- if (this.mainFrame() === frame) {
- this.emit(PageEvent.DOMContentLoaded, undefined);
- }
- this.emit(PageEvent.FrameNavigated, frame);
- }
- }
-
- #onContextCreated(context: BrowsingContext): void {
- if (
- !this.frame(context.id) &&
- (this.frame(context.parent ?? '') || !this.#frameTree.getMainFrame())
- ) {
- const frame = new BidiFrame(
- this,
- context,
- this._timeoutSettings,
- context.parent
- );
- this.#frameTree.addFrame(frame);
- if (frame !== this.mainFrame()) {
- this.emit(PageEvent.FrameAttached, frame);
- }
- }
- }
-
- #onContextDestroyed(context: BrowsingContext): void {
- const frame = this.frame(context.id);
-
- if (frame) {
- if (frame === this.mainFrame()) {
- this.emit(PageEvent.Close, undefined);
- }
- this.#removeFramesRecursively(frame);
- }
- }
-
- #removeFramesRecursively(frame: BidiFrame): void {
- for (const child of frame.childFrames()) {
- this.#removeFramesRecursively(child);
- }
- frame[disposeSymbol]();
- this.#networkManager.clearMapAfterFrameDispose(frame);
- this.#frameTree.removeFrame(frame);
- this.emit(PageEvent.FrameDetached, frame);
- }
-
- #onLogEntryAdded(event: Bidi.Log.Entry): void {
- const frame = this.frame(event.source.context);
- if (!frame) {
- return;
- }
- if (isConsoleLogEntry(event)) {
- const args = event.args.map(arg => {
- return createBidiHandle(frame.mainRealm(), arg);
- });
-
- const text = args
- .reduce((value, arg) => {
- const parsedValue = arg.isPrimitiveValue
- ? BidiDeserializer.deserialize(arg.remoteValue())
- : arg.toString();
- return `${value} ${parsedValue}`;
- }, '')
- .slice(1);
-
- this.emit(
- PageEvent.Console,
- new ConsoleMessage(
- event.method as any,
- text,
- args,
- getStackTraceLocations(event.stackTrace)
- )
- );
- } else if (isJavaScriptLogEntry(event)) {
- const error = new Error(event.text ?? '');
-
- const messageHeight = error.message.split('\n').length;
- const messageLines = error.stack!.split('\n').splice(0, messageHeight);
-
- const stackLines = [];
- if (event.stackTrace) {
- for (const frame of event.stackTrace.callFrames) {
- // Note we need to add `1` because the values are 0-indexed.
- stackLines.push(
- ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
- frame.lineNumber + 1
- }:${frame.columnNumber + 1})`
- );
- if (stackLines.length >= Error.stackTraceLimit) {
- break;
- }
- }
- }
-
- error.stack = [...messageLines, ...stackLines].join('\n');
- this.emit(PageEvent.PageError, error);
- } else {
- debugError(
- `Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"`
- );
- }
- }
-
- #onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void {
- const frame = this.frame(event.context);
- if (!frame) {
- return;
+ const frames = [this.#frame];
+ for (const frame of frames) {
+ frames.push(...frame.childFrames());
}
- const type = validateDialogType(event.type);
-
- const dialog = new BidiDialog(
- frame.context(),
- type,
- event.message,
- event.defaultValue
- );
- this.emit(PageEvent.Dialog, dialog);
- }
-
- getNavigationResponse(id?: string | null): BidiHTTPResponse | null {
- return this.#networkManager.getNavigationResponse(id);
+ return frames;
}
override isClosed(): boolean {
- return this.#closedDeferred.finished();
+ return this.#frame.detached;
}
override async close(options?: {runBeforeUnload?: boolean}): Promise<void> {
- if (this.#closedDeferred.finished()) {
+ try {
+ await this.#frame.browsingContext.close(options?.runBeforeUnload);
+ } catch {
return;
}
-
- this.#closedDeferred.reject(new TargetCloseError('Page closed!'));
- this.#networkManager.dispose();
-
- await this.#connection.send('browsingContext.close', {
- context: this.mainFrame()._id,
- promptUnload: options?.runBeforeUnload ?? false,
- });
-
- this.emit(PageEvent.Close, undefined);
- this.removeAllListeners();
}
override async reload(
options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> {
- const {
- waitUntil = 'load',
- timeout: ms = this._timeoutSettings.navigationTimeout(),
- } = options;
-
- const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
-
- const result$ = zip(
- from(
- this.#connection.send('browsingContext.reload', {
- context: this.mainFrame()._id,
- wait: readiness,
- })
- ),
- ...(networkIdle !== null
- ? [
- this.waitForNetworkIdle$({
- timeout: ms,
- concurrency: networkIdle === 'networkidle2' ? 2 : 0,
- idleTime: NETWORK_IDLE_TIME,
- }),
- ]
- : [])
- ).pipe(
- map(([{result}]) => {
- return result;
- }),
- raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())),
- rewriteNavigationError(this.url(), ms)
+ const [response] = await Promise.all([
+ this.#frame.waitForNavigation(options),
+ this.#frame.browsingContext.reload(),
+ ]).catch(
+ rewriteNavigationError(
+ this.url(),
+ options.timeout ?? this._timeoutSettings.navigationTimeout()
+ )
);
-
- const result = await firstValueFrom(result$);
- return this.getNavigationResponse(result.navigation);
+ return response;
}
override setDefaultNavigationTimeout(timeout: number): void {
@@ -578,8 +269,19 @@ export class BidiPage extends Page {
}
override async setViewport(viewport: Viewport): Promise<void> {
- if (!this.#browsingContext.supportsCdp()) {
- await this.#emulationManager.emulateViewport(viewport);
+ if (!this.browser().cdpSupported) {
+ await this.#frame.browsingContext.setViewport({
+ viewport:
+ viewport.width && viewport.height
+ ? {
+ width: viewport.width,
+ height: viewport.height,
+ }
+ : null,
+ devicePixelRatio: viewport.deviceScaleFactor
+ ? viewport.deviceScaleFactor
+ : null,
+ });
this.#viewport = viewport;
return;
}
@@ -609,10 +311,9 @@ export class BidiPage extends Page {
preferCSSPageSize,
} = parsePDFOptions(options, 'cm');
const pageRanges = ranges ? ranges.split(', ') : [];
- const {result} = await firstValueFrom(
+ const data = await firstValueFrom(
from(
- this.#connection.send('browsingContext.print', {
- context: this.mainFrame()._id,
+ this.#frame.browsingContext.print({
background,
margin,
orientation: landscape ? 'landscape' : 'portrait',
@@ -627,7 +328,7 @@ export class BidiPage extends Page {
).pipe(raceWith(timeout(ms)))
);
- const buffer = Buffer.from(result.data, 'base64');
+ const buffer = Buffer.from(data, 'base64');
await this._maybeWriteBufferToFile(path, buffer);
@@ -636,19 +337,15 @@ export class BidiPage extends Page {
override async createPDFStream(
options?: PDFOptions | undefined
- ): Promise<Readable> {
+ ): Promise<ReadableStream<Uint8Array>> {
const buffer = await this.pdf(options);
- try {
- const {Readable} = await import('stream');
- return Readable.from(buffer);
- } catch (error) {
- if (error instanceof TypeError) {
- throw new Error(
- 'Can only pass a file path in a Node-like environment.'
- );
- }
- throw error;
- }
+
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue(buffer);
+ controller.close();
+ },
+ });
}
override async _screenshot(
@@ -697,10 +394,7 @@ export class BidiPage extends Page {
}
}
- const {
- result: {data},
- } = await this.#connection.send('browsingContext.captureScreenshot', {
- context: this.mainFrame()._id,
+ const data = await this.#frame.browsingContext.captureScreenshot({
origin: captureBeyondViewport ? 'document' : 'viewport',
format: {
type: `image/${type}`,
@@ -712,19 +406,11 @@ export class BidiPage extends Page {
}
override async createCDPSession(): Promise<CDPSession> {
- const {sessionId} = await this.mainFrame()
- .context()
- .cdpSession.send('Target.attachToTarget', {
- targetId: this.mainFrame()._id,
- flatten: true,
- });
- return new CdpSessionWrapper(this.mainFrame().context(), sessionId);
+ return await this.#frame.createCDPSession();
}
override async bringToFront(): Promise<void> {
- await this.#connection.send('browsingContext.activate', {
- context: this.mainFrame()._id,
- });
+ await this.#frame.browsingContext.activate();
}
override async evaluateOnNewDocument<
@@ -735,20 +421,16 @@ export class BidiPage extends Page {
...args: Params
): Promise<NewDocumentScriptEvaluation> {
const expression = evaluationExpression(pageFunction, ...args);
- const {result} = await this.#connection.send('script.addPreloadScript', {
- functionDeclaration: expression,
- contexts: [this.mainFrame()._id],
- });
+ const script =
+ await this.#frame.browsingContext.addPreloadScript(expression);
- return {identifier: result.script};
+ return {identifier: script};
}
override async removeScriptToEvaluateOnNewDocument(
id: string
): Promise<void> {
- await this.#connection.send('script.removePreloadScript', {
- script: id,
- });
+ await this.#frame.browsingContext.removePreloadScript(id);
}
override async exposeFunction<Args extends unknown[], Ret>(
@@ -774,20 +456,37 @@ export class BidiPage extends Page {
});
}
+ override async cookies(...urls: string[]): Promise<Cookie[]> {
+ const normalizedUrls = (urls.length ? urls : [this.url()]).map(url => {
+ return new URL(url);
+ });
+
+ const cookies = await this.#frame.browsingContext.getCookies();
+ return cookies
+ .map(cookie => {
+ return bidiToPuppeteerCookie(cookie);
+ })
+ .filter(cookie => {
+ return normalizedUrls.some(url => {
+ return testUrlMatchCookie(cookie, url);
+ });
+ });
+ }
+
override isServiceWorkerBypassed(): never {
throw new UnsupportedOperation();
}
- override target(): BiDiPageTarget {
- return this.#target;
+ override target(): never {
+ throw new UnsupportedOperation();
}
override waitForFileChooser(): never {
throw new UnsupportedOperation();
}
- override workers(): never {
- throw new UnsupportedOperation();
+ override workers(): BidiWebWorker[] {
+ return [...this.#workers];
}
override setRequestInterception(): never {
@@ -810,21 +509,98 @@ export class BidiPage extends Page {
throw new UnsupportedOperation();
}
- override cookies(): never {
- throw new UnsupportedOperation();
- }
+ override async setCookie(...cookies: CookieParam[]): Promise<void> {
+ const pageURL = this.url();
+ const pageUrlStartsWithHTTP = pageURL.startsWith('http');
+ for (const cookie of cookies) {
+ let cookieUrl = cookie.url || '';
+ if (!cookieUrl && pageUrlStartsWithHTTP) {
+ cookieUrl = pageURL;
+ }
+ assert(
+ cookieUrl !== 'about:blank',
+ `Blank page can not have cookie "${cookie.name}"`
+ );
+ assert(
+ !String.prototype.startsWith.call(cookieUrl || '', 'data:'),
+ `Data URL page can not have cookie "${cookie.name}"`
+ );
- override setCookie(): never {
- throw new UnsupportedOperation();
+ const normalizedUrl = URL.canParse(cookieUrl)
+ ? new URL(cookieUrl)
+ : undefined;
+
+ const domain = cookie.domain ?? normalizedUrl?.hostname;
+ assert(
+ domain !== undefined,
+ `At least one of the url and domain needs to be specified`
+ );
+
+ const bidiCookie: Bidi.Storage.PartialCookie = {
+ domain: domain,
+ name: cookie.name,
+ value: {
+ type: 'string',
+ value: cookie.value,
+ },
+ ...(cookie.path !== undefined ? {path: cookie.path} : {}),
+ ...(cookie.httpOnly !== undefined ? {httpOnly: cookie.httpOnly} : {}),
+ ...(cookie.secure !== undefined ? {secure: cookie.secure} : {}),
+ ...(cookie.sameSite !== undefined
+ ? {sameSite: convertCookiesSameSiteCdpToBiDi(cookie.sameSite)}
+ : {}),
+ ...(cookie.expires !== undefined ? {expiry: cookie.expires} : {}),
+ // Chrome-specific properties.
+ ...cdpSpecificCookiePropertiesFromPuppeteerToBidi(
+ cookie,
+ 'sameParty',
+ 'sourceScheme',
+ 'priority',
+ 'url'
+ ),
+ };
+
+ if (cookie.partitionKey !== undefined) {
+ await this.browserContext().userContext.setCookie(
+ bidiCookie,
+ cookie.partitionKey
+ );
+ } else {
+ await this.#frame.browsingContext.setCookie(bidiCookie);
+ }
+ }
}
- override deleteCookie(): never {
- throw new UnsupportedOperation();
+ override async deleteCookie(
+ ...cookies: DeleteCookiesRequest[]
+ ): Promise<void> {
+ await Promise.all(
+ cookies.map(async deleteCookieRequest => {
+ const cookieUrl = deleteCookieRequest.url ?? this.url();
+ const normalizedUrl = URL.canParse(cookieUrl)
+ ? new URL(cookieUrl)
+ : undefined;
+
+ const domain = deleteCookieRequest.domain ?? normalizedUrl?.hostname;
+ assert(
+ domain !== undefined,
+ `At least one of the url and domain needs to be specified`
+ );
+
+ const filter = {
+ domain: domain,
+ name: deleteCookieRequest.name,
+ ...(deleteCookieRequest.path !== undefined
+ ? {path: deleteCookieRequest.path}
+ : {}),
+ };
+ await this.#frame.browsingContext.deleteCookie(filter);
+ })
+ );
}
- override removeExposedFunction(): never {
- // TODO: Quick win?
- throw new UnsupportedOperation();
+ override async removeExposedFunction(name: string): Promise<void> {
+ await this.#frame.removeExposedFunction(name);
}
override authenticate(): never {
@@ -848,7 +624,7 @@ export class BidiPage extends Page {
override async goForward(
options: WaitForOptions = {}
): Promise<HTTPResponse | null> {
- return await this.#go(+1, options);
+ return await this.#go(1, options);
}
async #go(
@@ -856,22 +632,19 @@ export class BidiPage extends Page {
options: WaitForOptions
): Promise<HTTPResponse | null> {
try {
- const result = await Promise.all([
+ const [response] = await Promise.all([
this.waitForNavigation(options),
- this.#connection.send('browsingContext.traverseHistory', {
- delta,
- context: this.mainFrame()._id,
- }),
+ this.#frame.browsingContext.traverseHistory(delta),
]);
- return result[0];
- } catch (err) {
+ return response;
+ } catch (error) {
// TODO: waitForNavigation should be cancelled if an error happens.
- if (isErrorLike(err)) {
- if (err.message.includes('no such history entry')) {
+ if (isErrorLike(error)) {
+ if (error.message.includes('no such history entry')) {
return null;
}
}
- throw err;
+ throw error;
}
}
@@ -880,34 +653,137 @@ export class BidiPage extends Page {
}
}
-function isConsoleLogEntry(
- event: Bidi.Log.Entry
-): event is Bidi.Log.ConsoleLogEntry {
- return event.type === 'console';
+function evaluationExpression(fun: Function | string, ...args: unknown[]) {
+ return `() => {${evaluationString(fun, ...args)}}`;
}
-function isJavaScriptLogEntry(
- event: Bidi.Log.Entry
-): event is Bidi.Log.JavascriptLogEntry {
- return event.type === 'javascript';
+/**
+ * Check domains match.
+ * According to cookies spec, this check should match subdomains as well, but CDP
+ * implementation does not do that, so this method matches only the exact domains, not
+ * what is written in the spec:
+ * https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
+ */
+function testUrlMatchCookieHostname(
+ cookie: Cookie,
+ normalizedUrl: URL
+): boolean {
+ const cookieDomain = cookie.domain.toLowerCase();
+ const urlHostname = normalizedUrl.hostname.toLowerCase();
+ return cookieDomain === urlHostname;
}
-function getStackTraceLocations(
- stackTrace?: Bidi.Script.StackTrace
-): ConsoleMessageLocation[] {
- const stackTraceLocations: ConsoleMessageLocation[] = [];
- if (stackTrace) {
- for (const callFrame of stackTrace.callFrames) {
- stackTraceLocations.push({
- url: callFrame.url,
- lineNumber: callFrame.lineNumber,
- columnNumber: callFrame.columnNumber,
- });
+/**
+ * Check paths match.
+ * Spec: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
+ */
+function testUrlMatchCookiePath(cookie: Cookie, normalizedUrl: URL): boolean {
+ const uriPath = normalizedUrl.pathname;
+ const cookiePath = cookie.path;
+
+ if (uriPath === cookiePath) {
+ // The cookie-path and the request-path are identical.
+ return true;
+ }
+ if (uriPath.startsWith(cookiePath)) {
+ // The cookie-path is a prefix of the request-path.
+ if (cookiePath.endsWith('/')) {
+ // The last character of the cookie-path is %x2F ("/").
+ return true;
+ }
+ if (uriPath[cookiePath.length] === '/') {
+ // The first character of the request-path that is not included in the cookie-path
+ // is a %x2F ("/") character.
+ return true;
}
}
- return stackTraceLocations;
+ return false;
}
-function evaluationExpression(fun: Function | string, ...args: unknown[]) {
- return `() => {${evaluationString(fun, ...args)}}`;
+/**
+ * Checks the cookie matches the URL according to the spec:
+ */
+function testUrlMatchCookie(cookie: Cookie, url: URL): boolean {
+ const normalizedUrl = new URL(url);
+ assert(cookie !== undefined);
+ if (!testUrlMatchCookieHostname(cookie, normalizedUrl)) {
+ return false;
+ }
+ return testUrlMatchCookiePath(cookie, normalizedUrl);
+}
+
+function bidiToPuppeteerCookie(bidiCookie: Bidi.Network.Cookie): Cookie {
+ return {
+ name: bidiCookie.name,
+ // Presents binary value as base64 string.
+ value: bidiCookie.value.value,
+ domain: bidiCookie.domain,
+ path: bidiCookie.path,
+ size: bidiCookie.size,
+ httpOnly: bidiCookie.httpOnly,
+ secure: bidiCookie.secure,
+ sameSite: convertCookiesSameSiteBiDiToCdp(bidiCookie.sameSite),
+ expires: bidiCookie.expiry ?? -1,
+ session: bidiCookie.expiry === undefined || bidiCookie.expiry <= 0,
+ // Extending with CDP-specific properties with `goog:` prefix.
+ ...cdpSpecificCookiePropertiesFromBidiToPuppeteer(
+ bidiCookie,
+ 'sameParty',
+ 'sourceScheme',
+ 'partitionKey',
+ 'partitionKeyOpaque',
+ 'priority'
+ ),
+ };
+}
+
+const CDP_SPECIFIC_PREFIX = 'goog:';
+
+/**
+ * Gets CDP-specific properties from the BiDi cookie and returns them as a new object.
+ */
+function cdpSpecificCookiePropertiesFromBidiToPuppeteer(
+ bidiCookie: Bidi.Network.Cookie,
+ ...propertyNames: Array<keyof Cookie>
+): Partial<Cookie> {
+ const result: Partial<Cookie> = {};
+ for (const property of propertyNames) {
+ if (bidiCookie[CDP_SPECIFIC_PREFIX + property] !== undefined) {
+ result[property] = bidiCookie[CDP_SPECIFIC_PREFIX + property];
+ }
+ }
+ return result;
+}
+
+/**
+ * Gets CDP-specific properties from the cookie, adds CDP-specific prefixes and returns
+ * them as a new object which can be used in BiDi.
+ */
+function cdpSpecificCookiePropertiesFromPuppeteerToBidi(
+ cookieParam: CookieParam,
+ ...propertyNames: Array<keyof CookieParam>
+): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const property of propertyNames) {
+ if (cookieParam[property] !== undefined) {
+ result[CDP_SPECIFIC_PREFIX + property] = cookieParam[property];
+ }
+ }
+ return result;
+}
+
+function convertCookiesSameSiteBiDiToCdp(
+ sameSite: Bidi.Network.SameSite | undefined
+): CookieSameSite {
+ return sameSite === 'strict' ? 'Strict' : sameSite === 'lax' ? 'Lax' : 'None';
+}
+
+function convertCookiesSameSiteCdpToBiDi(
+ sameSite: CookieSameSite | undefined
+): Bidi.Network.SameSite {
+ return sameSite === 'Strict'
+ ? Bidi.Network.SameSite.Strict
+ : sameSite === 'Lax'
+ ? Bidi.Network.SameSite.Lax
+ : Bidi.Network.SameSite.None;
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts
index 84f13bc703..1027941e2f 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts
@@ -1,80 +1,63 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import {EventEmitter, type EventType} from '../common/EventEmitter.js';
+import type {JSHandle} from '../api/JSHandle.js';
+import {Realm} from '../api/Realm.js';
+import {ARIAQueryHandler} from '../cdp/AriaQueryHandler.js';
+import {LazyArg} from '../common/LazyArg.js';
import {scriptInjector} from '../common/ScriptInjector.js';
+import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {
- PuppeteerURL,
- SOURCE_URL_REGEX,
+ debugError,
getSourcePuppeteerURLIfAvailable,
getSourceUrlComment,
isString,
+ PuppeteerURL,
+ SOURCE_URL_REGEX,
} from '../common/util.js';
import type PuppeteerUtil from '../injected/injected.js';
-import {disposeSymbol} from '../util/disposable.js';
+import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {stringifyFunction} from '../util/Function.js';
-import type {BidiConnection} from './Connection.js';
+import type {
+ Realm as BidiRealmCore,
+ DedicatedWorkerRealm,
+ SharedWorkerRealm,
+} from './core/Realm.js';
+import type {WindowRealm} from './core/Realm.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiElementHandle} from './ElementHandle.js';
+import {ExposeableFunction} from './ExposedFunction.js';
+import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
-import type {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './util.js';
+import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
-export class BidiRealm extends EventEmitter<Record<EventType, any>> {
- readonly connection: BidiConnection;
-
- #id!: string;
- #sandbox!: Sandbox;
+export abstract class BidiRealm extends Realm {
+ readonly realm: BidiRealmCore;
- constructor(connection: BidiConnection) {
- super();
- this.connection = connection;
+ constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
+ super(timeoutSettings);
+ this.realm = realm;
}
- get target(): Bidi.Script.Target {
- return {
- context: this.#sandbox.environment._id,
- sandbox: this.#sandbox.name,
- };
- }
-
- handleRealmDestroyed = async (
- params: Bidi.Script.RealmDestroyed['params']
- ): Promise<void> => {
- if (params.realm === this.#id) {
- // Note: The Realm is destroyed, so in theory the handle should be as
- // well.
+ protected initialize(): void {
+ this.realm.on('destroyed', ({reason}) => {
+ this.taskManager.terminateAll(new Error(reason));
+ });
+ this.realm.on('updated', () => {
this.internalPuppeteerUtil = undefined;
- this.#sandbox.environment.clearDocumentHandle();
- }
- };
-
- handleRealmCreated = (params: Bidi.Script.RealmCreated['params']): void => {
- if (
- params.type === 'window' &&
- params.context === this.#sandbox.environment._id &&
- params.sandbox === this.#sandbox.name
- ) {
- this.#id = params.realm;
- void this.#sandbox.taskManager.rerunAll();
- }
- };
-
- setSandbox(sandbox: Sandbox): void {
- this.#sandbox = sandbox;
- this.connection.on(
- Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
- this.handleRealmCreated
- );
- this.connection.on(
- Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
- this.handleRealmDestroyed
- );
+ void this.taskManager.rerunAll();
+ });
}
protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>;
@@ -95,7 +78,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>;
}
- async evaluateHandle<
+ override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
@@ -105,7 +88,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return await this.#evaluate(false, pageFunction, ...args);
}
- async evaluate<
+ override async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
@@ -144,8 +127,6 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
PuppeteerURL.INTERNAL_URL
);
- const sandbox = this.#sandbox;
-
let responsePromise;
const resultOwnership = returnByValue
? Bidi.Script.ResultOwnership.None
@@ -161,11 +142,8 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
? pageFunction
: `${pageFunction}\n${sourceUrlComment}\n`;
- responsePromise = this.connection.send('script.evaluate', {
- expression,
- target: this.target,
+ responsePromise = this.realm.evaluate(expression, true, {
resultOwnership,
- awaitPromise: true,
userActivation: true,
serializationOptions,
});
@@ -174,24 +152,25 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`;
- responsePromise = this.connection.send('script.callFunction', {
+ responsePromise = this.realm.callFunction(
functionDeclaration,
- arguments: args.length
- ? await Promise.all(
- args.map(arg => {
- return BidiSerializer.serialize(sandbox, arg);
- })
- )
- : [],
- target: this.target,
- resultOwnership,
- awaitPromise: true,
- userActivation: true,
- serializationOptions,
- });
+ /* awaitPromise= */ true,
+ {
+ arguments: args.length
+ ? await Promise.all(
+ args.map(arg => {
+ return this.serialize(arg);
+ })
+ )
+ : [],
+ resultOwnership,
+ userActivation: true,
+ serializationOptions,
+ }
+ );
}
- const {result} = await responsePromise;
+ const result = await responsePromise;
if ('type' in result && result.type === 'exception') {
throw createEvaluationError(result.exceptionDetails);
@@ -199,30 +178,211 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return returnByValue
? BidiDeserializer.deserialize(result.result)
- : createBidiHandle(sandbox, result.result);
+ : this.createHandle(result.result);
}
- [disposeSymbol](): void {
- this.connection.off(
- Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
- this.handleRealmCreated
- );
- this.connection.off(
- Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
- this.handleRealmDestroyed
+ createHandle(
+ result: Bidi.Script.RemoteValue
+ ): BidiJSHandle<unknown> | BidiElementHandle<Node> {
+ if (
+ (result.type === 'node' || result.type === 'window') &&
+ this instanceof BidiFrameRealm
+ ) {
+ return BidiElementHandle.from(result, this);
+ }
+ return BidiJSHandle.from(result, this);
+ }
+
+ async serialize(arg: unknown): Promise<Bidi.Script.LocalValue> {
+ if (arg instanceof LazyArg) {
+ arg = await arg.get(this);
+ }
+
+ if (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle) {
+ if (arg.realm !== this) {
+ if (
+ !(arg.realm instanceof BidiFrameRealm) ||
+ !(this instanceof BidiFrameRealm)
+ ) {
+ throw new Error(
+ "Trying to evaluate JSHandle from different global types. Usually this means you're using a handle from a worker in a page or vice versa."
+ );
+ }
+ if (arg.realm.environment !== this.environment) {
+ throw new Error(
+ "Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page."
+ );
+ }
+ }
+ if (arg.disposed) {
+ throw new Error('JSHandle is disposed!');
+ }
+ return arg.remoteValue() as Bidi.Script.RemoteReference;
+ }
+
+ return BidiSerializer.serialize(arg);
+ }
+
+ async destroyHandles(handles: Array<BidiJSHandle<unknown>>): Promise<void> {
+ const handleIds = handles
+ .map(({id}) => {
+ return id;
+ })
+ .filter((id): id is string => {
+ return id !== undefined;
+ });
+
+ if (handleIds.length === 0) {
+ return;
+ }
+
+ await this.realm.disown(handleIds).catch(error => {
+ // Exceptions might happen in case of a page been navigated or closed.
+ // Swallow these since they are harmless and we don't leak anything in this case.
+ debugError(error);
+ });
+ }
+
+ override async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
+ return (await this.evaluateHandle(node => {
+ return node;
+ }, handle)) as unknown as T;
+ }
+
+ override async transferHandle<T extends JSHandle<Node>>(
+ handle: T
+ ): Promise<T> {
+ if (handle.realm === this) {
+ return handle;
+ }
+ const transferredHandle = this.adoptHandle(handle);
+ await handle.dispose();
+ return await transferredHandle;
+ }
+}
+
+/**
+ * @internal
+ */
+export class BidiFrameRealm extends BidiRealm {
+ static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm {
+ const frameRealm = new BidiFrameRealm(realm, frame);
+ frameRealm.#initialize();
+ return frameRealm;
+ }
+ declare readonly realm: WindowRealm;
+
+ readonly #frame: BidiFrame;
+
+ private constructor(realm: WindowRealm, frame: BidiFrame) {
+ super(realm, frame.timeoutSettings);
+ this.#frame = frame;
+ }
+
+ #initialize() {
+ super.initialize();
+
+ // This should run first.
+ this.realm.on('updated', () => {
+ this.environment.clearDocumentHandle();
+ this.#bindingsInstalled = false;
+ });
+ }
+
+ #bindingsInstalled = false;
+ override get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerUtil>> {
+ let promise = Promise.resolve() as Promise<unknown>;
+ if (!this.#bindingsInstalled) {
+ promise = Promise.all([
+ ExposeableFunction.from(
+ this.environment as BidiFrame,
+ '__ariaQuerySelector',
+ ARIAQueryHandler.queryOne,
+ !!this.sandbox
+ ),
+ ExposeableFunction.from(
+ this.environment as BidiFrame,
+ '__ariaQuerySelectorAll',
+ async (
+ element: BidiElementHandle<Node>,
+ selector: string
+ ): Promise<JSHandle<Node[]>> => {
+ const results = ARIAQueryHandler.queryAll(element, selector);
+ return await element.realm.evaluateHandle(
+ (...elements) => {
+ return elements;
+ },
+ ...(await AsyncIterableUtil.collect(results))
+ );
+ },
+ !!this.sandbox
+ ),
+ ]);
+ this.#bindingsInstalled = true;
+ }
+ return promise.then(() => {
+ return super.puppeteerUtil;
+ });
+ }
+
+ get sandbox(): string | undefined {
+ return this.realm.sandbox;
+ }
+
+ override get environment(): BidiFrame {
+ return this.#frame;
+ }
+
+ override async adoptBackendNode(
+ backendNodeId?: number | undefined
+ ): Promise<JSHandle<Node>> {
+ const {object} = await this.#frame.client.send('DOM.resolveNode', {
+ backendNodeId,
+ executionContextId: await this.realm.resolveExecutionContextId(),
+ });
+ using handle = BidiElementHandle.from(
+ {
+ handle: object.objectId,
+ type: 'node',
+ },
+ this
);
+ // We need the sharedId, so we perform the following to obtain it.
+ return await handle.evaluateHandle(element => {
+ return element;
+ });
}
}
/**
* @internal
*/
-export function createBidiHandle(
- sandbox: Sandbox,
- result: Bidi.Script.RemoteValue
-): BidiJSHandle<unknown> | BidiElementHandle<Node> {
- if (result.type === 'node' || result.type === 'window') {
- return new BidiElementHandle(sandbox, result);
- }
- return new BidiJSHandle(sandbox, result);
+export class BidiWorkerRealm extends BidiRealm {
+ static from(
+ realm: DedicatedWorkerRealm | SharedWorkerRealm,
+ worker: BidiWebWorker
+ ): BidiWorkerRealm {
+ const workerRealm = new BidiWorkerRealm(realm, worker);
+ workerRealm.initialize();
+ return workerRealm;
+ }
+ declare readonly realm: DedicatedWorkerRealm | SharedWorkerRealm;
+
+ readonly #worker: BidiWebWorker;
+
+ private constructor(
+ realm: DedicatedWorkerRealm | SharedWorkerRealm,
+ frame: BidiWebWorker
+ ) {
+ super(realm, frame.timeoutSettings);
+ this.#worker = frame;
+ }
+
+ override get environment(): BidiWebWorker {
+ return this.#worker;
+ }
+
+ override async adoptBackendNode(): Promise<JSHandle<Node>> {
+ throw new Error('Cannot adopt DOM nodes into a worker.');
+ }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts
deleted file mode 100644
index 4411b3dbcd..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import type {JSHandle} from '../api/JSHandle.js';
-import {Realm} from '../api/Realm.js';
-import type {TimeoutSettings} from '../common/TimeoutSettings.js';
-import type {EvaluateFunc, HandleFor} from '../common/types.js';
-import {withSourcePuppeteerURLIfNone} from '../common/util.js';
-
-import type {BrowsingContext} from './BrowsingContext.js';
-import {BidiElementHandle} from './ElementHandle.js';
-import type {BidiFrame} from './Frame.js';
-import type {BidiRealm as BidiRealm} from './Realm.js';
-/**
- * A unique key for {@link SandboxChart} to denote the default world.
- * Realms are automatically created in the default sandbox.
- *
- * @internal
- */
-export const MAIN_SANDBOX = Symbol('mainSandbox');
-/**
- * A unique key for {@link SandboxChart} to denote the puppeteer sandbox.
- * This world contains all puppeteer-internal bindings/code.
- *
- * @internal
- */
-export const PUPPETEER_SANDBOX = Symbol('puppeteerSandbox');
-
-/**
- * @internal
- */
-export interface SandboxChart {
- [key: string]: Sandbox;
- [MAIN_SANDBOX]: Sandbox;
- [PUPPETEER_SANDBOX]: Sandbox;
-}
-
-/**
- * @internal
- */
-export class Sandbox extends Realm {
- readonly name: string | undefined;
- readonly realm: BidiRealm;
- #frame: BidiFrame;
-
- constructor(
- name: string | undefined,
- frame: BidiFrame,
- // TODO: We should split the Realm and BrowsingContext
- realm: BidiRealm | BrowsingContext,
- timeoutSettings: TimeoutSettings
- ) {
- super(timeoutSettings);
- this.name = name;
- this.realm = realm;
- this.#frame = frame;
- this.realm.setSandbox(this);
- }
-
- override get environment(): BidiFrame {
- return this.#frame;
- }
-
- async evaluateHandle<
- Params extends unknown[],
- Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
- >(
- pageFunction: Func | string,
- ...args: Params
- ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
- pageFunction = withSourcePuppeteerURLIfNone(
- this.evaluateHandle.name,
- pageFunction
- );
- return await this.realm.evaluateHandle(pageFunction, ...args);
- }
-
- async evaluate<
- Params extends unknown[],
- Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
- >(
- pageFunction: Func | string,
- ...args: Params
- ): Promise<Awaited<ReturnType<Func>>> {
- pageFunction = withSourcePuppeteerURLIfNone(
- this.evaluate.name,
- pageFunction
- );
- return await this.realm.evaluate(pageFunction, ...args);
- }
-
- async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
- return (await this.evaluateHandle(node => {
- return node;
- }, handle)) as unknown as T;
- }
-
- async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
- if (handle.realm === this) {
- return handle;
- }
- const transferredHandle = await this.evaluateHandle(node => {
- return node;
- }, handle);
- await handle.dispose();
- return transferredHandle as unknown as T;
- }
-
- override async adoptBackendNode(
- backendNodeId?: number
- ): Promise<JSHandle<Node>> {
- const {object} = await this.environment.client.send('DOM.resolveNode', {
- backendNodeId: backendNodeId,
- });
- return new BidiElementHandle(this, {
- handle: object.objectId,
- type: 'node',
- });
- }
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts
index c147ec9281..523380782b 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts
@@ -6,13 +6,8 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import {LazyArg} from '../common/LazyArg.js';
import {isDate, isPlainObject, isRegExp} from '../common/util.js';
-import {BidiElementHandle} from './ElementHandle.js';
-import {BidiJSHandle} from './JSHandle.js';
-import type {Sandbox} from './Sandbox.js';
-
/**
* @internal
*/
@@ -22,7 +17,39 @@ class UnserializableError extends Error {}
* @internal
*/
export class BidiSerializer {
- static serializeNumber(arg: number): Bidi.Script.LocalValue {
+ static serialize(arg: unknown): Bidi.Script.LocalValue {
+ switch (typeof arg) {
+ case 'symbol':
+ case 'function':
+ throw new UnserializableError(`Unable to serializable ${typeof arg}`);
+ case 'object':
+ return this.#serializeObject(arg);
+
+ case 'undefined':
+ return {
+ type: 'undefined',
+ };
+ case 'number':
+ return this.#serializeNumber(arg);
+ case 'bigint':
+ return {
+ type: 'bigint',
+ value: arg.toString(),
+ };
+ case 'string':
+ return {
+ type: 'string',
+ value: arg,
+ };
+ case 'boolean':
+ return {
+ type: 'boolean',
+ value: arg,
+ };
+ }
+ }
+
+ static #serializeNumber(arg: number): Bidi.Script.LocalValue {
let value: Bidi.Script.SpecialNumber | number;
if (Object.is(arg, -0)) {
value = '-0';
@@ -41,14 +68,14 @@ export class BidiSerializer {
};
}
- static serializeObject(arg: object | null): Bidi.Script.LocalValue {
+ static #serializeObject(arg: object | null): Bidi.Script.LocalValue {
if (arg === null) {
return {
type: 'null',
};
} else if (Array.isArray(arg)) {
const parsedArray = arg.map(subArg => {
- return BidiSerializer.serializeRemoteValue(subArg);
+ return this.serialize(subArg);
});
return {
@@ -70,10 +97,7 @@ export class BidiSerializer {
const parsedObject: Bidi.Script.MappingLocalValue = [];
for (const key in arg) {
- parsedObject.push([
- BidiSerializer.serializeRemoteValue(key),
- BidiSerializer.serializeRemoteValue(arg[key]),
- ]);
+ parsedObject.push([this.serialize(key), this.serialize(arg[key])]);
}
return {
@@ -99,66 +123,4 @@ export class BidiSerializer {
'Custom object sterilization not possible. Use plain objects instead.'
);
}
-
- static serializeRemoteValue(arg: unknown): Bidi.Script.LocalValue {
- switch (typeof arg) {
- case 'symbol':
- case 'function':
- throw new UnserializableError(`Unable to serializable ${typeof arg}`);
- case 'object':
- return BidiSerializer.serializeObject(arg);
-
- case 'undefined':
- return {
- type: 'undefined',
- };
- case 'number':
- return BidiSerializer.serializeNumber(arg);
- case 'bigint':
- return {
- type: 'bigint',
- value: arg.toString(),
- };
- case 'string':
- return {
- type: 'string',
- value: arg,
- };
- case 'boolean':
- return {
- type: 'boolean',
- value: arg,
- };
- }
- }
-
- static async serialize(
- sandbox: Sandbox,
- arg: unknown
- ): Promise<Bidi.Script.LocalValue> {
- if (arg instanceof LazyArg) {
- arg = await arg.get(sandbox.realm);
- }
- // eslint-disable-next-line rulesdir/use-using -- We want this to continue living.
- const objectHandle =
- arg && (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle)
- ? arg
- : null;
- if (objectHandle) {
- if (
- objectHandle.realm.environment.context() !==
- sandbox.environment.context()
- ) {
- throw new Error(
- 'JSHandles can be evaluated only in the context they were created!'
- );
- }
- if (objectHandle.disposed) {
- throw new Error('JSHandle is disposed!');
- }
- return objectHandle.remoteValue() as Bidi.Script.RemoteReference;
- }
-
- return BidiSerializer.serializeRemoteValue(arg);
- }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts
index fb01c34638..b9d78538aa 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts
@@ -4,48 +4,46 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {CDPSession} from '../api/CDPSession.js';
-import type {Page} from '../api/Page.js';
import {Target, TargetType} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
+import type {CDPSession} from '../puppeteer-core.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
-import {type BrowsingContext, CdpSessionWrapper} from './BrowsingContext.js';
+import type {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js';
+import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
-export abstract class BidiTarget extends Target {
- protected _browserContext: BidiBrowserContext;
+export class BidiBrowserTarget extends Target {
+ #browser: BidiBrowser;
- constructor(browserContext: BidiBrowserContext) {
+ constructor(browser: BidiBrowser) {
super();
- this._browserContext = browserContext;
+ this.#browser = browser;
}
- _setBrowserContext(browserContext: BidiBrowserContext): void {
- this._browserContext = browserContext;
+ override asPage(): Promise<BidiPage> {
+ throw new UnsupportedOperation();
}
-
- override asPage(): Promise<Page> {
+ override url(): string {
+ return '';
+ }
+ override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
-
+ override type(): TargetType {
+ return TargetType.BROWSER;
+ }
override browser(): BidiBrowser {
- return this._browserContext.browser();
+ return this.#browser;
}
-
override browserContext(): BidiBrowserContext {
- return this._browserContext;
- }
-
- override opener(): never {
- throw new UnsupportedOperation();
+ return this.#browser.defaultBrowserContext();
}
-
- override createCDPSession(): Promise<CDPSession> {
+ override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
@@ -53,39 +51,39 @@ export abstract class BidiTarget extends Target {
/**
* @internal
*/
-export class BiDiBrowserTarget extends Target {
- #browser: BidiBrowser;
+export class BidiPageTarget extends Target {
+ #page: BidiPage;
- constructor(browser: BidiBrowser) {
+ constructor(page: BidiPage) {
super();
- this.#browser = browser;
+ this.#page = page;
}
+ override async page(): Promise<BidiPage> {
+ return this.#page;
+ }
+ override async asPage(): Promise<BidiPage> {
+ return BidiPage.from(
+ this.browserContext(),
+ this.#page.mainFrame().browsingContext
+ );
+ }
override url(): string {
- return '';
+ return this.#page.url();
}
-
- override type(): TargetType {
- return TargetType.BROWSER;
+ override createCDPSession(): Promise<CDPSession> {
+ return this.#page.createCDPSession();
}
-
- override asPage(): Promise<Page> {
- throw new UnsupportedOperation();
+ override type(): TargetType {
+ return TargetType.PAGE;
}
-
override browser(): BidiBrowser {
- return this.#browser;
+ return this.browserContext().browser();
}
-
override browserContext(): BidiBrowserContext {
- return this.#browser.defaultBrowserContext();
- }
-
- override opener(): never {
- throw new UnsupportedOperation();
+ return this.#page.browserContext();
}
-
- override createCDPSession(): Promise<CDPSession> {
+ override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
@@ -93,59 +91,80 @@ export class BiDiBrowserTarget extends Target {
/**
* @internal
*/
-export class BiDiBrowsingContextTarget extends BidiTarget {
- protected _browsingContext: BrowsingContext;
+export class BidiFrameTarget extends Target {
+ #frame: BidiFrame;
+ #page: BidiPage | undefined;
- constructor(
- browserContext: BidiBrowserContext,
- browsingContext: BrowsingContext
- ) {
- super(browserContext);
-
- this._browsingContext = browsingContext;
+ constructor(frame: BidiFrame) {
+ super();
+ this.#frame = frame;
}
+ override async page(): Promise<BidiPage> {
+ if (this.#page === undefined) {
+ this.#page = BidiPage.from(
+ this.browserContext(),
+ this.#frame.browsingContext
+ );
+ }
+ return this.#page;
+ }
+ override async asPage(): Promise<BidiPage> {
+ return BidiPage.from(this.browserContext(), this.#frame.browsingContext);
+ }
override url(): string {
- return this._browsingContext.url;
+ return this.#frame.url();
}
-
- override async createCDPSession(): Promise<CDPSession> {
- const {sessionId} = await this._browsingContext.cdpSession.send(
- 'Target.attachToTarget',
- {
- targetId: this._browsingContext.id,
- flatten: true,
- }
- );
- return new CdpSessionWrapper(this._browsingContext, sessionId);
+ override createCDPSession(): Promise<CDPSession> {
+ return this.#frame.createCDPSession();
}
-
override type(): TargetType {
return TargetType.PAGE;
}
+ override browser(): BidiBrowser {
+ return this.browserContext().browser();
+ }
+ override browserContext(): BidiBrowserContext {
+ return this.#frame.page().browserContext();
+ }
+ override opener(): Target | undefined {
+ throw new UnsupportedOperation();
+ }
}
/**
* @internal
*/
-export class BiDiPageTarget extends BiDiBrowsingContextTarget {
- #page: BidiPage;
-
- constructor(
- browserContext: BidiBrowserContext,
- browsingContext: BrowsingContext
- ) {
- super(browserContext, browsingContext);
+export class BidiWorkerTarget extends Target {
+ #worker: BidiWebWorker;
- this.#page = new BidiPage(browsingContext, browserContext, this);
+ constructor(worker: BidiWebWorker) {
+ super();
+ this.#worker = worker;
}
override async page(): Promise<BidiPage> {
- return this.#page;
+ throw new UnsupportedOperation();
}
-
- override _setBrowserContext(browserContext: BidiBrowserContext): void {
- super._setBrowserContext(browserContext);
- this.#page._setBrowserContext(browserContext);
+ override async asPage(): Promise<BidiPage> {
+ throw new UnsupportedOperation();
+ }
+ override url(): string {
+ return this.#worker.url();
+ }
+ override createCDPSession(): Promise<CDPSession> {
+ throw new UnsupportedOperation();
+ }
+ override type(): TargetType {
+ return TargetType.OTHER;
+ }
+ override browser(): BidiBrowser {
+ return this.browserContext().browser();
+ }
+ override browserContext(): BidiBrowserContext {
+ return this.#worker.frame.page().browserContext();
+ }
+ override opener(): Target | undefined {
+ throw new UnsupportedOperation();
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/WebWorker.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/WebWorker.ts
new file mode 100644
index 0000000000..a8b0e28846
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/WebWorker.ts
@@ -0,0 +1,48 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {WebWorker} from '../api/WebWorker.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import type {CDPSession} from '../puppeteer-core.js';
+
+import type {DedicatedWorkerRealm, SharedWorkerRealm} from './core/Realm.js';
+import type {BidiFrame} from './Frame.js';
+import {BidiWorkerRealm} from './Realm.js';
+
+/**
+ * @internal
+ */
+export class BidiWebWorker extends WebWorker {
+ static from(
+ frame: BidiFrame,
+ realm: DedicatedWorkerRealm | SharedWorkerRealm
+ ): BidiWebWorker {
+ const worker = new BidiWebWorker(frame, realm);
+ return worker;
+ }
+
+ readonly #frame: BidiFrame;
+ readonly #realm: BidiWorkerRealm;
+ private constructor(
+ frame: BidiFrame,
+ realm: DedicatedWorkerRealm | SharedWorkerRealm
+ ) {
+ super(realm.origin);
+ this.#frame = frame;
+ this.#realm = BidiWorkerRealm.from(realm, this);
+ }
+
+ get frame(): BidiFrame {
+ return this.#frame;
+ }
+
+ mainRealm(): BidiWorkerRealm {
+ return this.#realm;
+ }
+
+ get client(): CDPSession {
+ throw new UnsupportedOperation();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts
index 373d6d999c..4279ba96fd 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts
@@ -7,7 +7,6 @@
export * from './BidiOverCdp.js';
export * from './Browser.js';
export * from './BrowserContext.js';
-export * from './BrowsingContext.js';
export * from './Connection.js';
export * from './ElementHandle.js';
export * from './Frame.js';
@@ -15,8 +14,5 @@ export * from './HTTPRequest.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export * from './JSHandle.js';
-export * from './NetworkManager.js';
export * from './Page.js';
export * from './Realm.js';
-export * from './Sandbox.js';
-export * from './Target.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts
index 7c4a8ed01c..efeabc3a59 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts
@@ -11,7 +11,7 @@ import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {BrowsingContext} from './BrowsingContext.js';
-import type {SharedWorkerRealm} from './Realm.js';
+import {SharedWorkerRealm} from './Realm.js';
import type {Session} from './Session.js';
import {UserContext} from './UserContext.js';
@@ -57,6 +57,7 @@ export class Browser extends EventEmitter<{
readonly #disposables = new DisposableStack();
readonly #userContexts = new Map<string, UserContext>();
readonly session: Session;
+ readonly #sharedWorkers = new Map<string, SharedWorkerRealm>();
// keep-sorted end
private constructor(session: Session) {
@@ -64,11 +65,6 @@ export class Browser extends EventEmitter<{
// keep-sorted start
this.session = session;
// keep-sorted end
-
- this.#userContexts.set(
- UserContext.DEFAULT,
- UserContext.create(this, UserContext.DEFAULT)
- );
}
async #initialize() {
@@ -80,14 +76,29 @@ export class Browser extends EventEmitter<{
});
sessionEmitter.on('script.realmCreated', info => {
- if (info.type === 'shared-worker') {
- // TODO: Create a SharedWorkerRealm.
+ if (info.type !== 'shared-worker') {
+ return;
}
+ this.#sharedWorkers.set(
+ info.realm,
+ SharedWorkerRealm.from(this, info.realm, info.origin)
+ );
});
+ await this.#syncUserContexts();
await this.#syncBrowsingContexts();
}
+ async #syncUserContexts() {
+ const {
+ result: {userContexts},
+ } = await this.session.send('browser.getUserContexts', {});
+
+ for (const context of userContexts) {
+ this.#createUserContext(context.userContext);
+ }
+ }
+
async #syncBrowsingContexts() {
// In case contexts are created or destroyed during `getTree`, we use this
// set to detect them.
@@ -99,16 +110,13 @@ export class Browser extends EventEmitter<{
sessionEmitter.on('browsingContext.contextCreated', info => {
contextIds.add(info.context);
});
- sessionEmitter.on('browsingContext.contextDestroyed', info => {
- contextIds.delete(info.context);
- });
const {result} = await this.session.send('browsingContext.getTree', {});
contexts = result.contexts;
}
// Simulating events so contexts are created naturally.
for (const info of contexts) {
- if (contextIds.has(info.context)) {
+ if (!contextIds.has(info.context)) {
this.session.emit('browsingContext.contextCreated', info);
}
if (info.children) {
@@ -117,6 +125,22 @@ export class Browser extends EventEmitter<{
}
}
+ #createUserContext(id: string) {
+ const userContext = UserContext.create(this, id);
+ this.#userContexts.set(userContext.id, userContext);
+
+ const userContextEmitter = this.#disposables.use(
+ new EventEmitter(userContext)
+ );
+ userContextEmitter.once('closed', () => {
+ userContextEmitter.removeAllListeners();
+
+ this.#userContexts.delete(userContext.id);
+ });
+
+ return userContext;
+ }
+
// keep-sorted start block=yes
get closed(): boolean {
return this.#closed;
@@ -185,30 +209,15 @@ export class Browser extends EventEmitter<{
});
}
- static userContextId = 0;
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async createUserContext(): Promise<UserContext> {
- // TODO: implement incognito context https://github.com/w3c/webdriver-bidi/issues/289.
- // TODO: Call `createUserContext` once available.
- // Generating a monotonically increasing context id.
- const context = `${++Browser.userContextId}`;
-
- const userContext = UserContext.create(this, context);
- this.#userContexts.set(userContext.id, userContext);
-
- const userContextEmitter = this.#disposables.use(
- new EventEmitter(userContext)
- );
- userContextEmitter.once('closed', () => {
- userContextEmitter.removeAllListeners();
-
- this.#userContexts.delete(context);
- });
-
- return userContext;
+ const {
+ result: {userContext: context},
+ } = await this.session.send('browser.createUserContext', {});
+ return this.#createUserContext(context);
}
[disposeSymbol](): void {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts
index 9bec2a506c..07309576a3 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts
@@ -12,6 +12,7 @@ import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {AddPreloadScriptOptions} from './Browser.js';
import {Navigation} from './Navigation.js';
+import type {DedicatedWorkerRealm} from './Realm.js';
import {WindowRealm} from './Realm.js';
import {Request} from './Request.js';
import type {UserContext} from './UserContext.js';
@@ -60,6 +61,14 @@ export type SetViewportOptions = Omit<
/**
* @internal
*/
+export type GetCookiesOptions = Omit<
+ Bidi.Storage.GetCookiesParameters,
+ 'partition'
+>;
+
+/**
+ * @internal
+ */
export class BrowsingContext extends EventEmitter<{
/** Emitted when this context is closed. */
closed: {
@@ -95,6 +104,11 @@ export class BrowsingContext extends EventEmitter<{
DOMContentLoaded: void;
/** Emitted whenever the frame emits `load` */
load: void;
+ /** Emitted whenever a dedicated worker is created */
+ worker: {
+ /** The realm for the new dedicated worker */
+ realm: DedicatedWorkerRealm;
+ };
}> {
static from(
userContext: UserContext,
@@ -135,7 +149,7 @@ export class BrowsingContext extends EventEmitter<{
this.userContext = context;
// keep-sorted end
- this.defaultRealm = WindowRealm.from(this);
+ this.defaultRealm = this.#createWindowRealm();
}
#initialize() {
@@ -202,7 +216,16 @@ export class BrowsingContext extends EventEmitter<{
}
this.#url = info.url;
- this.#requests.clear();
+ for (const [id, request] of this.#requests) {
+ if (request.disposed) {
+ this.#requests.delete(id);
+ }
+ }
+ // If the navigation hasn't finished, then this is nested navigation. The
+ // current navigation will handle this.
+ if (this.#navigation !== undefined && !this.#navigation.disposed) {
+ return;
+ }
// Note the navigation ID is null for this event.
this.#navigation = Navigation.from(this);
@@ -224,7 +247,8 @@ export class BrowsingContext extends EventEmitter<{
if (event.context !== this.id) {
return;
}
- if (this.#requests.has(event.request.request)) {
+ if (event.redirectCount !== 0) {
+ // Means the request is a redirect. This is handled in Request.
return;
}
@@ -265,7 +289,12 @@ export class BrowsingContext extends EventEmitter<{
return this.closed;
}
get realms(): Iterable<WindowRealm> {
- return this.#realms.values();
+ // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required
+ const self = this;
+ return (function* () {
+ yield self.defaultRealm;
+ yield* self.#realms.values();
+ })();
}
get top(): BrowsingContext {
let context = this as BrowsingContext;
@@ -279,6 +308,14 @@ export class BrowsingContext extends EventEmitter<{
}
// keep-sorted end
+ #createWindowRealm(sandbox?: string) {
+ const realm = WindowRealm.from(this, sandbox);
+ realm.on('worker', realm => {
+ this.emit('worker', {realm});
+ });
+ return realm;
+ }
+
@inertIfDisposed
private dispose(reason?: string): void {
this.#reason = reason;
@@ -345,33 +382,23 @@ export class BrowsingContext extends EventEmitter<{
async navigate(
url: string,
wait?: Bidi.BrowsingContext.ReadinessState
- ): Promise<Navigation> {
+ ): Promise<void> {
await this.#session.send('browsingContext.navigate', {
context: this.id,
url,
wait,
});
- return await new Promise(resolve => {
- this.once('navigation', ({navigation}) => {
- resolve(navigation);
- });
- });
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
- async reload(options: ReloadOptions = {}): Promise<Navigation> {
+ async reload(options: ReloadOptions = {}): Promise<void> {
await this.#session.send('browsingContext.reload', {
context: this.id,
...options,
});
- return await new Promise(resolve => {
- this.once('navigation', ({navigation}) => {
- resolve(navigation);
- });
- });
}
@throwIfDisposed<BrowsingContext>(context => {
@@ -436,7 +463,7 @@ export class BrowsingContext extends EventEmitter<{
return context.#reason!;
})
createWindowRealm(sandbox: string): WindowRealm {
- return WindowRealm.from(this, sandbox);
+ return this.#createWindowRealm(sandbox);
}
@throwIfDisposed<BrowsingContext>(context => {
@@ -464,6 +491,54 @@ export class BrowsingContext extends EventEmitter<{
await this.userContext.browser.removePreloadScript(script);
}
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async getCookies(
+ options: GetCookiesOptions = {}
+ ): Promise<Bidi.Network.Cookie[]> {
+ const {
+ result: {cookies},
+ } = await this.#session.send('storage.getCookies', {
+ ...options,
+ partition: {
+ type: 'context',
+ context: this.id,
+ },
+ });
+ return cookies;
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async setCookie(cookie: Bidi.Storage.PartialCookie): Promise<void> {
+ await this.#session.send('storage.setCookie', {
+ cookie,
+ partition: {
+ type: 'context',
+ context: this.id,
+ },
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async setFiles(
+ element: Bidi.Script.SharedReference,
+ files: string[]
+ ): Promise<void> {
+ await this.#session.send('input.setFiles', {
+ context: this.id,
+ element,
+ files,
+ });
+ }
+
[disposeSymbol](): void {
this.#reason ??=
'Browsing context already closed, probably because the user context closed.';
@@ -472,4 +547,24 @@ export class BrowsingContext extends EventEmitter<{
this.#disposables.dispose();
super[disposeSymbol]();
}
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async deleteCookie(
+ ...cookieFilters: Bidi.Storage.CookieFilter[]
+ ): Promise<void> {
+ await Promise.all(
+ cookieFilters.map(async filter => {
+ await this.#session.send('storage.deleteCookies', {
+ filter: filter,
+ partition: {
+ type: 'context',
+ context: this.id,
+ },
+ });
+ })
+ );
+ }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts
index b9de14372b..9c26a03503 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts
@@ -38,6 +38,21 @@ export interface Commands {
returnType: Bidi.EmptyResult;
};
+ 'browser.createUserContext': {
+ params: Bidi.EmptyParams;
+ returnType: Bidi.Browser.CreateUserContextResult;
+ };
+ 'browser.getUserContexts': {
+ params: Bidi.EmptyParams;
+ returnType: Bidi.Browser.GetUserContextsResult;
+ };
+ 'browser.removeUserContext': {
+ params: {
+ userContext: Bidi.Browser.UserContext;
+ };
+ returnType: Bidi.Browser.RemoveUserContext;
+ };
+
'browsingContext.activate': {
params: Bidi.BrowsingContext.ActivateParameters;
returnType: Bidi.EmptyResult;
@@ -91,6 +106,15 @@ export interface Commands {
params: Bidi.Input.ReleaseActionsParameters;
returnType: Bidi.EmptyResult;
};
+ 'input.setFiles': {
+ params: Bidi.Input.SetFilesParameters;
+ returnType: Bidi.EmptyResult;
+ };
+
+ 'permissions.setPermission': {
+ params: Bidi.Permissions.SetPermissionParameters;
+ returnType: Bidi.EmptyResult;
+ };
'session.end': {
params: Bidi.EmptyParams;
@@ -112,6 +136,19 @@ export interface Commands {
params: Bidi.Session.SubscriptionRequest;
returnType: Bidi.EmptyResult;
};
+
+ 'storage.deleteCookies': {
+ params: Bidi.Storage.DeleteCookiesParameters;
+ returnType: Bidi.Storage.DeleteCookiesResult;
+ };
+ 'storage.getCookies': {
+ params: Bidi.Storage.GetCookiesParameters;
+ returnType: Bidi.Storage.GetCookiesResult;
+ };
+ 'storage.setCookie': {
+ params: Bidi.Storage.SetCookieParameters;
+ returnType: Bidi.Storage.SetCookieParameters;
+ };
}
/**
@@ -133,7 +170,4 @@ export interface Connection<Events extends BidiEvents = BidiEvents>
method: T,
params: Commands[T]['params']
): Promise<{result: Commands[T]['returnType']}>;
-
- // This will pipe events into the provided emitter.
- pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void;
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts
index a7efbfeb2c..50040164a5 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts
@@ -41,9 +41,10 @@ export class Navigation extends EventEmitter<{
// keep-sorted start
#request: Request | undefined;
+ #navigation: Navigation | undefined;
readonly #browsingContext: BrowsingContext;
readonly #disposables = new DisposableStack();
- readonly #id = new Deferred<string>();
+ readonly #id = new Deferred<string | null>();
// keep-sorted end
private constructor(context: BrowsingContext) {
@@ -65,31 +66,48 @@ export class Navigation extends EventEmitter<{
this.dispose();
});
- this.#browsingContext.on('request', ({request}) => {
- if (request.navigation === this.#id.value()) {
- this.#request = request;
- this.emit('request', request);
+ browsingContextEmitter.on('request', ({request}) => {
+ if (
+ request.navigation === undefined ||
+ this.#request !== undefined ||
+ // If a request with a navigation ID comes in, then the navigation ID is
+ // for this navigation.
+ !this.#matches(request.navigation)
+ ) {
+ return;
}
+
+ this.#request = request;
+ this.emit('request', request);
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session)
);
- // To get the navigation ID if any.
+ sessionEmitter.on('browsingContext.navigationStarted', info => {
+ if (
+ info.context !== this.#browsingContext.id ||
+ this.#navigation !== undefined
+ ) {
+ return;
+ }
+ this.#navigation = Navigation.from(this.#browsingContext);
+ });
+
for (const eventName of [
'browsingContext.domContentLoaded',
'browsingContext.load',
] as const) {
sessionEmitter.on(eventName, info => {
- if (info.context !== this.#browsingContext.id) {
- return;
- }
- if (!info.navigation) {
+ if (
+ info.context !== this.#browsingContext.id ||
+ info.navigation === null ||
+ !this.#matches(info.navigation)
+ ) {
return;
}
- if (!this.#id.resolved()) {
- this.#id.resolve(info.navigation);
- }
+
+ this.dispose();
});
}
@@ -99,18 +117,15 @@ export class Navigation extends EventEmitter<{
['browsingContext.navigationAborted', 'aborted'],
] as const) {
sessionEmitter.on(eventName, info => {
- if (info.context !== this.#browsingContext.id) {
- return;
- }
- if (!info.navigation) {
- return;
- }
- if (!this.#id.resolved()) {
- this.#id.resolve(info.navigation);
- }
- if (this.#id.value() !== info.navigation) {
+ if (
+ info.context !== this.#browsingContext.id ||
+ // Note we don't check if `navigation` is null since `null` means the
+ // fragment navigated.
+ !this.#matches(info.navigation)
+ ) {
return;
}
+
this.emit(event, {
url: info.url,
timestamp: new Date(info.timestamp),
@@ -120,6 +135,17 @@ export class Navigation extends EventEmitter<{
}
}
+ #matches(navigation: string | null): boolean {
+ if (this.#navigation !== undefined && !this.#navigation.disposed) {
+ return false;
+ }
+ if (!this.#id.resolved()) {
+ this.#id.resolve(navigation);
+ return true;
+ }
+ return this.#id.value() === navigation;
+ }
+
// keep-sorted start block=yes
get #session() {
return this.#browsingContext.userContext.browser.session;
@@ -130,6 +156,9 @@ export class Navigation extends EventEmitter<{
get request(): Request | undefined {
return this.#request;
}
+ get navigation(): Navigation | undefined {
+ return this.#navigation;
+ }
// keep-sorted end
@inertIfDisposed
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts
index d9bbbede50..392194cec8 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts
@@ -9,7 +9,9 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+import type {BidiConnection} from '../Connection.js';
+import type {Browser} from './Browser.js';
import type {BrowsingContext} from './BrowsingContext.js';
import type {Session} from './Session.js';
@@ -33,6 +35,8 @@ export type EvaluateOptions = Omit<
* @internal
*/
export abstract class Realm extends EventEmitter<{
+ /** Emitted whenever the realm has updated. */
+ updated: Realm;
/** Emitted when the realm is destroyed. */
destroyed: {reason: string};
/** Emitted when a dedicated worker is created in the realm. */
@@ -55,22 +59,12 @@ export abstract class Realm extends EventEmitter<{
// keep-sorted end
}
- protected initialize(): void {
- const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
- sessionEmitter.on('script.realmDestroyed', info => {
- if (info.realm !== this.id) {
- return;
- }
- this.dispose('Realm already destroyed.');
- });
- }
-
// keep-sorted start block=yes
get disposed(): boolean {
return this.#reason !== undefined;
}
protected abstract get session(): Session;
- protected get target(): Bidi.Script.Target {
+ get target(): Bidi.Script.Target {
return {realm: this.id};
}
// keep-sorted end
@@ -128,6 +122,18 @@ export abstract class Realm extends EventEmitter<{
return result;
}
+ @throwIfDisposed<Realm>(realm => {
+ // SAFETY: Disposal implies this exists.
+ return realm.#reason!;
+ })
+ async resolveExecutionContextId(): Promise<number> {
+ const {result} = await (this.session.connection as BidiConnection).send(
+ 'cdp.resolveRealm',
+ {realm: this.id}
+ );
+ return result.executionContextId;
+ }
+
[disposeSymbol](): void {
this.#reason ??=
'Realm already destroyed, probably because all associated browsing contexts closed.';
@@ -144,7 +150,7 @@ export abstract class Realm extends EventEmitter<{
export class WindowRealm extends Realm {
static from(context: BrowsingContext, sandbox?: string): WindowRealm {
const realm = new WindowRealm(context, sandbox);
- realm.initialize();
+ realm.#initialize();
return realm;
}
@@ -153,13 +159,7 @@ export class WindowRealm extends Realm {
readonly sandbox?: string;
// keep-sorted end
- readonly #workers: {
- dedicated: Map<string, DedicatedWorkerRealm>;
- shared: Map<string, SharedWorkerRealm>;
- } = {
- dedicated: new Map(),
- shared: new Map(),
- };
+ readonly #workers = new Map<string, DedicatedWorkerRealm>();
private constructor(context: BrowsingContext, sandbox?: string) {
super('', '');
@@ -169,16 +169,26 @@ export class WindowRealm extends Realm {
// keep-sorted end
}
- override initialize(): void {
- super.initialize();
+ #initialize(): void {
+ const browsingContextEmitter = this.disposables.use(
+ new EventEmitter(this.browsingContext)
+ );
+ browsingContextEmitter.on('closed', ({reason}) => {
+ this.dispose(reason);
+ });
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
sessionEmitter.on('script.realmCreated', info => {
- if (info.type !== 'window') {
+ if (
+ info.type !== 'window' ||
+ info.context !== this.browsingContext.id ||
+ info.sandbox !== this.sandbox
+ ) {
return;
}
(this as any).id = info.realm;
(this as any).origin = info.origin;
+ this.emit('updated', this);
});
sessionEmitter.on('script.realmCreated', info => {
if (info.type !== 'dedicated-worker') {
@@ -189,32 +199,16 @@ export class WindowRealm extends Realm {
}
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
- this.#workers.dedicated.set(realm.id, realm);
+ this.#workers.set(realm.id, realm);
const realmEmitter = this.disposables.use(new EventEmitter(realm));
realmEmitter.once('destroyed', () => {
realmEmitter.removeAllListeners();
- this.#workers.dedicated.delete(realm.id);
+ this.#workers.delete(realm.id);
});
this.emit('worker', realm);
});
-
- this.browsingContext.userContext.browser.on('sharedworker', ({realm}) => {
- if (!realm.owners.has(this)) {
- return;
- }
-
- this.#workers.shared.set(realm.id, realm);
-
- const realmEmitter = this.disposables.use(new EventEmitter(realm));
- realmEmitter.once('destroyed', () => {
- realmEmitter.removeAllListeners();
- this.#workers.shared.delete(realm.id);
- });
-
- this.emit('sharedworker', realm);
- });
}
override get session(): Session {
@@ -244,7 +238,7 @@ export class DedicatedWorkerRealm extends Realm {
origin: string
): DedicatedWorkerRealm {
const realm = new DedicatedWorkerRealm(owner, id, origin);
- realm.initialize();
+ realm.#initialize();
return realm;
}
@@ -262,10 +256,14 @@ export class DedicatedWorkerRealm extends Realm {
this.owners = new Set([owner]);
}
- override initialize(): void {
- super.initialize();
-
+ #initialize(): void {
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
+ sessionEmitter.on('script.realmDestroyed', info => {
+ if (info.realm !== this.id) {
+ return;
+ }
+ this.dispose('Realm already destroyed.');
+ });
sessionEmitter.on('script.realmCreated', info => {
if (info.type !== 'dedicated-worker') {
return;
@@ -296,34 +294,30 @@ export class DedicatedWorkerRealm extends Realm {
* @internal
*/
export class SharedWorkerRealm extends Realm {
- static from(
- owners: [WindowRealm, ...WindowRealm[]],
- id: string,
- origin: string
- ): SharedWorkerRealm {
- const realm = new SharedWorkerRealm(owners, id, origin);
- realm.initialize();
+ static from(browser: Browser, id: string, origin: string): SharedWorkerRealm {
+ const realm = new SharedWorkerRealm(browser, id, origin);
+ realm.#initialize();
return realm;
}
// keep-sorted start
readonly #workers = new Map<string, DedicatedWorkerRealm>();
- readonly owners: Set<WindowRealm>;
+ readonly browser: Browser;
// keep-sorted end
- private constructor(
- owners: [WindowRealm, ...WindowRealm[]],
- id: string,
- origin: string
- ) {
+ private constructor(browser: Browser, id: string, origin: string) {
super(id, origin);
- this.owners = new Set(owners);
+ this.browser = browser;
}
- override initialize(): void {
- super.initialize();
-
+ #initialize(): void {
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
+ sessionEmitter.on('script.realmDestroyed', info => {
+ if (info.realm !== this.id) {
+ return;
+ }
+ this.dispose('Realm already destroyed.');
+ });
sessionEmitter.on('script.realmCreated', info => {
if (info.type !== 'dedicated-worker') {
return;
@@ -345,7 +339,6 @@ export class SharedWorkerRealm extends Realm {
}
override get session(): Session {
- // SAFETY: At least one owner will exist.
- return this.owners.values().next().value.session;
+ return this.browser.session;
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts
index 2a445f7d87..fd616b668d 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts
@@ -66,10 +66,11 @@ export class Request extends EventEmitter<{
new EventEmitter(this.#session)
);
sessionEmitter.on('network.beforeRequestSent', event => {
- if (event.context !== this.#browsingContext.id) {
- return;
- }
- if (event.request.request !== this.id) {
+ if (
+ event.context !== this.#browsingContext.id ||
+ event.request.request !== this.id ||
+ event.redirectCount !== this.#event.redirectCount + 1
+ ) {
return;
}
this.#redirect = Request.from(this.#browsingContext, event);
@@ -77,10 +78,11 @@ export class Request extends EventEmitter<{
this.dispose();
});
sessionEmitter.on('network.fetchError', event => {
- if (event.context !== this.#browsingContext.id) {
- return;
- }
- if (event.request.request !== this.id) {
+ if (
+ event.context !== this.#browsingContext.id ||
+ event.request.request !== this.id ||
+ this.#event.redirectCount !== event.redirectCount
+ ) {
return;
}
this.#error = event.errorText;
@@ -88,14 +90,19 @@ export class Request extends EventEmitter<{
this.dispose();
});
sessionEmitter.on('network.responseCompleted', event => {
- if (event.context !== this.#browsingContext.id) {
- return;
- }
- if (event.request.request !== this.id) {
+ if (
+ event.context !== this.#browsingContext.id ||
+ event.request.request !== this.id ||
+ this.#event.redirectCount !== event.redirectCount
+ ) {
return;
}
this.#response = event.response;
this.emit('success', this.#response);
+ // In case this is a redirect.
+ if (this.#response.status >= 300 && this.#response.status < 400) {
+ return;
+ }
this.dispose();
});
}
@@ -126,7 +133,7 @@ export class Request extends EventEmitter<{
return this.#event.navigation ?? undefined;
}
get redirect(): Request | undefined {
- return this.redirect;
+ return this.#redirect;
}
get response(): Bidi.Network.ResponseData | undefined {
return this.#response;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts
index b6e28061f1..ffd39769e7 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts
@@ -8,7 +8,11 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {debugError} from '../../common/util.js';
-import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
+import {
+ bubble,
+ inertIfDisposed,
+ throwIfDisposed,
+} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import {Browser} from './Browser.js';
@@ -81,7 +85,8 @@ export class Session
readonly #disposables = new DisposableStack();
readonly #info: Bidi.Session.NewResult;
readonly browser!: Browser;
- readonly connection: Connection;
+ @bubble()
+ accessor connection: Connection;
// keep-sorted end
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
@@ -93,8 +98,6 @@ export class Session
}
async #initialize(): Promise<void> {
- this.connection.pipeTo(this);
-
// SAFETY: We use `any` to allow assignment of the readonly property.
(this as any).browser = await Browser.from(this);
@@ -102,6 +105,19 @@ export class Session
browserEmitter.once('closed', ({reason}) => {
this.dispose(reason);
});
+
+ // TODO: Currently, some implementations do not emit navigationStarted event
+ // for fragment navigations (as per spec) and some do. This could emits a
+ // synthetic navigationStarted to work around this inconsistency.
+ const seen = new WeakSet();
+ this.on('browsingContext.fragmentNavigated', info => {
+ if (seen.has(info)) {
+ return;
+ }
+ seen.add(info);
+ this.emit('browsingContext.navigationStarted', info);
+ this.emit('browsingContext.fragmentNavigated', info);
+ });
}
// keep-sorted start block=yes
@@ -125,10 +141,6 @@ export class Session
this[disposeSymbol]();
}
- pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
- this.connection.pipeTo(emitter);
- }
-
/**
* Currently, there is a 1:1 relationship between the session and the
* session. In the future, we might support multiple sessions and in that
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts
index 01ee5c7649..72859c6a53 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts
@@ -12,6 +12,7 @@ import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {Browser} from './Browser.js';
+import type {GetCookiesOptions} from './BrowsingContext.js';
import {BrowsingContext} from './BrowsingContext.js';
/**
@@ -43,7 +44,7 @@ export class UserContext extends EventEmitter<{
reason: string;
};
}> {
- static DEFAULT = 'default';
+ static DEFAULT = 'default' as const;
static create(browser: Browser, id: string): UserContext {
const context = new UserContext(browser, id);
@@ -84,6 +85,10 @@ export class UserContext extends EventEmitter<{
return;
}
+ if (info.userContext !== this.#id) {
+ return;
+ }
+
const browsingContext = BrowsingContext.from(
this,
undefined,
@@ -143,6 +148,7 @@ export class UserContext extends EventEmitter<{
type,
...options,
referenceContext: options.referenceContext?.id,
+ userContext: this.#id,
});
const browsingContext = this.#browsingContexts.get(contextId);
@@ -161,12 +167,71 @@ export class UserContext extends EventEmitter<{
})
async remove(): Promise<void> {
try {
- // TODO: Call `removeUserContext` once available.
+ await this.#session.send('browser.removeUserContext', {
+ userContext: this.#id,
+ });
} finally {
this.dispose('User context already closed.');
}
}
+ @throwIfDisposed<UserContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async getCookies(
+ options: GetCookiesOptions = {},
+ sourceOrigin: string | undefined = undefined
+ ): Promise<Bidi.Network.Cookie[]> {
+ const {
+ result: {cookies},
+ } = await this.#session.send('storage.getCookies', {
+ ...options,
+ partition: {
+ type: 'storageKey',
+ userContext: this.#id,
+ sourceOrigin,
+ },
+ });
+ return cookies;
+ }
+
+ @throwIfDisposed<UserContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async setCookie(
+ cookie: Bidi.Storage.PartialCookie,
+ sourceOrigin?: string
+ ): Promise<void> {
+ await this.#session.send('storage.setCookie', {
+ cookie,
+ partition: {
+ type: 'storageKey',
+ sourceOrigin,
+ userContext: this.id,
+ },
+ });
+ }
+
+ @throwIfDisposed<UserContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async setPermissions(
+ origin: string,
+ descriptor: Bidi.Permissions.PermissionDescriptor,
+ state: Bidi.Permissions.PermissionState
+ ): Promise<void> {
+ await this.#session.send('permissions.setPermission', {
+ origin,
+ descriptor,
+ state,
+ // @ts-expect-error not standard implementation.
+ 'goog:userContext': this.#id,
+ });
+ }
+
[disposeSymbol](): void {
this.#reason ??=
'User context already closed, probably because the browser disconnected/closed.';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts
deleted file mode 100644
index 73b86cba9c..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-
-import type {
- ObservableInput,
- ObservedValueOf,
- OperatorFunction,
-} from '../../third_party/rxjs/rxjs.js';
-import {catchError} from '../../third_party/rxjs/rxjs.js';
-import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
-import {ProtocolError, TimeoutError} from '../common/Errors.js';
-
-/**
- * @internal
- */
-export type BiDiNetworkIdle = Extract<
- PuppeteerLifeCycleEvent,
- 'networkidle0' | 'networkidle2'
-> | null;
-
-/**
- * @internal
- */
-export function getBiDiLifeCycles(
- event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
-): [
- Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'>,
- BiDiNetworkIdle,
-] {
- if (Array.isArray(event)) {
- const pageLifeCycle = event.some(lifeCycle => {
- return lifeCycle !== 'domcontentloaded';
- })
- ? 'load'
- : 'domcontentloaded';
-
- const networkLifeCycle = event.reduce((acc, lifeCycle) => {
- if (lifeCycle === 'networkidle0') {
- return lifeCycle;
- } else if (acc !== 'networkidle0' && lifeCycle === 'networkidle2') {
- return lifeCycle;
- }
- return acc;
- }, null as BiDiNetworkIdle);
-
- return [pageLifeCycle, networkLifeCycle];
- }
-
- if (event === 'networkidle0' || event === 'networkidle2') {
- return ['load', event];
- }
-
- return [event, null];
-}
-
-/**
- * @internal
- */
-export const lifeCycleToReadinessState = new Map<
- PuppeteerLifeCycleEvent,
- Bidi.BrowsingContext.ReadinessState
->([
- ['load', Bidi.BrowsingContext.ReadinessState.Complete],
- ['domcontentloaded', Bidi.BrowsingContext.ReadinessState.Interactive],
-]);
-
-export function getBiDiReadinessState(
- event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
-): [Bidi.BrowsingContext.ReadinessState, BiDiNetworkIdle] {
- const lifeCycles = getBiDiLifeCycles(event);
- const readiness = lifeCycleToReadinessState.get(lifeCycles[0])!;
- return [readiness, lifeCycles[1]];
-}
-
-/**
- * @internal
- */
-export const lifeCycleToSubscribedEvent = new Map<
- PuppeteerLifeCycleEvent,
- 'browsingContext.load' | 'browsingContext.domContentLoaded'
->([
- ['load', 'browsingContext.load'],
- ['domcontentloaded', 'browsingContext.domContentLoaded'],
-]);
-
-/**
- * @internal
- */
-export function getBiDiLifecycleEvent(
- event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
-): [
- 'browsingContext.load' | 'browsingContext.domContentLoaded',
- BiDiNetworkIdle,
-] {
- const lifeCycles = getBiDiLifeCycles(event);
- const bidiEvent = lifeCycleToSubscribedEvent.get(lifeCycles[0])!;
- return [bidiEvent, lifeCycles[1]];
-}
-
-/**
- * @internal
- */
-export function rewriteNavigationError<T, R extends ObservableInput<T>>(
- message: string,
- ms: number
-): OperatorFunction<T, T | ObservedValueOf<R>> {
- return catchError<T, R>(error => {
- if (error instanceof ProtocolError) {
- error.message += ` at ${message}`;
- } else if (error instanceof TimeoutError) {
- error.message = `Navigation timeout of ${ms} ms exceeded`;
- }
- throw error;
- });
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts
index 41e88e26c2..e1d64c2f4c 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts
@@ -6,32 +6,10 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import {PuppeteerURL, debugError} from '../common/util.js';
+import {ProtocolError, TimeoutError} from '../common/Errors.js';
+import {PuppeteerURL} from '../common/util.js';
import {BidiDeserializer} from './Deserializer.js';
-import type {BidiRealm} from './Realm.js';
-
-/**
- * @internal
- */
-export async function releaseReference(
- client: BidiRealm,
- remoteReference: Bidi.Script.RemoteReference
-): Promise<void> {
- if (!remoteReference.handle) {
- return;
- }
- await client.connection
- .send('script.disown', {
- target: client.target,
- handles: [remoteReference.handle],
- })
- .catch(error => {
- // Exceptions might happen in case of a page been navigated or closed.
- // Swallow these since they are harmless and we don't leak anything in this case.
- debugError(error);
- });
-}
/**
* @internal
@@ -79,3 +57,20 @@ export function createEvaluationError(
error.stack = [details.text, ...stackLines].join('\n');
return error;
}
+
+/**
+ * @internal
+ */
+export function rewriteNavigationError(
+ message: string,
+ ms: number
+): (error: unknown) => never {
+ return error => {
+ if (error instanceof ProtocolError) {
+ error.message += ` at ${message}`;
+ } else if (error instanceof TimeoutError) {
+ error.message = `Navigation timeout of ${ms} ms exceeded`;
+ }
+ throw error;
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts
index 7a6a6f8582..7fe372788f 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts
@@ -1,3 +1,8 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
import {JSHandle} from '../api/JSHandle.js';
import {debugError} from '../common/util.js';
import {DisposableStack} from '../util/disposable.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts
index 7698acd164..5c8a4c24da 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts
@@ -18,7 +18,6 @@ import {
type IsPageTargetCallback,
type Permission,
type TargetFilterCallback,
- type WaitForTargetOptions,
} from '../api/Browser.js';
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
@@ -201,7 +200,7 @@ export class CdpBrowser extends BrowserBase {
return this.#isPageTargetCallback;
}
- override async createIncognitoBrowserContext(
+ override async createBrowserContext(
options: BrowserContextOptions = {}
): Promise<CdpBrowserContext> {
const {proxyServer, proxyBypassList} = options;
@@ -451,15 +450,6 @@ export class CdpBrowserContext extends BrowserContext {
});
}
- override waitForTarget(
- predicate: (x: Target) => boolean | Promise<boolean>,
- options: WaitForTargetOptions = {}
- ): Promise<Target> {
- return this.#browser.waitForTarget(target => {
- return target.browserContext() === this && predicate(target);
- }, options);
- }
-
override async pages(): Promise<Page[]> {
const pages = await Promise.all(
this.targets()
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts
index 8598967fe7..823b3b462e 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts
@@ -267,13 +267,23 @@ export class EmulationManager {
const hasTouch = viewport.hasTouch || false;
await Promise.all([
- client.send('Emulation.setDeviceMetricsOverride', {
- mobile,
- width,
- height,
- deviceScaleFactor,
- screenOrientation,
- }),
+ client
+ .send('Emulation.setDeviceMetricsOverride', {
+ mobile,
+ width,
+ height,
+ deviceScaleFactor,
+ screenOrientation,
+ })
+ .catch(err => {
+ if (
+ err.message.includes('Target does not support metrics override')
+ ) {
+ debugError(err);
+ return;
+ }
+ throw err;
+ }),
client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch,
}),
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts
index 844120d7ff..edc7009b11 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts
@@ -20,6 +20,7 @@ import type {
DeviceRequestPromptManager,
} from './DeviceRequestPrompt.js';
import type {FrameManager} from './FrameManager.js';
+import type {IsolatedWorldChart} from './IsolatedWorld.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {
@@ -35,6 +36,7 @@ export class CdpFrame extends Frame {
#url = '';
#detached = false;
#client!: CDPSession;
+ worlds!: IsolatedWorldChart;
_frameManager: FrameManager;
override _id: string;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts
index 029e77470b..1331513e19 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts
@@ -28,6 +28,7 @@ import type {CdpHTTPResponse} from './HTTPResponse.js';
* @internal
*/
export class CdpHTTPRequest extends HTTPRequest {
+ override id: string;
declare _redirectChain: CdpHTTPRequest[];
declare _response: CdpHTTPResponse | null;
@@ -91,7 +92,7 @@ export class CdpHTTPRequest extends HTTPRequest {
) {
super();
this.#client = client;
- this._requestId = data.requestId;
+ this.id = data.requestId;
this.#isNavigationRequest =
data.requestId === data.loaderId && data.type === 'Document';
this._interceptionId = interceptionId;
@@ -188,7 +189,7 @@ export class CdpHTTPRequest extends HTTPRequest {
override async fetchPostData(): Promise<string | undefined> {
try {
const result = await this.#client.send('Network.getRequestPostData', {
- requestId: this._requestId,
+ requestId: this.id,
});
return result.postData;
} catch (err) {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts
index 2b2264ffd4..eb92ab07e3 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts
@@ -130,7 +130,7 @@ export class CdpHTTPResponse extends HTTPResponse {
const response = await this.#client.send(
'Network.getResponseBody',
{
- requestId: this.#request._requestId,
+ requestId: this.#request.id,
}
);
return Buffer.from(
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts
index 9bfafddcf3..0674ef4634 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts
@@ -10,16 +10,16 @@ import type {CDPSession} from '../api/CDPSession.js';
import type {Point} from '../api/ElementHandle.js';
import {
Keyboard,
- type KeyDownOptions,
- type KeyPressOptions,
Mouse,
MouseButton,
+ Touchscreen,
+ type KeyDownOptions,
+ type KeyPressOptions,
+ type KeyboardTypeOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
type MouseWheelOptions,
- Touchscreen,
- type KeyboardTypeOptions,
} from '../api/Input.js';
import {
_keyDefinitions,
@@ -573,6 +573,7 @@ export class CdpTouchscreen extends Touchscreen {
y: Math.round(y),
radiusX: 0.5,
radiusY: 0.5,
+ force: 0.5,
},
],
modifiers: this.#keyboard._modifiers,
@@ -588,6 +589,7 @@ export class CdpTouchscreen extends Touchscreen {
y: Math.round(y),
radiusX: 0.5,
radiusY: 0.5,
+ force: 0.5,
},
],
modifiers: this.#keyboard._modifiers,
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts
index a4f5aaa468..fe71ca52fc 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts
@@ -191,14 +191,14 @@ export class LifecycleWatcher {
}
#onRequestFailed(request: HTTPRequest): void {
- if (this.#navigationRequest?._requestId !== request._requestId) {
+ if (this.#navigationRequest?.id !== request.id) {
return;
}
this.#navigationResponseReceived?.resolve();
}
#onResponse(response: HTTPResponse): void {
- if (this.#navigationRequest?._requestId !== response.request()._requestId) {
+ if (this.#navigationRequest?.id !== response.request().id) {
return;
}
this.#navigationResponseReceived?.resolve();
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts
index c3e9a8f609..96f0d20963 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts
@@ -128,6 +128,7 @@ describe('NetworkManager', () => {
url: 'http://localhost:8907/redirect/1.html',
status: 302,
statusText: 'Found',
+ charset: 'utf-8',
headers: {
location: '/redirect/2.html',
Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
@@ -217,6 +218,7 @@ describe('NetworkManager', () => {
url: 'http://localhost:8907/redirect/2.html',
status: 302,
statusText: 'Found',
+ charset: 'utf-8',
headers: {
location: '/redirect/3.html',
Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
@@ -321,6 +323,7 @@ describe('NetworkManager', () => {
url: 'http://localhost:8907/redirect/3.html',
status: 302,
statusText: 'Found',
+ charset: 'utf-8',
headers: {
location: 'http://localhost:8907/empty.html',
Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
@@ -433,6 +436,7 @@ describe('NetworkManager', () => {
'Keep-Alive': 'timeout=5',
'Content-Length': '0',
},
+ charset: 'utf-8',
mimeType: 'text/html',
connectionReused: true,
connectionId: 322,
@@ -613,6 +617,7 @@ describe('NetworkManager', () => {
connection: 'keep-alive',
'content-length': '85862',
},
+ charset: 'utf-8',
mimeType: 'text/plain',
connectionReused: false,
connectionId: 119,
@@ -725,6 +730,7 @@ describe('NetworkManager', () => {
url: 'http://10.1.0.39:42915/empty.html',
status: 200,
statusText: 'OK',
+ charset: 'utf-8',
headers: {
'Cache-Control': 'no-cache, no-store',
Connection: 'keep-alive',
@@ -932,6 +938,7 @@ describe('NetworkManager', () => {
url: 'http://127.0.0.1:54590/empty.html',
status: 200,
statusText: 'OK',
+ charset: 'utf-8',
headers: {
'Cache-Control': 'no-cache, no-store',
Connection: 'keep-alive',
@@ -1036,6 +1043,7 @@ describe('NetworkManager', () => {
url: 'http://localhost:56295/empty.html',
status: 200,
statusText: 'OK',
+ charset: 'utf-8',
headers: {
'Cache-Control': 'no-cache, no-store',
Connection: 'keep-alive',
@@ -1221,6 +1229,7 @@ describe('NetworkManager', () => {
url: 'http://localhost:3000/',
status: 200,
statusText: 'OK',
+ charset: 'utf-8',
headers: {
'Cache-Control': 'max-age=5',
Connection: 'keep-alive',
@@ -1394,6 +1403,7 @@ describe('NetworkManager', () => {
url: 'http://localhost:3000/redirect',
status: 302,
statusText: 'Found',
+ charset: 'utf-8',
headers: {
Connection: 'keep-alive',
Date: 'Wed, 05 Apr 2023 12:39:13 GMT',
@@ -1457,6 +1467,7 @@ describe('NetworkManager', () => {
url: 'http://localhost:3000/',
status: 200,
statusText: 'OK',
+ charset: 'utf-8',
headers: {
'Cache-Control': 'max-age=5',
'Content-Type': 'text/html; charset=utf-8',
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts
index 8b24b9a748..4fd61116d2 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts
@@ -36,11 +36,17 @@ export interface Credentials {
* @public
*/
export interface NetworkConditions {
- // Download speed (bytes/s)
+ /**
+ * Download speed (bytes/s)
+ */
download: number;
- // Upload speed (bytes/s)
+ /**
+ * Upload speed (bytes/s)
+ */
upload: number;
- // Latency (ms)
+ /**
+ * Latency (ms)
+ */
latency: number;
}
@@ -631,7 +637,7 @@ export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
}
#forgetRequest(request: CdpHTTPRequest, events: boolean): void {
- const requestId = request._requestId;
+ const requestId = request.id;
const interceptionId = request._interceptionId;
this.#networkEventManager.forgetRequest(requestId);
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts
index 491637f0ea..d5341cf3bb 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts
@@ -4,8 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {Readable} from 'stream';
-
import type {Protocol} from 'devtools-protocol';
import {firstValueFrom, from, raceWith} from '../../third_party/rxjs/rxjs.js';
@@ -32,6 +30,11 @@ import {
ConsoleMessage,
type ConsoleMessageType,
} from '../common/ConsoleMessage.js';
+import type {
+ Cookie,
+ DeleteCookiesRequest,
+ CookieParam,
+} from '../common/Cookie.js';
import {TargetCloseError} from '../common/Errors.js';
import {FileChooser} from '../common/FileChooser.js';
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
@@ -80,6 +83,15 @@ import {
} from './utils.js';
import {CdpWebWorker} from './WebWorker.js';
+function convertConsoleMessageLevel(method: string): ConsoleMessageType {
+ switch (method) {
+ case 'warning':
+ return 'warn';
+ default:
+ return method as ConsoleMessageType;
+ }
+}
+
/**
* @internal
*/
@@ -346,6 +358,8 @@ export class CdpPage extends Page {
const worker = new CdpWebWorker(
session,
session._target().url(),
+ session._target()._targetId,
+ session._target().type(),
this.#addConsoleMessage.bind(this),
this.#handleException.bind(this)
);
@@ -470,7 +484,12 @@ export class CdpPage extends Page {
if (source !== 'worker') {
this.emit(
PageEvent.Console,
- new ConsoleMessage(level, text, [], [{url, lineNumber}])
+ new ConsoleMessage(
+ convertConsoleMessageLevel(level),
+ text,
+ [],
+ [{url, lineNumber}]
+ )
);
}
}
@@ -572,16 +591,14 @@ export class CdpPage extends Page {
) as HandleFor<Prototype[]>;
}
- override async cookies(
- ...urls: string[]
- ): Promise<Protocol.Network.Cookie[]> {
+ override async cookies(...urls: string[]): Promise<Cookie[]> {
const originalCookies = (
await this.#primaryTargetClient.send('Network.getCookies', {
urls: urls.length ? urls : [this.url()],
})
).cookies;
- const unsupportedCookieAttributes = ['priority'];
+ const unsupportedCookieAttributes = ['sourcePort'];
const filterUnsupportedAttributes = (
cookie: Protocol.Network.Cookie
): Protocol.Network.Cookie => {
@@ -594,7 +611,7 @@ export class CdpPage extends Page {
}
override async deleteCookie(
- ...cookies: Protocol.Network.DeleteCookiesRequest[]
+ ...cookies: DeleteCookiesRequest[]
): Promise<void> {
const pageURL = this.url();
for (const cookie of cookies) {
@@ -606,9 +623,7 @@ export class CdpPage extends Page {
}
}
- override async setCookie(
- ...cookies: Protocol.Network.CookieParam[]
- ): Promise<void> {
+ override async setCookie(...cookies: CookieParam[]): Promise<void> {
const pageURL = this.url();
const startsWithHTTP = pageURL.startsWith('http');
const items = cookies.map(cookie => {
@@ -810,7 +825,11 @@ export class CdpPage extends Page {
const values = event.args.map(arg => {
return createCdpHandle(context._world, arg);
});
- this.#addConsoleMessage(event.type, values, event.stackTrace);
+ this.#addConsoleMessage(
+ convertConsoleMessageLevel(event.type),
+ values,
+ event.stackTrace
+ );
}
async #onBindingCalled(
@@ -842,7 +861,7 @@ export class CdpPage extends Page {
}
#addConsoleMessage(
- eventType: ConsoleMessageType,
+ eventType: string,
args: JSHandle[],
stackTrace?: Protocol.Runtime.StackTrace
): void {
@@ -874,7 +893,7 @@ export class CdpPage extends Page {
}
}
const message = new ConsoleMessage(
- eventType,
+ convertConsoleMessageLevel(eventType),
textTokens.join(' '),
args,
stackTraceLocations
@@ -1086,7 +1105,9 @@ export class CdpPage extends Page {
return data;
}
- override async createPDFStream(options: PDFOptions = {}): Promise<Readable> {
+ override async createPDFStream(
+ options: PDFOptions = {}
+ ): Promise<ReadableStream<Uint8Array>> {
const {timeout: ms = this._timeoutSettings.timeout()} = options;
const {
landscape,
@@ -1102,6 +1123,7 @@ export class CdpPage extends Page {
preferCSSPageSize,
omitBackground,
tagged: generateTaggedPDF,
+ outline: generateDocumentOutline,
} = parsePDFOptions(options);
if (omitBackground) {
@@ -1127,6 +1149,7 @@ export class CdpPage extends Page {
pageRanges,
preferCSSPageSize,
generateTaggedPDF,
+ generateDocumentOutline,
}
);
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
index df035ae52b..2e30f900c3 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
@@ -40,10 +40,3 @@ export const PredefinedNetworkConditions = Object.freeze({
latency: 150 * 3.75,
} as NetworkConditions,
});
-
-/**
- * @deprecated Import {@link PredefinedNetworkConditions}.
- *
- * @public
- */
-export const networkConditions = PredefinedNetworkConditions;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts
index b3e9ea83ec..ab8b00475b 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts
@@ -290,6 +290,8 @@ export class WorkerTarget extends CdpTarget {
return new CdpWebWorker(
client,
this._getTargetInfo().url,
+ this._targetId,
+ this.type(),
() => {} /* consoleAPICalled */,
() => {} /* exceptionThrown */
);
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts
index 552e8a6cf5..ed2407ba66 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts
@@ -7,8 +7,8 @@ import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import type {Realm} from '../api/Realm.js';
+import {TargetType} from '../api/Target.js';
import {WebWorker} from '../api/WebWorker.js';
-import type {ConsoleMessageType} from '../common/ConsoleMessage.js';
import {TimeoutSettings} from '../common/TimeoutSettings.js';
import {debugError} from '../common/util.js';
@@ -20,7 +20,7 @@ import {CdpJSHandle} from './JSHandle.js';
* @internal
*/
export type ConsoleAPICalledCallback = (
- eventType: ConsoleMessageType,
+ eventType: string,
handles: CdpJSHandle[],
trace?: Protocol.Runtime.StackTrace
) => void;
@@ -38,15 +38,21 @@ export type ExceptionThrownCallback = (
export class CdpWebWorker extends WebWorker {
#world: IsolatedWorld;
#client: CDPSession;
+ readonly #id: string;
+ readonly #targetType: TargetType;
constructor(
client: CDPSession,
url: string,
+ targetId: string,
+ targetType: TargetType,
consoleAPICalled: ConsoleAPICalledCallback,
exceptionThrown: ExceptionThrownCallback
) {
super(url);
+ this.#id = targetId;
this.#client = client;
+ this.#targetType = targetType;
this.#world = new IsolatedWorld(this, new TimeoutSettings());
this.#client.once('Runtime.executionContextCreated', async event => {
@@ -80,4 +86,25 @@ export class CdpWebWorker extends WebWorker {
get client(): CDPSession {
return this.#client;
}
+
+ override async close(): Promise<void> {
+ switch (this.#targetType) {
+ case TargetType.SERVICE_WORKER:
+ case TargetType.SHARED_WORKER: {
+ // For service and shared workers we need to close the target and detach to allow
+ // the worker to stop.
+ await this.client.connection()?.send('Target.closeTarget', {
+ targetId: this.#id,
+ });
+ await this.client.connection()?.send('Target.detachFromTarget', {
+ sessionId: this.client.id(),
+ });
+ break;
+ }
+ default:
+ await this.evaluate(() => {
+ self.close();
+ });
+ }
+ }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts
index 217e53bedd..4c8308da6e 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts
@@ -14,7 +14,6 @@ import {isErrorLike} from '../util/ErrorLike.js';
import type {ConnectionTransport} from './ConnectionTransport.js';
import type {ConnectOptions} from './ConnectOptions.js';
import type {BrowserConnectOptions} from './ConnectOptions.js';
-import {getFetch} from './fetch.js';
const getWebSocketTransportClass = async () => {
return isNode
@@ -93,9 +92,8 @@ async function getConnectionTransport(
async function getWSEndpoint(browserURL: string): Promise<string> {
const endpointURL = new URL('/json/version', browserURL);
- const fetch = await getFetch();
try {
- const result = await fetch(endpointURL.toString(), {
+ const result = await globalThis.fetch(endpointURL.toString(), {
method: 'GET',
});
if (!result.ok) {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts
index c64d109a7c..fe71e57587 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts
@@ -32,7 +32,13 @@ export interface Configuration {
* See {@link PuppeteerNode.launch | puppeteer.launch} on how executable path
* is inferred.
*
- * @defaultValue A compatible-revision of the browser.
+ * Use a specific browser version (e.g., 119.0.6045.105). If you use an alias
+ * such `stable` or `canary` it will only work during the installation of
+ * Puppeteer and it will fail when launching the browser.
+ *
+ * @example 119.0.6045.105
+ * @defaultValue The pinned browser version supported by the current Puppeteer
+ * version.
*/
browserRevision?: string;
/**
@@ -51,20 +57,12 @@ export interface Configuration {
* @remarks
* This must include the protocol and may even need a path prefix.
*
- * @defaultValue Either https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing or
+ * @defaultValue Either https://storage.googleapis.com/chrome-for-testing-public or
* https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central,
* depending on the product.
*/
downloadBaseUrl?: string;
/**
- * Specifies the path for the downloads folder.
- *
- * Can be overridden by `PUPPETEER_DOWNLOAD_PATH`.
- *
- * @defaultValue `<cacheDirectory>`
- */
- downloadPath?: string;
- /**
* Specifies an executable path to be used in
* {@link PuppeteerNode.launch | puppeteer.launch}.
*
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts
index 85d2db9f75..c2aad7679d 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts
@@ -35,7 +35,7 @@ export type ConsoleMessageType =
| 'debug'
| 'info'
| 'error'
- | 'warning'
+ | 'warn'
| 'dir'
| 'dirxml'
| 'table'
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Cookie.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Cookie.ts
new file mode 100644
index 0000000000..c9f7283075
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Cookie.ts
@@ -0,0 +1,186 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Represents the cookie's 'SameSite' status:
+ * https://tools.ietf.org/html/draft-west-first-party-cookies
+ *
+ * @public
+ */
+export type CookieSameSite = 'Strict' | 'Lax' | 'None';
+
+/**
+ * Represents the cookie's 'Priority' status:
+ * https://tools.ietf.org/html/draft-west-cookie-priority-00
+ *
+ * @public
+ */
+export type CookiePriority = 'Low' | 'Medium' | 'High';
+
+/**
+ * Represents the source scheme of the origin that originally set the cookie. A value of
+ * "Unset" allows protocol clients to emulate legacy cookie scope for the scheme.
+ * This is a temporary ability and it will be removed in the future.
+ *
+ * @public
+ */
+export type CookieSourceScheme = 'Unset' | 'NonSecure' | 'Secure';
+
+/**
+ * Represents a cookie object.
+ *
+ * @public
+ */
+export interface Cookie {
+ /**
+ * Cookie name.
+ */
+ name: string;
+ /**
+ * Cookie value.
+ */
+ value: string;
+ /**
+ * Cookie domain.
+ */
+ domain: string;
+ /**
+ * Cookie path.
+ */
+ path: string;
+ /**
+ * Cookie expiration date as the number of seconds since the UNIX epoch. Set to `-1` for
+ * session cookies
+ */
+ expires: number;
+ /**
+ * Cookie size.
+ */
+ size: number;
+ /**
+ * True if cookie is http-only.
+ */
+ httpOnly: boolean;
+ /**
+ * True if cookie is secure.
+ */
+ secure: boolean;
+ /**
+ * True in case of session cookie.
+ */
+ session: boolean;
+ /**
+ * Cookie SameSite type.
+ */
+ sameSite?: CookieSameSite;
+ /**
+ * Cookie Priority. Supported only in Chrome.
+ */
+ priority?: CookiePriority;
+ /**
+ * True if cookie is SameParty. Supported only in Chrome.
+ */
+ sameParty?: boolean;
+ /**
+ * Cookie source scheme type. Supported only in Chrome.
+ */
+ sourceScheme?: CookieSourceScheme;
+ /**
+ * Cookie partition key. The site of the top-level URL the browser was visiting at the
+ * start of the request to the endpoint that set the cookie. Supported only in Chrome.
+ */
+ partitionKey?: string;
+ /**
+ * True if cookie partition key is opaque. Supported only in Chrome.
+ */
+ partitionKeyOpaque?: boolean;
+}
+
+/**
+ * Cookie parameter object
+ *
+ * @public
+ */
+export interface CookieParam {
+ /**
+ * Cookie name.
+ */
+ name: string;
+ /**
+ * Cookie value.
+ */
+ value: string;
+ /**
+ * The request-URI to associate with the setting of the cookie. This value can affect
+ * the default domain, path, and source scheme values of the created cookie.
+ */
+ url?: string;
+ /**
+ * Cookie domain.
+ */
+ domain?: string;
+ /**
+ * Cookie path.
+ */
+ path?: string;
+ /**
+ * True if cookie is secure.
+ */
+ secure?: boolean;
+ /**
+ * True if cookie is http-only.
+ */
+ httpOnly?: boolean;
+ /**
+ * Cookie SameSite type.
+ */
+ sameSite?: CookieSameSite;
+ /**
+ * Cookie expiration date, session cookie if not set
+ */
+ expires?: number;
+ /**
+ * Cookie Priority. Supported only in Chrome.
+ */
+ priority?: CookiePriority;
+ /**
+ * True if cookie is SameParty. Supported only in Chrome.
+ */
+ sameParty?: boolean;
+ /**
+ * Cookie source scheme type. Supported only in Chrome.
+ */
+ sourceScheme?: CookieSourceScheme;
+ /**
+ * Cookie partition key. The site of the top-level URL the browser was visiting at the
+ * start of the request to the endpoint that set the cookie. If not set, the cookie will
+ * be set as not partitioned.
+ */
+ partitionKey?: string;
+}
+
+/**
+ * @public
+ */
+export interface DeleteCookiesRequest {
+ /**
+ * Name of the cookies to remove.
+ */
+ name: string;
+ /**
+ * If specified, deletes all the cookies with the given name where domain and path match
+ * provided URL. Otherwise, deletes only cookies related to the current page's domain.
+ */
+ url?: string;
+ /**
+ * If specified, deletes only cookies with the exact domain.
+ */
+ domain?: string;
+ /**
+ * If specified, deletes only cookies with the exact path.
+ */
+ path?: string;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts
index dbf5c13c95..1f1a35dd0b 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts
@@ -1543,10 +1543,3 @@ for (const device of knownDevices) {
* @public
*/
export const KnownDevices = Object.freeze(knownDevicesByName);
-
-/**
- * @deprecated Import {@link KnownDevices}
- *
- * @public
- */
-export const devices = KnownDevices;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts
index 8225d64f07..4d0a43ea33 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts
@@ -5,11 +5,11 @@
*/
/**
- * @deprecated Do not use.
+ * The base class for all Puppeteer-specific errors
*
* @public
*/
-export class CustomError extends Error {
+export class PuppeteerError extends Error {
/**
* @internal
*/
@@ -36,14 +36,14 @@ export class CustomError extends Error {
*
* @public
*/
-export class TimeoutError extends CustomError {}
+export class TimeoutError extends PuppeteerError {}
/**
* ProtocolError is emitted whenever there is an error from the protocol.
*
* @public
*/
-export class ProtocolError extends CustomError {
+export class ProtocolError extends PuppeteerError {
#code?: number;
#originalMessage = '';
@@ -76,49 +76,9 @@ export class ProtocolError extends CustomError {
*
* @public
*/
-export class UnsupportedOperation extends CustomError {}
+export class UnsupportedOperation extends PuppeteerError {}
/**
* @internal
*/
export class TargetCloseError extends ProtocolError {}
-
-/**
- * @deprecated Do not use.
- *
- * @public
- */
-export interface PuppeteerErrors {
- TimeoutError: typeof TimeoutError;
- ProtocolError: typeof ProtocolError;
-}
-
-/**
- * @deprecated Import error classes directly.
- *
- * Puppeteer methods might throw errors if they are unable to fulfill a request.
- * For example, `page.waitForSelector(selector[, options])` might fail if the
- * selector doesn't match any nodes during the given timeframe.
- *
- * For certain types of errors Puppeteer uses specific error classes. These
- * classes are available via `puppeteer.errors`.
- *
- * @example
- * An example of handling a timeout error:
- *
- * ```ts
- * try {
- * await page.waitForSelector('.foo');
- * } catch (e) {
- * if (e instanceof TimeoutError) {
- * // Do something if this is a timeout.
- * }
- * }
- * ```
- *
- * @public
- */
-export const errors: PuppeteerErrors = Object.freeze({
- TimeoutError,
- ProtocolError,
-});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts
index cf05ef6700..f3875e99e8 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts
@@ -19,7 +19,7 @@ describe('EventEmitter', () => {
});
describe('on', () => {
- const onTests = (methodName: 'on' | 'addListener'): void => {
+ const onTests = (methodName: 'on'): void => {
it(`${methodName}: adds an event listener that is fired when the event is emitted`, () => {
const listener = sinon.spy();
emitter[methodName]('foo', listener);
@@ -43,12 +43,10 @@ describe('EventEmitter', () => {
});
};
onTests('on');
- // we support addListener for legacy reasons
- onTests('addListener');
});
describe('off', () => {
- const offTests = (methodName: 'off' | 'removeListener'): void => {
+ const offTests = (methodName: 'off'): void => {
it(`${methodName}: removes the listener so it is no longer called`, () => {
const listener = sinon.spy();
emitter.on('foo', listener);
@@ -67,8 +65,6 @@ describe('EventEmitter', () => {
});
};
offTests('off');
- // we support removeListener for legacy reasons
- offTests('removeListener');
});
describe('once', () => {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts
index 4a8bcb801f..0aace8b2eb 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts
@@ -27,18 +27,6 @@ export interface CommonEventEmitter<Events extends Record<EventType, unknown>> {
handler?: Handler<Events[Key]>
): this;
emit<Key extends keyof Events>(type: Key, event: Events[Key]): boolean;
- /* To maintain parity with the built in NodeJS event emitter which uses removeListener
- * rather than `off`.
- * If you're implementing new code you should use `off`.
- */
- addListener<Key extends keyof Events>(
- type: Key,
- handler: Handler<Events[Key]>
- ): this;
- removeListener<Key extends keyof Events>(
- type: Key,
- handler: Handler<Events[Key]>
- ): this;
once<Key extends keyof Events>(
type: Key,
handler: Handler<Events[Key]>
@@ -149,30 +137,6 @@ export class EventEmitter<Events extends Record<EventType, unknown>>
}
/**
- * Remove an event listener.
- *
- * @deprecated please use {@link EventEmitter.off} instead.
- */
- removeListener<Key extends keyof EventsWithWildcard<Events>>(
- type: Key,
- handler: Handler<EventsWithWildcard<Events>[Key]>
- ): this {
- return this.off(type, handler);
- }
-
- /**
- * Add an event listener.
- *
- * @deprecated please use {@link EventEmitter.on} instead.
- */
- addListener<Key extends keyof EventsWithWildcard<Events>>(
- type: Key,
- handler: Handler<EventsWithWildcard<Events>[Key]>
- ): this {
- return this.on(type, handler);
- }
-
- /**
* Like `on` but the listener will only be fired once and then it will be removed.
* @param type - the event you'd like to listen to
* @param handler - the handler function to run when the event occurs
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts
index 7cae9191a9..f87ec6817b 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts
@@ -158,11 +158,23 @@ export interface PDFOptions {
omitBackground?: boolean;
/**
* Generate tagged (accessible) PDF.
- * @defaultValue `false`
+ * @defaultValue `true`
* @experimental
*/
tagged?: boolean;
/**
+ * Generate document outline.
+ *
+ * @remarks
+ * If this is enabled the PDF will also be tagged (accessible)
+ * Currently only works in old Headless (headless = 'shell')
+ * crbug/840455#c47
+ *
+ * @defaultValue `false`
+ * @experimental
+ */
+ outline?: boolean;
+ /**
* Timeout in milliseconds. Pass `0` to disable timeout.
* @defaultValue `30_000`
*/
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts
index 0264c9175f..d505d6c5ff 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts
@@ -1,3 +1,8 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
import {source as injectedSource} from '../generated/injected.js';
/**
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts
index 6ef8925605..bf4274fcf1 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts
@@ -10,12 +10,12 @@ export * from './Configuration.js';
export * from './ConnectionTransport.js';
export * from './ConnectOptions.js';
export * from './ConsoleMessage.js';
+export * from './Cookie.js';
export * from './CustomQueryHandler.js';
export * from './Debug.js';
export * from './Device.js';
export * from './Errors.js';
export * from './EventEmitter.js';
-export * from './fetch.js';
export * from './FileChooser.js';
export * from './GetQueryHandler.js';
export * from './HandleIterator.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts
deleted file mode 100644
index 6c7a2b451c..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-/**
- * Gets the global version if we're in the browser, else loads the node-fetch module.
- *
- * @internal
- */
-export const getFetch = async (): Promise<typeof fetch> => {
- return (globalThis as any).fetch || (await import('cross-fetch')).fetch;
-};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts
index 2c8f76f664..f84453c612 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts
@@ -5,13 +5,19 @@
*/
import type FS from 'fs/promises';
-import type {Readable} from 'stream';
-import {map, NEVER, Observable, timer} from '../../third_party/rxjs/rxjs.js';
+import type {OperatorFunction} from '../../third_party/rxjs/rxjs.js';
+import {
+ filter,
+ from,
+ map,
+ mergeMap,
+ NEVER,
+ Observable,
+ timer,
+} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
-import {isNode} from '../environment.js';
import {assert} from '../util/assert.js';
-import {isErrorLike} from '../util/ErrorLike.js';
import {debug} from './Debug.js';
import {TimeoutError} from './Errors.js';
@@ -209,29 +215,39 @@ export async function importFSPromises(): Promise<typeof FS> {
* @internal
*/
export async function getReadableAsBuffer(
- readable: Readable,
+ readable: ReadableStream<Uint8Array>,
path?: string
): Promise<Buffer | null> {
- const buffers = [];
+ const buffers: Uint8Array[] = [];
+ const reader = readable.getReader();
if (path) {
const fs = await importFSPromises();
const fileHandle = await fs.open(path, 'w+');
try {
- for await (const chunk of readable) {
- buffers.push(chunk);
- await fileHandle.writeFile(chunk);
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ break;
+ }
+ buffers.push(value);
+ await fileHandle.writeFile(value);
}
} finally {
await fileHandle.close();
}
} else {
- for await (const chunk of readable) {
- buffers.push(chunk);
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ break;
+ }
+ buffers.push(value);
}
}
try {
return Buffer.concat(buffers);
} catch (error) {
+ debugError(error);
return null;
}
}
@@ -239,39 +255,34 @@ export async function getReadableAsBuffer(
/**
* @internal
*/
+
+/**
+ * @internal
+ */
export async function getReadableFromProtocolStream(
client: CDPSession,
handle: string
-): Promise<Readable> {
- // TODO: Once Node 18 becomes the lowest supported version, we can migrate to
- // ReadableStream.
- if (!isNode) {
- throw new Error('Cannot create a stream outside of Node.js environment.');
- }
+): Promise<ReadableStream<Uint8Array>> {
+ return new ReadableStream({
+ async pull(controller) {
+ function getUnit8Array(data: string, isBase64: boolean): Uint8Array {
+ if (isBase64) {
+ return Uint8Array.from(atob(data), m => {
+ return m.codePointAt(0)!;
+ });
+ }
+ const encoder = new TextEncoder();
+ return encoder.encode(data);
+ }
- const {Readable} = await import('stream');
+ const {data, base64Encoded, eof} = await client.send('IO.read', {
+ handle,
+ });
- let eof = false;
- return new Readable({
- async read(size: number) {
+ controller.enqueue(getUnit8Array(data, base64Encoded ?? false));
if (eof) {
- return;
- }
-
- try {
- const response = await client.send('IO.read', {handle, size});
- this.push(response.data, response.base64Encoded ? 'base64' : undefined);
- if (response.eof) {
- eof = true;
- await client.send('IO.close', {handle});
- this.push(null);
- }
- } catch (error) {
- if (isErrorLike(error)) {
- this.destroy(error);
- return;
- }
- throw error;
+ await client.send('IO.close', {handle});
+ controller.close();
}
},
});
@@ -349,7 +360,8 @@ export function parsePDFOptions(
pageRanges: '',
preferCSSPageSize: false,
omitBackground: false,
- tagged: false,
+ outline: false,
+ tagged: true,
};
let width = 8.5;
@@ -375,6 +387,11 @@ export function parsePDFOptions(
convertPrintParameterToInches(options.margin?.right, lengthUnit) || 0,
};
+ // Quirk https://bugs.chromium.org/p/chromium/issues/detail?id=840455#c44
+ if (options.outline) {
+ options.tagged = true;
+ }
+
return {
...defaults,
...options,
@@ -445,3 +462,21 @@ export function fromEmitterEvent<
};
});
}
+
+/**
+ * @internal
+ */
+export function filterAsync<T>(
+ predicate: (value: T) => boolean | PromiseLike<boolean>
+): OperatorFunction<T, T> {
+ return mergeMap<T, Observable<T>>((value): Observable<T> => {
+ return from(Promise.resolve(predicate(value))).pipe(
+ filter(isMatch => {
+ return isMatch;
+ }),
+ map(() => {
+ return value;
+ })
+ );
+ });
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts
index 34fe8f7748..cfc209ba57 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts
@@ -1,3 +1,8 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
const HIDDEN_VISIBILITY_VALUES = ['hidden', 'collapse'];
/**
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts
index 51d5a19983..0cec3de9ae 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts
@@ -36,25 +36,6 @@ export class ChromeLauncher extends ProductLauncher {
}
override launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> {
- const headless = options.headless ?? true;
- if (
- headless === true &&
- this.puppeteer.configuration.logLevel === 'warn' &&
- !Boolean(process.env['PUPPETEER_DISABLE_HEADLESS_WARNING'])
- ) {
- console.warn(
- [
- '\x1B[1m\x1B[43m\x1B[30m',
- 'Puppeteer old Headless deprecation warning:\x1B[0m\x1B[33m',
- ' In the near future `headless: true` will default to the new Headless mode',
- ' for Chrome instead of the old Headless implementation. For more',
- ' information, please see https://developer.chrome.com/articles/new-headless/.',
- ' Consider opting in early by passing `headless: "new"` to `puppeteer.launch()`',
- ' If you encounter any bugs, please report them to https://github.com/puppeteer/puppeteer/issues/new/choose.\x1B[0m\n',
- ].join('\n ')
- );
- }
-
if (
this.puppeteer.configuration.logLevel === 'warn' &&
process.platform === 'darwin' &&
@@ -231,6 +212,7 @@ export class ChromeLauncher extends ProductLauncher {
'--disable-sync',
'--enable-automation',
'--export-tagged-pdf',
+ '--generate-pdf-document-outline',
'--force-color-profile=srgb',
'--metrics-recording-only',
'--no-first-run',
@@ -253,7 +235,7 @@ export class ChromeLauncher extends ProductLauncher {
}
if (headless) {
chromeArguments.push(
- headless === 'new' ? '--headless=new' : '--headless',
+ headless === 'shell' ? '--headless' : '--headless=new',
'--hide-scrollbars',
'--mute-audio'
);
@@ -271,7 +253,7 @@ export class ChromeLauncher extends ProductLauncher {
override executablePath(
channel?: ChromeReleaseChannel,
- headless?: boolean | 'new'
+ headless?: boolean | 'shell'
): string {
if (channel) {
return computeSystemExecutablePath({
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts
index eb4f375fc7..1af09192ec 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts
@@ -43,12 +43,20 @@ export class FirefoxLauncher extends ProductLauncher {
return {
...extraPrefsFirefox,
...(protocol === 'webDriverBiDi'
- ? {}
+ ? {
+ // Only enable the WebDriver BiDi protocol
+ 'remote.active-protocols': 1,
+ }
: {
// Do not close the window when the last tab gets closed
'browser.tabs.closeWindowWithLastTab': false,
+ // Prevent various error message on the console
+ // jest-puppeteer asserts that no error message is emitted by the console
+ 'network.cookie.cookieBehavior': 0,
// Temporarily force disable BFCache in parent (https://bit.ly/bug-1732263)
'fission.bfcacheInParent': false,
+ // Only enable the CDP protocol
+ 'remote.active-protocols': 2,
}),
// Force all web content to use a single content process. TODO: remove
// this once Firefox supports mouse event dispatch from the main frame
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts
index 28e0b595df..d7717e45c5 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts
@@ -17,13 +17,18 @@ export interface BrowserLaunchArgumentOptions {
* Whether to run the browser in headless mode.
*
* @remarks
- * In the future `headless: true` will be equivalent to `headless: 'new'`.
- * You can read more about the change {@link https://developer.chrome.com/articles/new-headless/ | here}.
- * Consider opting in early by setting the value to `"new"`.
+ *
+ * - `true` launches the browser in the
+ * {@link https://developer.chrome.com/articles/new-headless/ | new headless}
+ * mode.
+ *
+ * - `'shell'` launches
+ * {@link https://developer.chrome.com/blog/chrome-headless-shell | shell}
+ * known as the old headless mode.
*
* @defaultValue `true`
*/
- headless?: boolean | 'new';
+ headless?: boolean | 'shell';
/**
* Path to a user data directory.
* {@link https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/user_data_dir.md | see the Chromium docs}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts
index ab3432cd3a..2da07e8f7c 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts
@@ -393,7 +393,7 @@ export abstract class ProductLauncher {
/**
* @internal
*/
- protected resolveExecutablePath(headless?: boolean | 'new'): string {
+ protected resolveExecutablePath(headless?: boolean | 'shell'): string {
let executablePath = this.puppeteer.configuration.executablePath;
if (executablePath) {
if (!existsSync(executablePath)) {
@@ -404,10 +404,10 @@ export abstract class ProductLauncher {
return executablePath;
}
- function productToBrowser(product?: Product, headless?: boolean | 'new') {
+ function productToBrowser(product?: Product, headless?: boolean | 'shell') {
switch (product) {
case 'chrome':
- if (headless === true) {
+ if (headless === 'shell') {
return InstalledBrowser.CHROMEHEADLESSSHELL;
}
return InstalledBrowser.CHROME;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts
index e50e09acdb..726ee24cbb 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts
@@ -223,7 +223,7 @@ export class PuppeteerNode extends Puppeteer {
* @internal
*/
get defaultDownloadPath(): string | undefined {
- return this.configuration.downloadPath ?? this.configuration.cacheDirectory;
+ return this.configuration.cacheDirectory;
}
/**
@@ -283,8 +283,7 @@ export class PuppeteerNode extends Puppeteer {
throw new Error('The current platform is not supported.');
}
- const cacheDir =
- this.configuration.downloadPath ?? this.configuration.cacheDirectory!;
+ const cacheDir = this.configuration.cacheDirectory!;
const installedBrowsers = await getInstalledBrowsers({
cacheDir,
});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts
index 37360204d8..c543cd9517 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts
@@ -8,7 +8,7 @@
* @internal
*/
export const PUPPETEER_REVISIONS = Object.freeze({
- chrome: '121.0.6167.85',
- 'chrome-headless-shell': '121.0.6167.85',
+ chrome: '122.0.6261.94',
+ 'chrome-headless-shell': '122.0.6261.94',
firefox: 'latest',
});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts
index 0dfb013bb3..6699ace36d 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts
@@ -1,3 +1,8 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
import {TimeoutError} from '../common/Errors.js';
/**
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts
index 9498bac306..f789837def 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts
@@ -1,3 +1,8 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
import {Deferred} from './Deferred.js';
import {disposeSymbol} from './disposable.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts
index 4cdaf15d5b..bc476b0153 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts
@@ -9,7 +9,9 @@ import {describe, it} from 'node:test';
import expect from 'expect';
import sinon from 'sinon';
-import {invokeAtMostOnceForArguments} from './decorators.js';
+import {EventEmitter} from '../common/EventEmitter.js';
+
+import {bubble, invokeAtMostOnceForArguments} from './decorators.js';
describe('decorators', function () {
describe('invokeAtMostOnceForArguments', () => {
@@ -76,4 +78,48 @@ describe('decorators', function () {
}).toThrow();
});
});
+
+ describe('bubble', () => {
+ it('should work', () => {
+ class Test extends EventEmitter<any> {
+ @bubble()
+ accessor field = new EventEmitter();
+ }
+
+ const t = new Test();
+ let a = false;
+ t.on('a', (value: boolean) => {
+ a = value;
+ });
+
+ t.field.emit('a', true);
+ expect(a).toBeTruthy();
+
+ // Set a new emitter.
+ t.field = new EventEmitter();
+ a = false;
+
+ t.field.emit('a', true);
+ expect(a).toBeTruthy();
+ });
+
+ it('should not bubble down', () => {
+ class Test extends EventEmitter<any> {
+ @bubble()
+ accessor field = new EventEmitter<any>();
+ }
+
+ const t = new Test();
+ let a = false;
+ t.field.on('a', (value: boolean) => {
+ a = value;
+ });
+
+ t.emit('a', true);
+ expect(a).toBeFalsy();
+
+ t.field.emit('a', true);
+ expect(a).toBeTruthy();
+ });
+ });
});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts
index af21c5fe29..c4dc3b6c0d 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import type {EventType} from '../common/EventEmitter.js';
+import type {EventEmitter} from '../common/EventEmitter.js';
import type {Disposed, Moveable} from '../common/types.js';
import {asyncDisposeSymbol, disposeSymbol} from './disposable.js';
@@ -138,3 +140,67 @@ export function guarded<T extends object>(
};
};
}
+
+const bubbleHandlers = new WeakMap<object, Map<any, any>>();
+
+/**
+ * Event emitter fields marked with `bubble` will have their events bubble up
+ * the field owner.
+ */
+// The type is too complicated to type.
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export function bubble<T extends EventType[]>(events?: T) {
+ return <This extends EventEmitter<any>, Value extends EventEmitter<any>>(
+ {set, get}: ClassAccessorDecoratorTarget<This, Value>,
+ context: ClassAccessorDecoratorContext<This, Value>
+ ): ClassAccessorDecoratorResult<This, Value> => {
+ context.addInitializer(function () {
+ const handlers = bubbleHandlers.get(this) ?? new Map();
+ if (handlers.has(events)) {
+ return;
+ }
+
+ const handler =
+ events !== undefined
+ ? (type: EventType, event: unknown) => {
+ if (events.includes(type)) {
+ this.emit(type, event);
+ }
+ }
+ : (type: EventType, event: unknown) => {
+ this.emit(type, event);
+ };
+
+ handlers.set(events, handler);
+ bubbleHandlers.set(this, handlers);
+ });
+ return {
+ set(emitter) {
+ const handler = bubbleHandlers.get(this)!.get(events)!;
+
+ // In case we are re-setting.
+ const oldEmitter = get.call(this);
+ if (oldEmitter !== undefined) {
+ oldEmitter.off('*', handler);
+ }
+
+ if (emitter === undefined) {
+ return;
+ }
+ emitter.on('*', handler);
+ set.call(this, emitter);
+ },
+ // @ts-expect-error -- TypeScript incorrectly types init to require a
+ // return.
+ init(emitter) {
+ if (emitter === undefined) {
+ return;
+ }
+ const handler = bubbleHandlers.get(this)!.get(events)!;
+
+ emitter.on('*', handler);
+ return emitter;
+ },
+ };
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts b/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts
index c20aaa8342..1d522c780b 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts
@@ -1,8 +1,3 @@
-/**
- * @license
- * Copyright 2022 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-
+// esline-disable rulesdir/check-license
export * from 'mitt';
export {default as default} from 'mitt';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts b/remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts
index b8b64788ae..6f4f844f5d 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts
@@ -1,13 +1,11 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
+// esline-disable rulesdir/check-license
export {
bufferCount,
catchError,
+ combineLatest,
concat,
concatMap,
+ debounceTime,
defaultIfEmpty,
defer,
delay,
@@ -16,6 +14,7 @@ export {
first,
firstValueFrom,
forkJoin,
+ delayWhen,
from,
fromEvent,
identity,
@@ -24,6 +23,7 @@ export {
map,
merge,
mergeMap,
+ mergeScan,
NEVER,
noop,
Observable,
@@ -31,9 +31,11 @@ export {
pipe,
race,
raceWith,
+ ReplaySubject,
retry,
startWith,
switchMap,
+ take,
takeUntil,
tap,
throwIfEmpty,
@@ -42,20 +44,3 @@ export {
} from 'rxjs';
export type * from 'rxjs';
-
-import {filter, from, map, mergeMap, type Observable} from 'rxjs';
-
-export function filterAsync<T>(
- predicate: (value: T) => boolean | PromiseLike<boolean>
-) {
- return mergeMap<T, Observable<T>>(value => {
- return from(Promise.resolve(predicate(value))).pipe(
- filter(isMatch => {
- return isMatch;
- }),
- map(() => {
- return value;
- })
- );
- });
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json
index 25c438c57d..cfe3a26f4c 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json
+++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json
@@ -3,6 +3,6 @@
"compilerOptions": {
"declarationMap": false,
"outDir": "../lib/esm/third_party",
- "sourceMap": false,
- },
+ "sourceMap": false
+ }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json b/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json
index b662532a01..a219f8b704 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json
+++ b/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json
@@ -3,6 +3,6 @@
"files": [],
"references": [
{"path": "src/tsconfig.esm.json"},
- {"path": "src/tsconfig.cjs.json"},
- ],
+ {"path": "src/tsconfig.cjs.json"}
+ ]
}