summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/browsers
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/browsers')
-rw-r--r--remote/test/puppeteer/packages/browsers/.mocharc.cjs8
-rw-r--r--remote/test/puppeteer/packages/browsers/CHANGELOG.md282
-rw-r--r--remote/test/puppeteer/packages/browsers/README.md28
-rw-r--r--remote/test/puppeteer/packages/browsers/api-extractor.docs.json15
-rw-r--r--remote/test/puppeteer/packages/browsers/api-extractor.json40
-rw-r--r--remote/test/puppeteer/packages/browsers/package.json113
-rw-r--r--remote/test/puppeteer/packages/browsers/src/CLI.ts401
-rw-r--r--remote/test/puppeteer/packages/browsers/src/Cache.ts211
-rw-r--r--remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts187
-rw-r--r--remote/test/puppeteer/packages/browsers/src/browser-data/chrome-headless-shell.ts69
-rw-r--r--remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts195
-rw-r--r--remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts56
-rw-r--r--remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts88
-rw-r--r--remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts330
-rw-r--r--remote/test/puppeteer/packages/browsers/src/browser-data/types.ts61
-rw-r--r--remote/test/puppeteer/packages/browsers/src/debug.ts9
-rw-r--r--remote/test/puppeteer/packages/browsers/src/detectPlatform.ts51
-rw-r--r--remote/test/puppeteer/packages/browsers/src/fileUtil.ts79
-rw-r--r--remote/test/puppeteer/packages/browsers/src/httpUtil.ts151
-rw-r--r--remote/test/puppeteer/packages/browsers/src/install.ts271
-rw-r--r--remote/test/puppeteer/packages/browsers/src/launch.ts479
-rw-r--r--remote/test/puppeteer/packages/browsers/src/main-cli.ts11
-rw-r--r--remote/test/puppeteer/packages/browsers/src/main.ts42
-rw-r--r--remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json8
-rw-r--r--remote/test/puppeteer/packages/browsers/src/tsconfig.esm.json6
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/chrome-headless-shell-data.spec.ts72
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/cli.spec.ts81
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/install.spec.ts93
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts119
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts94
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts233
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts122
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts71
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts81
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts93
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts62
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts122
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts87
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts97
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts75
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/firefox/launch.spec.ts92
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/mocha-utils.ts8
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/tsconfig.json9
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/tsdoc.json15
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/uninstall.spec.ts63
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/utils.ts75
-rw-r--r--remote/test/puppeteer/packages/browsers/test/src/versions.ts11
-rw-r--r--remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs75
-rw-r--r--remote/test/puppeteer/packages/browsers/tools/updateVersions.mjs43
-rw-r--r--remote/test/puppeteer/packages/browsers/tsconfig.json8
-rw-r--r--remote/test/puppeteer/packages/browsers/tsdoc.json15
51 files changed, 5107 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/browsers/.mocharc.cjs b/remote/test/puppeteer/packages/browsers/.mocharc.cjs
new file mode 100644
index 0000000000..50110ff654
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/.mocharc.cjs
@@ -0,0 +1,8 @@
+module.exports = {
+ logLevel: 'debug',
+ spec: 'test/build/**/*.spec.js',
+ require: ['./test/build/mocha-utils.js'],
+ exit: !!process.env.CI,
+ reporter: 'spec',
+ timeout: 10_000,
+};
diff --git a/remote/test/puppeteer/packages/browsers/CHANGELOG.md b/remote/test/puppeteer/packages/browsers/CHANGELOG.md
new file mode 100644
index 0000000000..abfb45bb6d
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/CHANGELOG.md
@@ -0,0 +1,282 @@
+# Changelog
+
+## [1.9.1](https://github.com/puppeteer/puppeteer/compare/browsers-v1.9.0...browsers-v1.9.1) (2024-01-04)
+
+
+### Bug Fixes
+
+* disable GFX sanity window for Firefox and enable WebDriver BiDi CI jobs for Windows ([#11578](https://github.com/puppeteer/puppeteer/issues/11578)) ([e41a265](https://github.com/puppeteer/puppeteer/commit/e41a2656d9e1f3f037b298457fbd6c6e08f5a371))
+
+## [1.9.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.8.0...browsers-v1.9.0) (2023-12-05)
+
+
+### Features
+
+* implement the Puppeteer CLI ([#11344](https://github.com/puppeteer/puppeteer/issues/11344)) ([53fb69b](https://github.com/puppeteer/puppeteer/commit/53fb69bf7f2bf06fa4fd7bb6d3cf21382386f6e7))
+
+
+### Bug Fixes
+
+* ng-schematics install Windows ([#11487](https://github.com/puppeteer/puppeteer/issues/11487)) ([02af748](https://github.com/puppeteer/puppeteer/commit/02af7482d9bf2163b90dfe623b0af18c513d5a3b))
+* remove CDP-specific preferences from defaults for Firefox ([#11477](https://github.com/puppeteer/puppeteer/issues/11477)) ([f8c9469](https://github.com/puppeteer/puppeteer/commit/f8c94699c7f5b15c7bb96f299c2c8217d74230cd))
+
+## [1.8.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.7.1...browsers-v1.8.0) (2023-10-20)
+
+
+### Features
+
+* enable tab targets ([#11099](https://github.com/puppeteer/puppeteer/issues/11099)) ([8324c16](https://github.com/puppeteer/puppeteer/commit/8324c1634883d97ed83f32a1e62acc9b5e64e0bd))
+
+## [1.7.1](https://github.com/puppeteer/puppeteer/compare/browsers-v1.7.0...browsers-v1.7.1) (2023-09-13)
+
+
+### Bug Fixes
+
+* use supported node range for types ([#10896](https://github.com/puppeteer/puppeteer/issues/10896)) ([2d851c1](https://github.com/puppeteer/puppeteer/commit/2d851c1398e5efcdabdb5304dc78e68cbd3fadd2))
+
+## [1.7.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.6.0...browsers-v1.7.0) (2023-08-18)
+
+
+### Features
+
+* support chrome-headless-shell ([#10739](https://github.com/puppeteer/puppeteer/issues/10739)) ([416843b](https://github.com/puppeteer/puppeteer/commit/416843ba68aaab7ae14bbc74c2ac705e877e91a7))
+
+## [1.6.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.5.1...browsers-v1.6.0) (2023-08-10)
+
+
+### Features
+
+* allow installing chrome/chromedriver by milestone and version prefix ([#10720](https://github.com/puppeteer/puppeteer/issues/10720)) ([bec2357](https://github.com/puppeteer/puppeteer/commit/bec2357aeedda42cfaf3096c6293c2f49ceb825e))
+
+## [1.5.1](https://github.com/puppeteer/puppeteer/compare/browsers-v1.5.0...browsers-v1.5.1) (2023-08-08)
+
+
+### Bug Fixes
+
+* add buildId to archive path ([#10699](https://github.com/puppeteer/puppeteer/issues/10699)) ([21461b0](https://github.com/puppeteer/puppeteer/commit/21461b02c65062f5ed240e8ea357e9b7f2d26b32))
+
+## [1.5.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.6...browsers-v1.5.0) (2023-08-02)
+
+
+### Features
+
+* add executablePath to InstalledBrowser ([#10594](https://github.com/puppeteer/puppeteer/issues/10594)) ([87522e7](https://github.com/puppeteer/puppeteer/commit/87522e778a6487111931458755e701f1c4b717d9))
+
+
+### Bug Fixes
+
+* clear pending TLS socket handle ([#10667](https://github.com/puppeteer/puppeteer/issues/10667)) ([87bd791](https://github.com/puppeteer/puppeteer/commit/87bd791ddc10c247bf154bbac2aa912327a4cf20))
+* remove typescript from peer dependencies ([#10593](https://github.com/puppeteer/puppeteer/issues/10593)) ([c60572a](https://github.com/puppeteer/puppeteer/commit/c60572a1ca36ea5946d287bd629ac31798d84cb0))
+
+## [1.4.6](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.5...browsers-v1.4.6) (2023-07-20)
+
+
+### Bug Fixes
+
+* restore proxy-agent ([#10569](https://github.com/puppeteer/puppeteer/issues/10569)) ([bf6304e](https://github.com/puppeteer/puppeteer/commit/bf6304e064eb52d39d7f993f1ea868da06f7f006))
+
+## [1.4.5](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.4...browsers-v1.4.5) (2023-07-13)
+
+
+### Bug Fixes
+
+* stop relying on vm2 (via proxy agent) ([#10548](https://github.com/puppeteer/puppeteer/issues/10548)) ([4070cd6](https://github.com/puppeteer/puppeteer/commit/4070cd68b6d01fb9a1643da2662ce0b6f53cf37d))
+
+## [1.4.4](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.3...browsers-v1.4.4) (2023-07-11)
+
+
+### Bug Fixes
+
+* correctly parse the default buildId ([#10535](https://github.com/puppeteer/puppeteer/issues/10535)) ([c308266](https://github.com/puppeteer/puppeteer/commit/c3082661113b4b55534f25da86e3b261d3952953))
+* remove Chromium channels ([#10536](https://github.com/puppeteer/puppeteer/issues/10536)) ([c0dc8ad](https://github.com/puppeteer/puppeteer/commit/c0dc8ad8a82446752e29f98d8eee617b9a67c942))
+
+## [1.4.3](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.2...browsers-v1.4.3) (2023-06-29)
+
+
+### Bug Fixes
+
+* negative timeout doesn't break launch ([#10480](https://github.com/puppeteer/puppeteer/issues/10480)) ([6a89a2a](https://github.com/puppeteer/puppeteer/commit/6a89a2aadcaf683fe57f1e0e13886f1fa937e194))
+
+## [1.4.2](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.1...browsers-v1.4.2) (2023-06-20)
+
+
+### Bug Fixes
+
+* include src into published package ([#10415](https://github.com/puppeteer/puppeteer/issues/10415)) ([d1ffad0](https://github.com/puppeteer/puppeteer/commit/d1ffad059ae66104842b92dc814d362c123b9646))
+
+## [1.4.1](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.0...browsers-v1.4.1) (2023-05-31)
+
+
+### Bug Fixes
+
+* pass on the auth from the download URL ([#10271](https://github.com/puppeteer/puppeteer/issues/10271)) ([3a1f4f0](https://github.com/puppeteer/puppeteer/commit/3a1f4f0f8f5fe4e20c4ed69f5485a827a841cf54))
+
+## [1.4.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.3.0...browsers-v1.4.0) (2023-05-24)
+
+
+### Features
+
+* use proxy-agent to support various proxies ([#10227](https://github.com/puppeteer/puppeteer/issues/10227)) ([2c0bd54](https://github.com/puppeteer/puppeteer/commit/2c0bd54d2e3b778818b9b4b32f436778f571b918))
+
+## [1.3.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.2.0...browsers-v1.3.0) (2023-05-15)
+
+
+### Features
+
+* add ability to uninstall a browser ([#10179](https://github.com/puppeteer/puppeteer/issues/10179)) ([d388a6e](https://github.com/puppeteer/puppeteer/commit/d388a6edfd164548b008cb0d8e9cb5c0d03cdcda))
+
+
+### Bug Fixes
+
+* update the command name ([#10178](https://github.com/puppeteer/puppeteer/issues/10178)) ([ccbb82d](https://github.com/puppeteer/puppeteer/commit/ccbb82d9cd5b77f8262c143a5663fc1f9938a8c4))
+
+## [1.2.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.1.0...browsers-v1.2.0) (2023-05-11)
+
+
+### Features
+
+* support Chrome channels for ChromeDriver ([#10158](https://github.com/puppeteer/puppeteer/issues/10158)) ([e313b05](https://github.com/puppeteer/puppeteer/commit/e313b054e658887e2c062ea55d8ee99f3f4f3789))
+
+## [1.1.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.0.1...browsers-v1.1.0) (2023-05-08)
+
+
+### Features
+
+* support stable/dev/beta/canary keywords for chrome and chromium ([#10140](https://github.com/puppeteer/puppeteer/issues/10140)) ([90ed263](https://github.com/puppeteer/puppeteer/commit/90ed263eafb0ca0420ea1918d7c1f326eaa58e20))
+
+## [1.0.1](https://github.com/puppeteer/puppeteer/compare/browsers-v1.0.0...browsers-v1.0.1) (2023-05-05)
+
+
+### Bug Fixes
+
+* rename PUPPETEER_DOWNLOAD_HOST to PUPPETEER_DOWNLOAD_BASE_URL ([#10130](https://github.com/puppeteer/puppeteer/issues/10130)) ([9758cae](https://github.com/puppeteer/puppeteer/commit/9758cae029f90908c4b5340561d9c51c26aa2f21))
+
+## [1.0.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.5.0...browsers-v1.0.0) (2023-05-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019))
+* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054))
+
+### Features
+
+* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) ([7405d65](https://github.com/puppeteer/puppeteer/commit/7405d6585aa09b240fbab09aa360674d4442b3d9))
+* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054)) ([df4d60c](https://github.com/puppeteer/puppeteer/commit/df4d60c187aa11c4ad783827242e9511f4ec2aab))
+
+
+### Bug Fixes
+
+* add Host header when used with http_proxy ([#10080](https://github.com/puppeteer/puppeteer/issues/10080)) ([edbfff7](https://github.com/puppeteer/puppeteer/commit/edbfff7b04baffc29c01c37c595d6b3355c0dea0))
+
+## [0.5.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.4.1...browsers-v0.5.0) (2023-04-21)
+
+
+### Features
+
+* **browser:** add a method to get installed browsers ([#10057](https://github.com/puppeteer/puppeteer/issues/10057)) ([e16e2a9](https://github.com/puppeteer/puppeteer/commit/e16e2a97284f5e7ab4073f375254572a6a89e800))
+
+## [0.4.1](https://github.com/puppeteer/puppeteer/compare/browsers-v0.4.0...browsers-v0.4.1) (2023-04-13)
+
+
+### Bug Fixes
+
+* report install errors properly ([#10016](https://github.com/puppeteer/puppeteer/issues/10016)) ([7381229](https://github.com/puppeteer/puppeteer/commit/7381229a164e598e7523862f2438cd0cd1cd796a))
+
+## [0.4.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.3.3...browsers-v0.4.0) (2023-04-06)
+
+
+### Features
+
+* **browsers:** support downloading chromedriver ([#9990](https://github.com/puppeteer/puppeteer/issues/9990)) ([ef0fb5d](https://github.com/puppeteer/puppeteer/commit/ef0fb5d87299c604af2387ac1c72be317c50316d))
+
+## [0.3.3](https://github.com/puppeteer/puppeteer/compare/browsers-v0.3.2...browsers-v0.3.3) (2023-04-06)
+
+
+### Bug Fixes
+
+* **browsers:** update package json ([#9968](https://github.com/puppeteer/puppeteer/issues/9968)) ([817288c](https://github.com/puppeteer/puppeteer/commit/817288cd901121ddc8a44226eda689bb784cee61))
+* **browsers:** various fixes and improvements ([#9966](https://github.com/puppeteer/puppeteer/issues/9966)) ([f1211cb](https://github.com/puppeteer/puppeteer/commit/f1211cbec091ec669de019aeb7fb4f011a81c1d7))
+* consider downloadHost as baseUrl ([#9973](https://github.com/puppeteer/puppeteer/issues/9973)) ([05a44af](https://github.com/puppeteer/puppeteer/commit/05a44afe5affcac9fe0f0a2e83f17807c99b2f0c))
+
+## [0.3.2](https://github.com/puppeteer/puppeteer/compare/browsers-v0.3.1...browsers-v0.3.2) (2023-04-03)
+
+
+### Bug Fixes
+
+* typo in the browsers package ([#9957](https://github.com/puppeteer/puppeteer/issues/9957)) ([c780384](https://github.com/puppeteer/puppeteer/commit/c7803844cf10b6edaa2da83134029b7acf5b45b2))
+
+## [0.3.1](https://github.com/puppeteer/puppeteer/compare/browsers-v0.3.0...browsers-v0.3.1) (2023-03-29)
+
+
+### Bug Fixes
+
+* bump @puppeteer/browsers ([#9938](https://github.com/puppeteer/puppeteer/issues/9938)) ([2a29d30](https://github.com/puppeteer/puppeteer/commit/2a29d30d1790b47c99f8d196b3844364d351acbd))
+
+## [0.3.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.2.0...browsers-v0.3.0) (2023-03-27)
+
+
+### Features
+
+* update Chrome browser binaries ([#9917](https://github.com/puppeteer/puppeteer/issues/9917)) ([fcb233c](https://github.com/puppeteer/puppeteer/commit/fcb233ce949f5f716aee39253e910104b04aa000))
+
+## [0.2.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.1.1...browsers-v0.2.0) (2023-03-24)
+
+
+### Features
+
+* implement a command to clear the cache ([#9868](https://github.com/puppeteer/puppeteer/issues/9868)) ([b8d38cb](https://github.com/puppeteer/puppeteer/commit/b8d38cb05f7eedf554ed46f2f7428b621197d1cc))
+
+## [0.1.1](https://github.com/puppeteer/puppeteer/compare/browsers-v0.1.0...browsers-v0.1.1) (2023-03-14)
+
+
+### Bug Fixes
+
+* export ChromeReleaseChannel ([#9851](https://github.com/puppeteer/puppeteer/issues/9851)) ([3e7a514](https://github.com/puppeteer/puppeteer/commit/3e7a514e556ddb4306aa3c15f24c512beaac65f4))
+
+## [0.1.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.5...browsers-v0.1.0) (2023-03-14)
+
+
+### Features
+
+* implement system channels for chrome in browsers ([#9844](https://github.com/puppeteer/puppeteer/issues/9844)) ([dec48a9](https://github.com/puppeteer/puppeteer/commit/dec48a95923e21a054c1d70d22c14001a0150293))
+
+
+### Bug Fixes
+
+* add browsers entry point ([#9846](https://github.com/puppeteer/puppeteer/issues/9846)) ([1a1e79d](https://github.com/puppeteer/puppeteer/commit/1a1e79d046ccad6fe843aa219501c17da08bc498))
+
+## [0.0.5](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.4...browsers-v0.0.5) (2023-03-07)
+
+
+### Bug Fixes
+
+* change the install output to include the executable path ([#9797](https://github.com/puppeteer/puppeteer/issues/9797)) ([8cca7bb](https://github.com/puppeteer/puppeteer/commit/8cca7bb7a2a1cdf62919d9c7eca62d6774e698db))
+
+## [0.0.4](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.3...browsers-v0.0.4) (2023-03-06)
+
+
+### Features
+
+* browsers: recognize chromium as a valid browser ([#9760](https://github.com/puppeteer/puppeteer/issues/9760)) ([04247a4](https://github.com/puppeteer/puppeteer/commit/04247a4e00b43683977bd8aa309d493eee663735))
+
+## [0.0.3](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.2...browsers-v0.0.3) (2023-02-22)
+
+
+### Bug Fixes
+
+* define options per command ([#9733](https://github.com/puppeteer/puppeteer/issues/9733)) ([8bae054](https://github.com/puppeteer/puppeteer/commit/8bae0545b7321d398dae3f522952dd981111587e))
+
+## [0.0.2](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.1...browsers-v0.0.2) (2023-02-22)
+
+
+### Bug Fixes
+
+* permissions for the browser CLI ([#9731](https://github.com/puppeteer/puppeteer/issues/9731)) ([e944931](https://github.com/puppeteer/puppeteer/commit/e944931de22726f35c5c83052892f8ab4667b035))
+
+## 0.0.1 (2023-02-22)
+
+
+### Features
+
+* initial release of browsers ([#9722](https://github.com/puppeteer/puppeteer/issues/9722)) ([#9727](https://github.com/puppeteer/puppeteer/issues/9727)) ([86a2d1d](https://github.com/puppeteer/puppeteer/commit/86a2d1dd3b2c024b886c6280e08a2d7dc8caabc5))
diff --git a/remote/test/puppeteer/packages/browsers/README.md b/remote/test/puppeteer/packages/browsers/README.md
new file mode 100644
index 0000000000..f5342126c6
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/README.md
@@ -0,0 +1,28 @@
+# @puppeteer/browsers
+
+Manage and launch browsers/drivers from a CLI or programmatically.
+
+## CLI
+
+Use `npx` to run the CLI:
+
+```bash
+npx @puppeteer/browsers --help
+```
+
+CLI help will provide all documentation you need to use the CLI.
+
+```bash
+npx @puppeteer/browsers --help # help for all commands
+npx @puppeteer/browsers install --help # help for the install command
+npx @puppeteer/browsers launch --help # help for the launch command
+```
+
+## Known limitations
+
+1. We support installing and running Firefox, Chrome and Chromium. The `latest`, `beta`, `dev`, `canary`, `stable` keywords are only supported for the install command. For the `launch` command you need to specify an exact build ID. The build ID is provided by the `install` command (see `npx @puppeteer/browsers install --help` for the format).
+2. Launching the system browsers is only possible for Chrome/Chromium.
+
+## API
+
+The programmatic API allows installing and launching browsers from your code. See the `test` folder for examples on how to use the `install`, `canInstall`, `launch`, `computeExecutablePath`, `computeSystemExecutablePath` and other methods.
diff --git a/remote/test/puppeteer/packages/browsers/api-extractor.docs.json b/remote/test/puppeteer/packages/browsers/api-extractor.docs.json
new file mode 100644
index 0000000000..6a41a3b59c
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/api-extractor.docs.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "mainEntryPointFilePath": "<projectFolder>/lib/esm/main.d.ts",
+
+ "extends": "./api-extractor.json",
+
+ "dtsRollup": {
+ "enabled": false
+ },
+
+ "docModel": {
+ "enabled": true,
+ "apiJsonFilePath": "<projectFolder>/../../docs/<unscopedPackageName>.api.json"
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/api-extractor.json b/remote/test/puppeteer/packages/browsers/api-extractor.json
new file mode 100644
index 0000000000..da1caae622
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/api-extractor.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "mainEntryPointFilePath": "<projectFolder>/lib/esm/main.d.ts",
+ "bundledPackages": [],
+
+ "apiReport": {
+ "enabled": false
+ },
+
+ "docModel": {
+ "enabled": false
+ },
+
+ "tsdocMetadata": {
+ "enabled": false
+ },
+
+ "messages": {
+ "compilerMessageReporting": {
+ "default": {
+ "logLevel": "warning"
+ }
+ },
+
+ "extractorMessageReporting": {
+ "ae-internal-missing-underscore": {
+ "logLevel": "none"
+ },
+ "default": {
+ "logLevel": "warning"
+ }
+ },
+
+ "tsdocMessageReporting": {
+ "default": {
+ "logLevel": "warning"
+ }
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/package.json b/remote/test/puppeteer/packages/browsers/package.json
new file mode 100644
index 0000000000..45de79abb8
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/package.json
@@ -0,0 +1,113 @@
+{
+ "name": "@puppeteer/browsers",
+ "version": "1.9.1",
+ "description": "Download and launch browsers",
+ "scripts": {
+ "build:docs": "wireit",
+ "build": "wireit",
+ "build:test": "wireit",
+ "clean": "../../tools/clean.js",
+ "test": "wireit"
+ },
+ "type": "commonjs",
+ "bin": "lib/cjs/main-cli.js",
+ "main": "./lib/cjs/main.js",
+ "exports": {
+ "import": "./lib/esm/main.js",
+ "require": "./lib/cjs/main.js"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b && tsx ../../tools/chmod.ts 755 lib/cjs/main-cli.js lib/esm/main-cli.js",
+ "files": [
+ "src/**/*.ts",
+ "tsconfig.json"
+ ],
+ "clean": "if-file-deleted",
+ "output": [
+ "lib/**",
+ "!lib/esm/package.json"
+ ],
+ "dependencies": [
+ "generate:package-json"
+ ]
+ },
+ "generate:package-json": {
+ "command": "tsx ../../tools/generate_module_package_json.ts lib/esm/package.json",
+ "files": [
+ "../../tools/generate_module_package_json.ts"
+ ],
+ "output": [
+ "lib/esm/package.json"
+ ]
+ },
+ "build:docs": {
+ "command": "api-extractor run --local --config \"./api-extractor.docs.json\"",
+ "files": [
+ "api-extractor.docs.json",
+ "lib/esm/main.d.ts",
+ "tsconfig.json"
+ ],
+ "dependencies": [
+ "build"
+ ]
+ },
+ "build:test": {
+ "command": "tsc -b test/src/tsconfig.json",
+ "files": [
+ "test/**/*.ts",
+ "test/src/tsconfig.json"
+ ],
+ "output": [
+ "test/build/**"
+ ],
+ "dependencies": [
+ "build",
+ "../testserver:build"
+ ]
+ },
+ "test": {
+ "command": "node tools/downloadTestBrowsers.mjs && mocha",
+ "files": [
+ ".mocharc.cjs"
+ ],
+ "dependencies": [
+ "build:test"
+ ]
+ }
+ },
+ "keywords": [
+ "puppeteer",
+ "browsers"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/browsers"
+ },
+ "author": "The Chromium Authors",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.3.0"
+ },
+ "files": [
+ "lib",
+ "src",
+ "!*.tsbuildinfo"
+ ],
+ "dependencies": {
+ "debug": "4.3.4",
+ "extract-zip": "2.0.1",
+ "progress": "2.0.3",
+ "proxy-agent": "6.3.1",
+ "tar-fs": "3.0.4",
+ "unbzip2-stream": "1.4.3",
+ "yargs": "17.7.2"
+ },
+ "devDependencies": {
+ "@types/debug": "4.1.12",
+ "@types/progress": "2.0.7",
+ "@types/tar-fs": "2.0.4",
+ "@types/unbzip2-stream": "1.4.3",
+ "@types/yargs": "17.0.32"
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/CLI.ts b/remote/test/puppeteer/packages/browsers/src/CLI.ts
new file mode 100644
index 0000000000..255f5545b4
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/CLI.ts
@@ -0,0 +1,401 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {stdin as input, stdout as output} from 'process';
+import * as readline from 'readline';
+
+import ProgressBar from 'progress';
+import type * as Yargs from 'yargs';
+import {hideBin} from 'yargs/helpers';
+import yargs from 'yargs/yargs';
+
+import {
+ resolveBuildId,
+ type Browser,
+ BrowserPlatform,
+ type ChromeReleaseChannel,
+} from './browser-data/browser-data.js';
+import {Cache} from './Cache.js';
+import {detectBrowserPlatform} from './detectPlatform.js';
+import {install} from './install.js';
+import {
+ computeExecutablePath,
+ computeSystemExecutablePath,
+ launch,
+} from './launch.js';
+
+interface InstallArgs {
+ browser: {
+ name: Browser;
+ buildId: string;
+ };
+ path?: string;
+ platform?: BrowserPlatform;
+ baseUrl?: string;
+}
+
+interface LaunchArgs {
+ browser: {
+ name: Browser;
+ buildId: string;
+ };
+ path?: string;
+ platform?: BrowserPlatform;
+ detached: boolean;
+ system: boolean;
+}
+
+interface ClearArgs {
+ path?: string;
+}
+
+/**
+ * @public
+ */
+export class CLI {
+ #cachePath;
+ #rl?: readline.Interface;
+ #scriptName = '';
+ #allowCachePathOverride = true;
+ #pinnedBrowsers?: Partial<{[key in Browser]: string}>;
+ #prefixCommand?: {cmd: string; description: string};
+
+ constructor(
+ opts?:
+ | string
+ | {
+ cachePath?: string;
+ scriptName?: string;
+ prefixCommand?: {cmd: string; description: string};
+ allowCachePathOverride?: boolean;
+ pinnedBrowsers?: Partial<{[key in Browser]: string}>;
+ },
+ rl?: readline.Interface
+ ) {
+ if (!opts) {
+ opts = {};
+ }
+ if (typeof opts === 'string') {
+ opts = {
+ cachePath: opts,
+ };
+ }
+ this.#cachePath = opts.cachePath ?? process.cwd();
+ this.#rl = rl;
+ this.#scriptName = opts.scriptName ?? '@puppeteer/browsers';
+ this.#allowCachePathOverride = opts.allowCachePathOverride ?? true;
+ this.#pinnedBrowsers = opts.pinnedBrowsers;
+ this.#prefixCommand = opts.prefixCommand;
+ }
+
+ #defineBrowserParameter(yargs: Yargs.Argv<unknown>): void {
+ yargs.positional('browser', {
+ description:
+ 'Which browser to install <browser>[@<buildId|latest>]. `latest` will try to find the latest available build. `buildId` is a browser-specific identifier such as a version or a revision.',
+ type: 'string',
+ coerce: (opt): InstallArgs['browser'] => {
+ return {
+ name: this.#parseBrowser(opt),
+ buildId: this.#parseBuildId(opt),
+ };
+ },
+ });
+ }
+
+ #definePlatformParameter(yargs: Yargs.Argv<unknown>): void {
+ yargs.option('platform', {
+ type: 'string',
+ desc: 'Platform that the binary needs to be compatible with.',
+ choices: Object.values(BrowserPlatform),
+ defaultDescription: 'Auto-detected',
+ });
+ }
+
+ #definePathParameter(yargs: Yargs.Argv<unknown>, required = false): void {
+ if (!this.#allowCachePathOverride) {
+ return;
+ }
+ yargs.option('path', {
+ type: 'string',
+ desc: 'Path to the root folder for the browser downloads and installation. The installation folder structure is compatible with the cache structure used by Puppeteer.',
+ defaultDescription: 'Current working directory',
+ ...(required ? {} : {default: process.cwd()}),
+ });
+ if (required) {
+ yargs.demandOption('path');
+ }
+ }
+
+ async run(argv: string[]): Promise<void> {
+ const yargsInstance = yargs(hideBin(argv));
+ let target = yargsInstance.scriptName(this.#scriptName);
+ if (this.#prefixCommand) {
+ target = target.command(
+ this.#prefixCommand.cmd,
+ this.#prefixCommand.description,
+ yargs => {
+ return this.#build(yargs);
+ }
+ );
+ } else {
+ target = this.#build(target);
+ }
+ await target
+ .demandCommand(1)
+ .help()
+ .wrap(Math.min(120, yargsInstance.terminalWidth()))
+ .parse();
+ }
+
+ #build(yargs: Yargs.Argv<unknown>): Yargs.Argv<unknown> {
+ const latestOrPinned = this.#pinnedBrowsers ? 'pinned' : 'latest';
+ return yargs
+ .command(
+ 'install <browser>',
+ 'Download and install the specified browser. If successful, the command outputs the actual browser buildId that was installed and the absolute path to the browser executable (format: <browser>@<buildID> <path>).',
+ yargs => {
+ this.#defineBrowserParameter(yargs);
+ this.#definePlatformParameter(yargs);
+ this.#definePathParameter(yargs);
+ yargs.option('base-url', {
+ type: 'string',
+ desc: 'Base URL to download from',
+ });
+ yargs.example(
+ '$0 install chrome',
+ `Install the ${latestOrPinned} available build of the Chrome browser.`
+ );
+ yargs.example(
+ '$0 install chrome@latest',
+ 'Install the latest available build for the Chrome browser.'
+ );
+ yargs.example(
+ '$0 install chrome@canary',
+ 'Install the latest available build for the Chrome Canary browser.'
+ );
+ yargs.example(
+ '$0 install chrome@115',
+ 'Install the latest available build for Chrome 115.'
+ );
+ yargs.example(
+ '$0 install chromedriver@canary',
+ 'Install the latest available build for ChromeDriver Canary.'
+ );
+ yargs.example(
+ '$0 install chromedriver@115',
+ 'Install the latest available build for ChromeDriver 115.'
+ );
+ yargs.example(
+ '$0 install chromedriver@115.0.5790',
+ 'Install the latest available patch (115.0.5790.X) build for ChromeDriver.'
+ );
+ yargs.example(
+ '$0 install chrome-headless-shell',
+ 'Install the latest available chrome-headless-shell build.'
+ );
+ yargs.example(
+ '$0 install chrome-headless-shell@beta',
+ 'Install the latest available chrome-headless-shell build corresponding to the Beta channel.'
+ );
+ yargs.example(
+ '$0 install chrome-headless-shell@118',
+ 'Install the latest available chrome-headless-shell 118 build.'
+ );
+ yargs.example(
+ '$0 install chromium@1083080',
+ 'Install the revision 1083080 of the Chromium browser.'
+ );
+ yargs.example(
+ '$0 install firefox',
+ 'Install the latest available build of the Firefox browser.'
+ );
+ yargs.example(
+ '$0 install firefox --platform mac',
+ 'Install the latest Mac (Intel) build of the Firefox browser.'
+ );
+ if (this.#allowCachePathOverride) {
+ yargs.example(
+ '$0 install firefox --path /tmp/my-browser-cache',
+ 'Install to the specified cache directory.'
+ );
+ }
+ },
+ async argv => {
+ const args = argv as unknown as InstallArgs;
+ args.platform ??= detectBrowserPlatform();
+ if (!args.platform) {
+ throw new Error(`Could not resolve the current platform`);
+ }
+ if (args.browser.buildId === 'pinned') {
+ const pinnedVersion = this.#pinnedBrowsers?.[args.browser.name];
+ if (!pinnedVersion) {
+ throw new Error(
+ `No pinned version found for ${args.browser.name}`
+ );
+ }
+ args.browser.buildId = pinnedVersion;
+ }
+ args.browser.buildId = await resolveBuildId(
+ args.browser.name,
+ args.platform,
+ args.browser.buildId
+ );
+ await install({
+ browser: args.browser.name,
+ buildId: args.browser.buildId,
+ platform: args.platform,
+ cacheDir: args.path ?? this.#cachePath,
+ downloadProgressCallback: makeProgressCallback(
+ args.browser.name,
+ args.browser.buildId
+ ),
+ baseUrl: args.baseUrl,
+ });
+ console.log(
+ `${args.browser.name}@${
+ args.browser.buildId
+ } ${computeExecutablePath({
+ browser: args.browser.name,
+ buildId: args.browser.buildId,
+ cacheDir: args.path ?? this.#cachePath,
+ platform: args.platform,
+ })}`
+ );
+ }
+ )
+ .command(
+ 'launch <browser>',
+ 'Launch the specified browser',
+ yargs => {
+ this.#defineBrowserParameter(yargs);
+ this.#definePlatformParameter(yargs);
+ this.#definePathParameter(yargs);
+ yargs.option('detached', {
+ type: 'boolean',
+ desc: 'Detach the child process.',
+ default: false,
+ });
+ yargs.option('system', {
+ type: 'boolean',
+ desc: 'Search for a browser installed on the system instead of the cache folder.',
+ default: false,
+ });
+ yargs.example(
+ '$0 launch chrome@115.0.5790.170',
+ 'Launch Chrome 115.0.5790.170'
+ );
+ yargs.example(
+ '$0 launch firefox@112.0a1',
+ 'Launch the Firefox browser identified by the milestone 112.0a1.'
+ );
+ yargs.example(
+ '$0 launch chrome@115.0.5790.170 --detached',
+ 'Launch the browser but detach the sub-processes.'
+ );
+ yargs.example(
+ '$0 launch chrome@canary --system',
+ 'Try to locate the Canary build of Chrome installed on the system and launch it.'
+ );
+ },
+ async argv => {
+ const args = argv as unknown as LaunchArgs;
+ const executablePath = args.system
+ ? computeSystemExecutablePath({
+ browser: args.browser.name,
+ // TODO: throw an error if not a ChromeReleaseChannel is provided.
+ channel: args.browser.buildId as ChromeReleaseChannel,
+ platform: args.platform,
+ })
+ : computeExecutablePath({
+ browser: args.browser.name,
+ buildId: args.browser.buildId,
+ cacheDir: args.path ?? this.#cachePath,
+ platform: args.platform,
+ });
+ launch({
+ executablePath,
+ detached: args.detached,
+ });
+ }
+ )
+ .command(
+ 'clear',
+ this.#allowCachePathOverride
+ ? 'Removes all installed browsers from the specified cache directory'
+ : `Removes all installed browsers from ${this.#cachePath}`,
+ yargs => {
+ this.#definePathParameter(yargs, true);
+ },
+ async argv => {
+ const args = argv as unknown as ClearArgs;
+ const cacheDir = args.path ?? this.#cachePath;
+ const rl = this.#rl ?? readline.createInterface({input, output});
+ rl.question(
+ `Do you want to permanently and recursively delete the content of ${cacheDir} (yes/No)? `,
+ answer => {
+ rl.close();
+ if (!['y', 'yes'].includes(answer.toLowerCase().trim())) {
+ console.log('Cancelled.');
+ return;
+ }
+ const cache = new Cache(cacheDir);
+ cache.clear();
+ console.log(`${cacheDir} cleared.`);
+ }
+ );
+ }
+ )
+ .demandCommand(1)
+ .help();
+ }
+
+ #parseBrowser(version: string): Browser {
+ return version.split('@').shift() as Browser;
+ }
+
+ #parseBuildId(version: string): string {
+ const parts = version.split('@');
+ return parts.length === 2
+ ? parts[1]!
+ : this.#pinnedBrowsers
+ ? 'pinned'
+ : 'latest';
+ }
+}
+
+/**
+ * @public
+ */
+export function makeProgressCallback(
+ browser: Browser,
+ buildId: string
+): (downloadedBytes: number, totalBytes: number) => void {
+ let progressBar: ProgressBar;
+ let lastDownloadedBytes = 0;
+ return (downloadedBytes: number, totalBytes: number) => {
+ if (!progressBar) {
+ progressBar = new ProgressBar(
+ `Downloading ${browser} r${buildId} - ${toMegabytes(
+ totalBytes
+ )} [:bar] :percent :etas `,
+ {
+ complete: '=',
+ incomplete: ' ',
+ width: 20,
+ total: totalBytes,
+ }
+ );
+ }
+ const delta = downloadedBytes - lastDownloadedBytes;
+ lastDownloadedBytes = downloadedBytes;
+ progressBar.tick(delta);
+ };
+}
+
+function toMegabytes(bytes: number) {
+ const mb = bytes / 1000 / 1000;
+ return `${Math.round(mb * 10) / 10} MB`;
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/Cache.ts b/remote/test/puppeteer/packages/browsers/src/Cache.ts
new file mode 100644
index 0000000000..13b465835a
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/Cache.ts
@@ -0,0 +1,211 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ Browser,
+ type BrowserPlatform,
+ executablePathByBrowser,
+} from './browser-data/browser-data.js';
+import {detectBrowserPlatform} from './detectPlatform.js';
+
+/**
+ * @public
+ */
+export class InstalledBrowser {
+ browser: Browser;
+ buildId: string;
+ platform: BrowserPlatform;
+ readonly executablePath: string;
+
+ #cache: Cache;
+
+ /**
+ * @internal
+ */
+ constructor(
+ cache: Cache,
+ browser: Browser,
+ buildId: string,
+ platform: BrowserPlatform
+ ) {
+ this.#cache = cache;
+ this.browser = browser;
+ this.buildId = buildId;
+ this.platform = platform;
+ this.executablePath = cache.computeExecutablePath({
+ browser,
+ buildId,
+ platform,
+ });
+ }
+
+ /**
+ * Path to the root of the installation folder. Use
+ * {@link computeExecutablePath} to get the path to the executable binary.
+ */
+ get path(): string {
+ return this.#cache.installationDir(
+ this.browser,
+ this.platform,
+ this.buildId
+ );
+ }
+}
+
+/**
+ * @internal
+ */
+export interface ComputeExecutablePathOptions {
+ /**
+ * Determines which platform the browser will be suited for.
+ *
+ * @defaultValue **Auto-detected.**
+ */
+ platform?: BrowserPlatform;
+ /**
+ * Determines which browser to launch.
+ */
+ browser: Browser;
+ /**
+ * Determines which buildId to download. BuildId should uniquely identify
+ * binaries and they are used for caching.
+ */
+ buildId: string;
+}
+
+/**
+ * The cache used by Puppeteer relies on the following structure:
+ *
+ * - rootDir
+ * -- <browser1> | browserRoot(browser1)
+ * ---- <platform>-<buildId> | installationDir()
+ * ------ the browser-platform-buildId
+ * ------ specific structure.
+ * -- <browser2> | browserRoot(browser2)
+ * ---- <platform>-<buildId> | installationDir()
+ * ------ the browser-platform-buildId
+ * ------ specific structure.
+ * @internal
+ */
+export class Cache {
+ #rootDir: string;
+
+ constructor(rootDir: string) {
+ this.#rootDir = rootDir;
+ }
+
+ /**
+ * @internal
+ */
+ get rootDir(): string {
+ return this.#rootDir;
+ }
+
+ browserRoot(browser: Browser): string {
+ return path.join(this.#rootDir, browser);
+ }
+
+ installationDir(
+ browser: Browser,
+ platform: BrowserPlatform,
+ buildId: string
+ ): string {
+ return path.join(this.browserRoot(browser), `${platform}-${buildId}`);
+ }
+
+ clear(): void {
+ fs.rmSync(this.#rootDir, {
+ force: true,
+ recursive: true,
+ maxRetries: 10,
+ retryDelay: 500,
+ });
+ }
+
+ uninstall(
+ browser: Browser,
+ platform: BrowserPlatform,
+ buildId: string
+ ): void {
+ fs.rmSync(this.installationDir(browser, platform, buildId), {
+ force: true,
+ recursive: true,
+ maxRetries: 10,
+ retryDelay: 500,
+ });
+ }
+
+ getInstalledBrowsers(): InstalledBrowser[] {
+ if (!fs.existsSync(this.#rootDir)) {
+ return [];
+ }
+ const types = fs.readdirSync(this.#rootDir);
+ const browsers = types.filter((t): t is Browser => {
+ return (Object.values(Browser) as string[]).includes(t);
+ });
+ return browsers.flatMap(browser => {
+ const files = fs.readdirSync(this.browserRoot(browser));
+ return files
+ .map(file => {
+ const result = parseFolderPath(
+ path.join(this.browserRoot(browser), file)
+ );
+ if (!result) {
+ return null;
+ }
+ return new InstalledBrowser(
+ this,
+ browser,
+ result.buildId,
+ result.platform as BrowserPlatform
+ );
+ })
+ .filter((item: InstalledBrowser | null): item is InstalledBrowser => {
+ return item !== null;
+ });
+ });
+ }
+
+ computeExecutablePath(options: ComputeExecutablePathOptions): string {
+ options.platform ??= detectBrowserPlatform();
+ if (!options.platform) {
+ throw new Error(
+ `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
+ );
+ }
+ const installationDir = this.installationDir(
+ options.browser,
+ options.platform,
+ options.buildId
+ );
+ return path.join(
+ installationDir,
+ executablePathByBrowser[options.browser](
+ options.platform,
+ options.buildId
+ )
+ );
+ }
+}
+
+function parseFolderPath(
+ folderPath: string
+): {platform: string; buildId: string} | undefined {
+ const name = path.basename(folderPath);
+ const splits = name.split('-');
+ if (splits.length !== 2) {
+ return;
+ }
+ const [platform, buildId] = splits;
+ if (!buildId || !platform) {
+ return;
+ }
+ return {platform, buildId};
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts
new file mode 100644
index 0000000000..67bb4990b2
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts
@@ -0,0 +1,187 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as chromeHeadlessShell from './chrome-headless-shell.js';
+import * as chrome from './chrome.js';
+import * as chromedriver from './chromedriver.js';
+import * as chromium from './chromium.js';
+import * as firefox from './firefox.js';
+import {
+ Browser,
+ BrowserPlatform,
+ BrowserTag,
+ ChromeReleaseChannel,
+ type ProfileOptions,
+} from './types.js';
+
+export type {ProfileOptions};
+
+export const downloadUrls = {
+ [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl,
+ [Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.resolveDownloadUrl,
+ [Browser.CHROME]: chrome.resolveDownloadUrl,
+ [Browser.CHROMIUM]: chromium.resolveDownloadUrl,
+ [Browser.FIREFOX]: firefox.resolveDownloadUrl,
+};
+
+export const downloadPaths = {
+ [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadPath,
+ [Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.resolveDownloadPath,
+ [Browser.CHROME]: chrome.resolveDownloadPath,
+ [Browser.CHROMIUM]: chromium.resolveDownloadPath,
+ [Browser.FIREFOX]: firefox.resolveDownloadPath,
+};
+
+export const executablePathByBrowser = {
+ [Browser.CHROMEDRIVER]: chromedriver.relativeExecutablePath,
+ [Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.relativeExecutablePath,
+ [Browser.CHROME]: chrome.relativeExecutablePath,
+ [Browser.CHROMIUM]: chromium.relativeExecutablePath,
+ [Browser.FIREFOX]: firefox.relativeExecutablePath,
+};
+
+export {Browser, BrowserPlatform, ChromeReleaseChannel};
+
+/**
+ * @public
+ */
+export async function resolveBuildId(
+ browser: Browser,
+ platform: BrowserPlatform,
+ tag: string
+): Promise<string> {
+ switch (browser) {
+ case Browser.FIREFOX:
+ switch (tag as BrowserTag) {
+ case BrowserTag.LATEST:
+ return await firefox.resolveBuildId('FIREFOX_NIGHTLY');
+ case BrowserTag.BETA:
+ case BrowserTag.CANARY:
+ case BrowserTag.DEV:
+ case BrowserTag.STABLE:
+ throw new Error(
+ `${tag} is not supported for ${browser}. Use 'latest' instead.`
+ );
+ }
+ case Browser.CHROME: {
+ switch (tag as BrowserTag) {
+ case BrowserTag.LATEST:
+ return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
+ case BrowserTag.BETA:
+ return await chrome.resolveBuildId(ChromeReleaseChannel.BETA);
+ case BrowserTag.CANARY:
+ return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
+ case BrowserTag.DEV:
+ return await chrome.resolveBuildId(ChromeReleaseChannel.DEV);
+ case BrowserTag.STABLE:
+ return await chrome.resolveBuildId(ChromeReleaseChannel.STABLE);
+ default:
+ const result = await chrome.resolveBuildId(tag);
+ if (result) {
+ return result;
+ }
+ }
+ return tag;
+ }
+ case Browser.CHROMEDRIVER: {
+ switch (tag) {
+ case BrowserTag.LATEST:
+ case BrowserTag.CANARY:
+ return await chromedriver.resolveBuildId(ChromeReleaseChannel.CANARY);
+ case BrowserTag.BETA:
+ return await chromedriver.resolveBuildId(ChromeReleaseChannel.BETA);
+ case BrowserTag.DEV:
+ return await chromedriver.resolveBuildId(ChromeReleaseChannel.DEV);
+ case BrowserTag.STABLE:
+ return await chromedriver.resolveBuildId(ChromeReleaseChannel.STABLE);
+ default:
+ const result = await chromedriver.resolveBuildId(tag);
+ if (result) {
+ return result;
+ }
+ }
+ return tag;
+ }
+ case Browser.CHROMEHEADLESSSHELL: {
+ switch (tag) {
+ case BrowserTag.LATEST:
+ case BrowserTag.CANARY:
+ return await chromeHeadlessShell.resolveBuildId(
+ ChromeReleaseChannel.CANARY
+ );
+ case BrowserTag.BETA:
+ return await chromeHeadlessShell.resolveBuildId(
+ ChromeReleaseChannel.BETA
+ );
+ case BrowserTag.DEV:
+ return await chromeHeadlessShell.resolveBuildId(
+ ChromeReleaseChannel.DEV
+ );
+ case BrowserTag.STABLE:
+ return await chromeHeadlessShell.resolveBuildId(
+ ChromeReleaseChannel.STABLE
+ );
+ default:
+ const result = await chromeHeadlessShell.resolveBuildId(tag);
+ if (result) {
+ return result;
+ }
+ }
+ return tag;
+ }
+ case Browser.CHROMIUM:
+ switch (tag as BrowserTag) {
+ case BrowserTag.LATEST:
+ return await chromium.resolveBuildId(platform);
+ case BrowserTag.BETA:
+ case BrowserTag.CANARY:
+ case BrowserTag.DEV:
+ case BrowserTag.STABLE:
+ throw new Error(
+ `${tag} is not supported for ${browser}. Use 'latest' instead.`
+ );
+ }
+ }
+ // We assume the tag is the buildId if it didn't match any keywords.
+ return tag;
+}
+
+/**
+ * @public
+ */
+export async function createProfile(
+ browser: Browser,
+ opts: ProfileOptions
+): Promise<void> {
+ switch (browser) {
+ case Browser.FIREFOX:
+ return await firefox.createProfile(opts);
+ case Browser.CHROME:
+ case Browser.CHROMIUM:
+ throw new Error(`Profile creation is not support for ${browser} yet`);
+ }
+}
+
+/**
+ * @public
+ */
+export function resolveSystemExecutablePath(
+ browser: Browser,
+ platform: BrowserPlatform,
+ channel: ChromeReleaseChannel
+): string {
+ switch (browser) {
+ case Browser.CHROMEDRIVER:
+ case Browser.CHROMEHEADLESSSHELL:
+ case Browser.FIREFOX:
+ case Browser.CHROMIUM:
+ throw new Error(
+ `System browser detection is not supported for ${browser} yet.`
+ );
+ case Browser.CHROME:
+ return chrome.resolveSystemExecutablePath(platform, channel);
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chrome-headless-shell.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome-headless-shell.ts
new file mode 100644
index 0000000000..b1c6178de8
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome-headless-shell.ts
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import path from 'path';
+
+import {BrowserPlatform} from './types.js';
+
+function folder(platform: BrowserPlatform): string {
+ switch (platform) {
+ case BrowserPlatform.LINUX:
+ return 'linux64';
+ case BrowserPlatform.MAC_ARM:
+ return 'mac-arm64';
+ case BrowserPlatform.MAC:
+ return 'mac-x64';
+ case BrowserPlatform.WIN32:
+ return 'win32';
+ case BrowserPlatform.WIN64:
+ return 'win64';
+ }
+}
+
+export function resolveDownloadUrl(
+ platform: BrowserPlatform,
+ buildId: string,
+ baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing'
+): string {
+ return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
+}
+
+export function resolveDownloadPath(
+ platform: BrowserPlatform,
+ buildId: string
+): string[] {
+ return [
+ buildId,
+ folder(platform),
+ `chrome-headless-shell-${folder(platform)}.zip`,
+ ];
+}
+
+export function relativeExecutablePath(
+ platform: BrowserPlatform,
+ _buildId: string
+): string {
+ switch (platform) {
+ case BrowserPlatform.MAC:
+ case BrowserPlatform.MAC_ARM:
+ return path.join(
+ 'chrome-headless-shell-' + folder(platform),
+ 'chrome-headless-shell'
+ );
+ case BrowserPlatform.LINUX:
+ return path.join(
+ 'chrome-headless-shell-linux64',
+ 'chrome-headless-shell'
+ );
+ case BrowserPlatform.WIN32:
+ case BrowserPlatform.WIN64:
+ return path.join(
+ 'chrome-headless-shell-' + folder(platform),
+ 'chrome-headless-shell.exe'
+ );
+ }
+}
+
+export {resolveBuildId} from './chrome.js';
diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts
new file mode 100644
index 0000000000..c6329255c3
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts
@@ -0,0 +1,195 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+
+import {getJSON} from '../httpUtil.js';
+
+import {BrowserPlatform, ChromeReleaseChannel} from './types.js';
+
+function folder(platform: BrowserPlatform): string {
+ switch (platform) {
+ case BrowserPlatform.LINUX:
+ return 'linux64';
+ case BrowserPlatform.MAC_ARM:
+ return 'mac-arm64';
+ case BrowserPlatform.MAC:
+ return 'mac-x64';
+ case BrowserPlatform.WIN32:
+ return 'win32';
+ case BrowserPlatform.WIN64:
+ return 'win64';
+ }
+}
+
+export function resolveDownloadUrl(
+ platform: BrowserPlatform,
+ buildId: string,
+ baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing'
+): string {
+ return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
+}
+
+export function resolveDownloadPath(
+ platform: BrowserPlatform,
+ buildId: string
+): string[] {
+ return [buildId, folder(platform), `chrome-${folder(platform)}.zip`];
+}
+
+export function relativeExecutablePath(
+ platform: BrowserPlatform,
+ _buildId: string
+): string {
+ switch (platform) {
+ case BrowserPlatform.MAC:
+ case BrowserPlatform.MAC_ARM:
+ return path.join(
+ 'chrome-' + folder(platform),
+ 'Google Chrome for Testing.app',
+ 'Contents',
+ 'MacOS',
+ 'Google Chrome for Testing'
+ );
+ case BrowserPlatform.LINUX:
+ return path.join('chrome-linux64', 'chrome');
+ case BrowserPlatform.WIN32:
+ case BrowserPlatform.WIN64:
+ return path.join('chrome-' + folder(platform), 'chrome.exe');
+ }
+}
+
+export async function getLastKnownGoodReleaseForChannel(
+ channel: ChromeReleaseChannel
+): Promise<{version: string; revision: string}> {
+ const data = (await getJSON(
+ new URL(
+ 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json'
+ )
+ )) as {
+ channels: Record<string, {version: string}>;
+ };
+
+ for (const channel of Object.keys(data.channels)) {
+ data.channels[channel.toLowerCase()] = data.channels[channel]!;
+ delete data.channels[channel];
+ }
+
+ return (
+ data as {
+ channels: {
+ [channel in ChromeReleaseChannel]: {version: string; revision: string};
+ };
+ }
+ ).channels[channel];
+}
+
+export async function getLastKnownGoodReleaseForMilestone(
+ milestone: string
+): Promise<{version: string; revision: string} | undefined> {
+ const data = (await getJSON(
+ new URL(
+ 'https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone.json'
+ )
+ )) as {
+ milestones: Record<string, {version: string; revision: string}>;
+ };
+ return data.milestones[milestone] as
+ | {version: string; revision: string}
+ | undefined;
+}
+
+export async function getLastKnownGoodReleaseForBuild(
+ /**
+ * @example `112.0.23`,
+ */
+ buildPrefix: string
+): Promise<{version: string; revision: string} | undefined> {
+ const data = (await getJSON(
+ new URL(
+ 'https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build.json'
+ )
+ )) as {
+ builds: Record<string, {version: string; revision: string}>;
+ };
+ return data.builds[buildPrefix] as
+ | {version: string; revision: string}
+ | undefined;
+}
+
+export async function resolveBuildId(
+ channel: ChromeReleaseChannel
+): Promise<string>;
+export async function resolveBuildId(
+ channel: string
+): Promise<string | undefined>;
+export async function resolveBuildId(
+ channel: ChromeReleaseChannel | string
+): Promise<string | undefined> {
+ if (
+ Object.values(ChromeReleaseChannel).includes(
+ channel as ChromeReleaseChannel
+ )
+ ) {
+ return (
+ await getLastKnownGoodReleaseForChannel(channel as ChromeReleaseChannel)
+ ).version;
+ }
+ if (channel.match(/^\d+$/)) {
+ // Potentially a milestone.
+ return (await getLastKnownGoodReleaseForMilestone(channel))?.version;
+ }
+ if (channel.match(/^\d+\.\d+\.\d+$/)) {
+ // Potentially a build prefix without the patch version.
+ return (await getLastKnownGoodReleaseForBuild(channel))?.version;
+ }
+ return;
+}
+
+export function resolveSystemExecutablePath(
+ platform: BrowserPlatform,
+ channel: ChromeReleaseChannel
+): string {
+ switch (platform) {
+ case BrowserPlatform.WIN64:
+ case BrowserPlatform.WIN32:
+ switch (channel) {
+ case ChromeReleaseChannel.STABLE:
+ return `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`;
+ case ChromeReleaseChannel.BETA:
+ return `${process.env['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe`;
+ case ChromeReleaseChannel.CANARY:
+ return `${process.env['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe`;
+ case ChromeReleaseChannel.DEV:
+ return `${process.env['PROGRAMFILES']}\\Google\\Chrome Dev\\Application\\chrome.exe`;
+ }
+ case BrowserPlatform.MAC_ARM:
+ case BrowserPlatform.MAC:
+ switch (channel) {
+ case ChromeReleaseChannel.STABLE:
+ return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+ case ChromeReleaseChannel.BETA:
+ return '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta';
+ case ChromeReleaseChannel.CANARY:
+ return '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary';
+ case ChromeReleaseChannel.DEV:
+ return '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev';
+ }
+ case BrowserPlatform.LINUX:
+ switch (channel) {
+ case ChromeReleaseChannel.STABLE:
+ return '/opt/google/chrome/chrome';
+ case ChromeReleaseChannel.BETA:
+ return '/opt/google/chrome-beta/chrome';
+ case ChromeReleaseChannel.DEV:
+ return '/opt/google/chrome-unstable/chrome';
+ }
+ }
+
+ throw new Error(
+ `Unable to detect browser executable path for '${channel}' on ${platform}.`
+ );
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts
new file mode 100644
index 0000000000..290598d0d7
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import path from 'path';
+
+import {BrowserPlatform} from './types.js';
+
+function folder(platform: BrowserPlatform): string {
+ switch (platform) {
+ case BrowserPlatform.LINUX:
+ return 'linux64';
+ case BrowserPlatform.MAC_ARM:
+ return 'mac-arm64';
+ case BrowserPlatform.MAC:
+ return 'mac-x64';
+ case BrowserPlatform.WIN32:
+ return 'win32';
+ case BrowserPlatform.WIN64:
+ return 'win64';
+ }
+}
+
+export function resolveDownloadUrl(
+ platform: BrowserPlatform,
+ buildId: string,
+ baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing'
+): string {
+ return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
+}
+
+export function resolveDownloadPath(
+ platform: BrowserPlatform,
+ buildId: string
+): string[] {
+ return [buildId, folder(platform), `chromedriver-${folder(platform)}.zip`];
+}
+
+export function relativeExecutablePath(
+ platform: BrowserPlatform,
+ _buildId: string
+): string {
+ switch (platform) {
+ case BrowserPlatform.MAC:
+ case BrowserPlatform.MAC_ARM:
+ return path.join('chromedriver-' + folder(platform), 'chromedriver');
+ case BrowserPlatform.LINUX:
+ return path.join('chromedriver-linux64', 'chromedriver');
+ case BrowserPlatform.WIN32:
+ case BrowserPlatform.WIN64:
+ return path.join('chromedriver-' + folder(platform), 'chromedriver.exe');
+ }
+}
+
+export {resolveBuildId} from './chrome.js';
diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts
new file mode 100644
index 0000000000..09cfc987a8
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts
@@ -0,0 +1,88 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+
+import {getText} from '../httpUtil.js';
+
+import {BrowserPlatform} from './types.js';
+
+function archive(platform: BrowserPlatform, buildId: string): string {
+ switch (platform) {
+ case BrowserPlatform.LINUX:
+ return 'chrome-linux';
+ case BrowserPlatform.MAC_ARM:
+ case BrowserPlatform.MAC:
+ return 'chrome-mac';
+ case BrowserPlatform.WIN32:
+ case BrowserPlatform.WIN64:
+ // Windows archive name changed at r591479.
+ return parseInt(buildId, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
+ }
+}
+
+function folder(platform: BrowserPlatform): string {
+ switch (platform) {
+ case BrowserPlatform.LINUX:
+ return 'Linux_x64';
+ case BrowserPlatform.MAC_ARM:
+ return 'Mac_Arm';
+ case BrowserPlatform.MAC:
+ return 'Mac';
+ case BrowserPlatform.WIN32:
+ return 'Win';
+ case BrowserPlatform.WIN64:
+ return 'Win_x64';
+ }
+}
+
+export function resolveDownloadUrl(
+ platform: BrowserPlatform,
+ buildId: string,
+ baseUrl = 'https://storage.googleapis.com/chromium-browser-snapshots'
+): string {
+ return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
+}
+
+export function resolveDownloadPath(
+ platform: BrowserPlatform,
+ buildId: string
+): string[] {
+ return [folder(platform), buildId, `${archive(platform, buildId)}.zip`];
+}
+
+export function relativeExecutablePath(
+ platform: BrowserPlatform,
+ _buildId: string
+): string {
+ switch (platform) {
+ case BrowserPlatform.MAC:
+ case BrowserPlatform.MAC_ARM:
+ return path.join(
+ 'chrome-mac',
+ 'Chromium.app',
+ 'Contents',
+ 'MacOS',
+ 'Chromium'
+ );
+ case BrowserPlatform.LINUX:
+ return path.join('chrome-linux', 'chrome');
+ case BrowserPlatform.WIN32:
+ case BrowserPlatform.WIN64:
+ return path.join('chrome-win', 'chrome.exe');
+ }
+}
+export async function resolveBuildId(
+ platform: BrowserPlatform
+): Promise<string> {
+ return await getText(
+ new URL(
+ `https://storage.googleapis.com/chromium-browser-snapshots/${folder(
+ platform
+ )}/LAST_CHANGE`
+ )
+ );
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts
new file mode 100644
index 0000000000..ccc30fa1b5
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts
@@ -0,0 +1,330 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import {getJSON} from '../httpUtil.js';
+
+import {BrowserPlatform, type ProfileOptions} from './types.js';
+
+function archive(platform: BrowserPlatform, buildId: string): string {
+ switch (platform) {
+ case BrowserPlatform.LINUX:
+ return `firefox-${buildId}.en-US.${platform}-x86_64.tar.bz2`;
+ case BrowserPlatform.MAC_ARM:
+ case BrowserPlatform.MAC:
+ return `firefox-${buildId}.en-US.mac.dmg`;
+ case BrowserPlatform.WIN32:
+ case BrowserPlatform.WIN64:
+ return `firefox-${buildId}.en-US.${platform}.zip`;
+ }
+}
+
+export function resolveDownloadUrl(
+ platform: BrowserPlatform,
+ buildId: string,
+ baseUrl = 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central'
+): string {
+ return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
+}
+
+export function resolveDownloadPath(
+ platform: BrowserPlatform,
+ buildId: string
+): string[] {
+ return [archive(platform, buildId)];
+}
+
+export function relativeExecutablePath(
+ platform: BrowserPlatform,
+ _buildId: string
+): string {
+ switch (platform) {
+ case BrowserPlatform.MAC_ARM:
+ case BrowserPlatform.MAC:
+ return path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox');
+ case BrowserPlatform.LINUX:
+ return path.join('firefox', 'firefox');
+ case BrowserPlatform.WIN32:
+ case BrowserPlatform.WIN64:
+ return path.join('firefox', 'firefox.exe');
+ }
+}
+
+export async function resolveBuildId(
+ channel: 'FIREFOX_NIGHTLY' = 'FIREFOX_NIGHTLY'
+): Promise<string> {
+ const versions = (await getJSON(
+ new URL('https://product-details.mozilla.org/1.0/firefox_versions.json')
+ )) as Record<string, string>;
+ const version = versions[channel];
+ if (!version) {
+ throw new Error(`Channel ${channel} is not found.`);
+ }
+ return version;
+}
+
+export async function createProfile(options: ProfileOptions): Promise<void> {
+ if (!fs.existsSync(options.path)) {
+ await fs.promises.mkdir(options.path, {
+ recursive: true,
+ });
+ }
+ await writePreferences({
+ preferences: {
+ ...defaultProfilePreferences(options.preferences),
+ ...options.preferences,
+ },
+ path: options.path,
+ });
+}
+
+function defaultProfilePreferences(
+ extraPrefs: Record<string, unknown>
+): Record<string, unknown> {
+ const server = 'dummy.test';
+
+ const defaultPrefs = {
+ // Make sure Shield doesn't hit the network.
+ 'app.normandy.api_url': '',
+ // Disable Firefox old build background check
+ 'app.update.checkInstallTime': false,
+ // Disable automatically upgrading Firefox
+ 'app.update.disabledForTesting': true,
+
+ // Increase the APZ content response timeout to 1 minute
+ 'apz.content_response_timeout': 60000,
+
+ // Prevent various error message on the console
+ // jest-puppeteer asserts that no error message is emitted by the console
+ 'browser.contentblocking.features.standard':
+ '-tp,tpPrivate,cookieBehavior0,-cm,-fp',
+
+ // Enable the dump function: which sends messages to the system
+ // console
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
+ 'browser.dom.window.dump.enabled': true,
+ // Disable topstories
+ 'browser.newtabpage.activity-stream.feeds.system.topstories': false,
+ // Always display a blank page
+ 'browser.newtabpage.enabled': false,
+ // Background thumbnails in particular cause grief: and disabling
+ // thumbnails in general cannot hurt
+ 'browser.pagethumbnails.capturing_disabled': true,
+
+ // Disable safebrowsing components.
+ 'browser.safebrowsing.blockedURIs.enabled': false,
+ 'browser.safebrowsing.downloads.enabled': false,
+ 'browser.safebrowsing.malware.enabled': false,
+ 'browser.safebrowsing.phishing.enabled': false,
+
+ // Disable updates to search engines.
+ 'browser.search.update': false,
+ // Do not restore the last open set of tabs if the browser has crashed
+ 'browser.sessionstore.resume_from_crash': false,
+ // Skip check for default browser on startup
+ 'browser.shell.checkDefaultBrowser': false,
+
+ // Disable newtabpage
+ 'browser.startup.homepage': 'about:blank',
+ // Do not redirect user when a milstone upgrade of Firefox is detected
+ 'browser.startup.homepage_override.mstone': 'ignore',
+ // Start with a blank page about:blank
+ 'browser.startup.page': 0,
+
+ // Do not allow background tabs to be zombified on Android: otherwise for
+ // tests that open additional tabs: the test harness tab itself might get
+ // unloaded
+ 'browser.tabs.disableBackgroundZombification': false,
+ // Do not warn when closing all other open tabs
+ 'browser.tabs.warnOnCloseOtherTabs': false,
+ // Do not warn when multiple tabs will be opened
+ 'browser.tabs.warnOnOpen': false,
+
+ // Do not automatically offer translations, as tests do not expect this.
+ 'browser.translations.automaticallyPopup': false,
+
+ // Disable the UI tour.
+ 'browser.uitour.enabled': false,
+ // Turn off search suggestions in the location bar so as not to trigger
+ // network connections.
+ 'browser.urlbar.suggest.searches': false,
+ // Disable first run splash page on Windows 10
+ 'browser.usedOnWindows10.introURL': '',
+ // Do not warn on quitting Firefox
+ 'browser.warnOnQuit': false,
+
+ // Defensively disable data reporting systems
+ 'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`,
+ 'datareporting.healthreport.logging.consoleEnabled': false,
+ 'datareporting.healthreport.service.enabled': false,
+ 'datareporting.healthreport.service.firstRun': false,
+ 'datareporting.healthreport.uploadEnabled': false,
+
+ // Do not show datareporting policy notifications which can interfere with tests
+ 'datareporting.policy.dataSubmissionEnabled': false,
+ 'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
+
+ // DevTools JSONViewer sometimes fails to load dependencies with its require.js.
+ // This doesn't affect Puppeteer but spams console (Bug 1424372)
+ 'devtools.jsonview.enabled': false,
+
+ // Disable popup-blocker
+ 'dom.disable_open_during_load': false,
+
+ // Enable the support for File object creation in the content process
+ // Required for |Page.setFileInputFiles| protocol method.
+ 'dom.file.createInChild': true,
+
+ // Disable the ProcessHangMonitor
+ 'dom.ipc.reportProcessHangs': false,
+
+ // Disable slow script dialogues
+ 'dom.max_chrome_script_run_time': 0,
+ 'dom.max_script_run_time': 0,
+
+ // Only load extensions from the application and user profile
+ // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
+ 'extensions.autoDisableScopes': 0,
+ 'extensions.enabledScopes': 5,
+
+ // Disable metadata caching for installed add-ons by default
+ 'extensions.getAddons.cache.enabled': false,
+
+ // Disable installing any distribution extensions or add-ons.
+ 'extensions.installDistroAddons': false,
+
+ // Disabled screenshots extension
+ 'extensions.screenshots.disabled': true,
+
+ // Turn off extension updates so they do not bother tests
+ 'extensions.update.enabled': false,
+
+ // Turn off extension updates so they do not bother tests
+ 'extensions.update.notifyUser': false,
+
+ // Make sure opening about:addons will not hit the network
+ 'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`,
+
+ // Allow the application to have focus even it runs in the background
+ 'focusmanager.testmode': true,
+
+ // Disable useragent updates
+ 'general.useragent.updates.enabled': false,
+
+ // Always use network provider for geolocation tests so we bypass the
+ // macOS dialog raised by the corelocation provider
+ 'geo.provider.testing': true,
+
+ // Do not scan Wifi
+ 'geo.wifi.scan': false,
+
+ // No hang monitor
+ 'hangmonitor.timeout': 0,
+
+ // Show chrome errors and warnings in the error console
+ 'javascript.options.showInConsole': true,
+
+ // Disable download and usage of OpenH264: and Widevine plugins
+ 'media.gmp-manager.updateEnabled': false,
+
+ // Disable the GFX sanity window
+ 'media.sanity-test.disabled': true,
+
+ // Prevent various error message on the console
+ // jest-puppeteer asserts that no error message is emitted by the console
+ 'network.cookie.cookieBehavior': 0,
+
+ // Disable experimental feature that is only available in Nightly
+ 'network.cookie.sameSite.laxByDefault': false,
+
+ // Do not prompt for temporary redirects
+ 'network.http.prompt-temp-redirect': false,
+
+ // Disable speculative connections so they are not reported as leaking
+ // when they are hanging around
+ 'network.http.speculative-parallel-limit': 0,
+
+ // Do not automatically switch between offline and online
+ 'network.manage-offline-status': false,
+
+ // Make sure SNTP requests do not hit the network
+ 'network.sntp.pools': server,
+
+ // Disable Flash.
+ 'plugin.state.flash': 0,
+
+ 'privacy.trackingprotection.enabled': false,
+
+ // Can be removed once Firefox 89 is no longer supported
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1710839
+ 'remote.enabled': true,
+
+ // Don't do network connections for mitm priming
+ 'security.certerrors.mitm.priming.enabled': false,
+
+ // Local documents have access to all other local documents,
+ // including directory listings
+ 'security.fileuri.strict_origin_policy': false,
+
+ // Do not wait for the notification button security delay
+ 'security.notification_enable_delay': 0,
+
+ // Ensure blocklist updates do not hit the network
+ 'services.settings.server': `http://${server}/dummy/blocklist/`,
+
+ // Do not automatically fill sign-in forms with known usernames and
+ // passwords
+ 'signon.autofillForms': false,
+
+ // Disable password capture, so that tests that include forms are not
+ // influenced by the presence of the persistent doorhanger notification
+ 'signon.rememberSignons': false,
+
+ // Disable first-run welcome page
+ 'startup.homepage_welcome_url': 'about:blank',
+
+ // Disable first-run welcome page
+ 'startup.homepage_welcome_url.additional': '',
+
+ // Disable browser animations (tabs, fullscreen, sliding alerts)
+ 'toolkit.cosmeticAnimations.enabled': false,
+
+ // Prevent starting into safe mode after application crashes
+ 'toolkit.startup.max_resumed_crashes': -1,
+ };
+
+ return Object.assign(defaultPrefs, extraPrefs);
+}
+
+/**
+ * Populates the user.js file with custom preferences as needed to allow
+ * Firefox's CDP support to properly function. These preferences will be
+ * automatically copied over to prefs.js during startup of Firefox. To be
+ * able to restore the original values of preferences a backup of prefs.js
+ * will be created.
+ *
+ * @param prefs - List of preferences to add.
+ * @param profilePath - Firefox profile to write the preferences to.
+ */
+async function writePreferences(options: ProfileOptions): Promise<void> {
+ const lines = Object.entries(options.preferences).map(([key, value]) => {
+ return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`;
+ });
+
+ await fs.promises.writeFile(
+ path.join(options.path, 'user.js'),
+ lines.join('\n')
+ );
+
+ // Create a backup of the preferences file if it already exitsts.
+ const prefsPath = path.join(options.path, 'prefs.js');
+ if (fs.existsSync(prefsPath)) {
+ const prefsBackupPath = path.join(options.path, 'prefs.js.puppeteer');
+ await fs.promises.copyFile(prefsPath, prefsBackupPath);
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/types.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/types.ts
new file mode 100644
index 0000000000..ac72661a2d
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/browser-data/types.ts
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Supported browsers.
+ *
+ * @public
+ */
+export enum Browser {
+ CHROME = 'chrome',
+ CHROMEHEADLESSSHELL = 'chrome-headless-shell',
+ CHROMIUM = 'chromium',
+ FIREFOX = 'firefox',
+ CHROMEDRIVER = 'chromedriver',
+}
+
+/**
+ * Platform names used to identify a OS platform x architecture combination in the way
+ * that is relevant for the browser download.
+ *
+ * @public
+ */
+export enum BrowserPlatform {
+ LINUX = 'linux',
+ MAC = 'mac',
+ MAC_ARM = 'mac_arm',
+ WIN32 = 'win32',
+ WIN64 = 'win64',
+}
+
+/**
+ * @public
+ */
+export enum BrowserTag {
+ CANARY = 'canary',
+ BETA = 'beta',
+ DEV = 'dev',
+ STABLE = 'stable',
+ LATEST = 'latest',
+}
+
+/**
+ * @public
+ */
+export interface ProfileOptions {
+ preferences: Record<string, unknown>;
+ path: string;
+}
+
+/**
+ * @public
+ */
+export enum ChromeReleaseChannel {
+ STABLE = 'stable',
+ DEV = 'dev',
+ CANARY = 'canary',
+ BETA = 'beta',
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/debug.ts b/remote/test/puppeteer/packages/browsers/src/debug.ts
new file mode 100644
index 0000000000..491097f41d
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/debug.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import debug from 'debug';
+
+export {debug};
diff --git a/remote/test/puppeteer/packages/browsers/src/detectPlatform.ts b/remote/test/puppeteer/packages/browsers/src/detectPlatform.ts
new file mode 100644
index 0000000000..df644c38b7
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/detectPlatform.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import os from 'os';
+
+import {BrowserPlatform} from './browser-data/browser-data.js';
+
+/**
+ * @public
+ */
+export function detectBrowserPlatform(): BrowserPlatform | undefined {
+ const platform = os.platform();
+ switch (platform) {
+ case 'darwin':
+ return os.arch() === 'arm64'
+ ? BrowserPlatform.MAC_ARM
+ : BrowserPlatform.MAC;
+ case 'linux':
+ return BrowserPlatform.LINUX;
+ case 'win32':
+ return os.arch() === 'x64' ||
+ // Windows 11 for ARM supports x64 emulation
+ (os.arch() === 'arm64' && isWindows11(os.release()))
+ ? BrowserPlatform.WIN64
+ : BrowserPlatform.WIN32;
+ default:
+ return undefined;
+ }
+}
+
+/**
+ * Windows 11 is identified by the version 10.0.22000 or greater
+ * @internal
+ */
+function isWindows11(version: string): boolean {
+ const parts = version.split('.');
+ if (parts.length > 2) {
+ const major = parseInt(parts[0] as string, 10);
+ const minor = parseInt(parts[1] as string, 10);
+ const patch = parseInt(parts[2] as string, 10);
+ return (
+ major > 10 ||
+ (major === 10 && minor > 0) ||
+ (major === 10 && minor === 0 && patch >= 22000)
+ );
+ }
+ return false;
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/fileUtil.ts b/remote/test/puppeteer/packages/browsers/src/fileUtil.ts
new file mode 100644
index 0000000000..50a6897853
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/fileUtil.ts
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {exec as execChildProcess} from 'child_process';
+import {createReadStream} from 'fs';
+import {mkdir, readdir} from 'fs/promises';
+import * as path from 'path';
+import {promisify} from 'util';
+
+import extractZip from 'extract-zip';
+import tar from 'tar-fs';
+import bzip from 'unbzip2-stream';
+
+const exec = promisify(execChildProcess);
+
+/**
+ * @internal
+ */
+export async function unpackArchive(
+ archivePath: string,
+ folderPath: string
+): Promise<void> {
+ if (archivePath.endsWith('.zip')) {
+ await extractZip(archivePath, {dir: folderPath});
+ } else if (archivePath.endsWith('.tar.bz2')) {
+ await extractTar(archivePath, folderPath);
+ } else if (archivePath.endsWith('.dmg')) {
+ await mkdir(folderPath);
+ await installDMG(archivePath, folderPath);
+ } else {
+ throw new Error(`Unsupported archive format: ${archivePath}`);
+ }
+}
+
+/**
+ * @internal
+ */
+function extractTar(tarPath: string, folderPath: string): Promise<void> {
+ return new Promise((fulfill, reject) => {
+ const tarStream = tar.extract(folderPath);
+ tarStream.on('error', reject);
+ tarStream.on('finish', fulfill);
+ const readStream = createReadStream(tarPath);
+ readStream.pipe(bzip()).pipe(tarStream);
+ });
+}
+
+/**
+ * @internal
+ */
+async function installDMG(dmgPath: string, folderPath: string): Promise<void> {
+ const {stdout} = await exec(
+ `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`
+ );
+
+ const volumes = stdout.match(/\/Volumes\/(.*)/m);
+ if (!volumes) {
+ throw new Error(`Could not find volume path in ${stdout}`);
+ }
+ const mountPath = volumes[0]!;
+
+ try {
+ const fileNames = await readdir(mountPath);
+ const appName = fileNames.find(item => {
+ return typeof item === 'string' && item.endsWith('.app');
+ });
+ if (!appName) {
+ throw new Error(`Cannot find app in ${mountPath}`);
+ }
+ const mountedPath = path.join(mountPath!, appName);
+
+ await exec(`cp -R "${mountedPath}" "${folderPath}"`);
+ } finally {
+ await exec(`hdiutil detach "${mountPath}" -quiet`);
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/httpUtil.ts b/remote/test/puppeteer/packages/browsers/src/httpUtil.ts
new file mode 100644
index 0000000000..96f7fc9f36
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/httpUtil.ts
@@ -0,0 +1,151 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {createWriteStream} from 'fs';
+import * as http from 'http';
+import * as https from 'https';
+import {URL, urlToHttpOptions} from 'url';
+
+import {ProxyAgent} from 'proxy-agent';
+
+export function headHttpRequest(url: URL): Promise<boolean> {
+ return new Promise(resolve => {
+ const request = httpRequest(
+ url,
+ 'HEAD',
+ response => {
+ // consume response data free node process
+ response.resume();
+ resolve(response.statusCode === 200);
+ },
+ false
+ );
+ request.on('error', () => {
+ resolve(false);
+ });
+ });
+}
+
+export function httpRequest(
+ url: URL,
+ method: string,
+ response: (x: http.IncomingMessage) => void,
+ keepAlive = true
+): http.ClientRequest {
+ const options: http.RequestOptions = {
+ protocol: url.protocol,
+ hostname: url.hostname,
+ port: url.port,
+ path: url.pathname + url.search,
+ method,
+ headers: keepAlive ? {Connection: 'keep-alive'} : undefined,
+ auth: urlToHttpOptions(url).auth,
+ agent: new ProxyAgent(),
+ };
+
+ const requestCallback = (res: http.IncomingMessage): void => {
+ if (
+ res.statusCode &&
+ res.statusCode >= 300 &&
+ res.statusCode < 400 &&
+ res.headers.location
+ ) {
+ httpRequest(new URL(res.headers.location), method, response);
+ } else {
+ response(res);
+ }
+ };
+ const request =
+ options.protocol === 'https:'
+ ? https.request(options, requestCallback)
+ : http.request(options, requestCallback);
+ request.end();
+ return request;
+}
+
+/**
+ * @internal
+ */
+export function downloadFile(
+ url: URL,
+ destinationPath: string,
+ progressCallback?: (downloadedBytes: number, totalBytes: number) => void
+): Promise<void> {
+ return new Promise<void>((resolve, reject) => {
+ let downloadedBytes = 0;
+ let totalBytes = 0;
+
+ function onData(chunk: string): void {
+ downloadedBytes += chunk.length;
+ progressCallback!(downloadedBytes, totalBytes);
+ }
+
+ const request = httpRequest(url, 'GET', response => {
+ if (response.statusCode !== 200) {
+ const error = new Error(
+ `Download failed: server returned code ${response.statusCode}. URL: ${url}`
+ );
+ // consume response data to free up memory
+ response.resume();
+ reject(error);
+ return;
+ }
+ const file = createWriteStream(destinationPath);
+ file.on('finish', () => {
+ return resolve();
+ });
+ file.on('error', error => {
+ return reject(error);
+ });
+ response.pipe(file);
+ totalBytes = parseInt(response.headers['content-length']!, 10);
+ if (progressCallback) {
+ response.on('data', onData);
+ }
+ });
+ request.on('error', error => {
+ return reject(error);
+ });
+ });
+}
+
+export async function getJSON(url: URL): Promise<unknown> {
+ const text = await getText(url);
+ try {
+ return JSON.parse(text);
+ } catch {
+ throw new Error('Could not parse JSON from ' + url.toString());
+ }
+}
+
+export function getText(url: URL): Promise<string> {
+ return new Promise((resolve, reject) => {
+ const request = httpRequest(
+ url,
+ 'GET',
+ response => {
+ let data = '';
+ if (response.statusCode && response.statusCode >= 400) {
+ return reject(new Error(`Got status code ${response.statusCode}`));
+ }
+ response.on('data', chunk => {
+ data += chunk;
+ });
+ response.on('end', () => {
+ try {
+ return resolve(String(data));
+ } catch {
+ return reject(new Error('Chrome version not found'));
+ }
+ });
+ },
+ false
+ );
+ request.on('error', err => {
+ reject(err);
+ });
+ });
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/install.ts b/remote/test/puppeteer/packages/browsers/src/install.ts
new file mode 100644
index 0000000000..375c75babc
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/install.ts
@@ -0,0 +1,271 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import {existsSync} from 'fs';
+import {mkdir, unlink} from 'fs/promises';
+import os from 'os';
+import path from 'path';
+
+import {
+ type Browser,
+ type BrowserPlatform,
+ downloadUrls,
+} from './browser-data/browser-data.js';
+import {Cache, InstalledBrowser} from './Cache.js';
+import {debug} from './debug.js';
+import {detectBrowserPlatform} from './detectPlatform.js';
+import {unpackArchive} from './fileUtil.js';
+import {downloadFile, headHttpRequest} from './httpUtil.js';
+
+const debugInstall = debug('puppeteer:browsers:install');
+
+const times = new Map<string, [number, number]>();
+function debugTime(label: string) {
+ times.set(label, process.hrtime());
+}
+
+function debugTimeEnd(label: string) {
+ const end = process.hrtime();
+ const start = times.get(label);
+ if (!start) {
+ return;
+ }
+ const duration =
+ end[0] * 1000 + end[1] / 1e6 - (start[0] * 1000 + start[1] / 1e6); // calculate duration in milliseconds
+ debugInstall(`Duration for ${label}: ${duration}ms`);
+}
+
+/**
+ * @public
+ */
+export interface InstallOptions {
+ /**
+ * Determines the path to download browsers to.
+ */
+ cacheDir: string;
+ /**
+ * Determines which platform the browser will be suited for.
+ *
+ * @defaultValue **Auto-detected.**
+ */
+ platform?: BrowserPlatform;
+ /**
+ * Determines which browser to install.
+ */
+ browser: Browser;
+ /**
+ * Determines which buildId to download. BuildId should uniquely identify
+ * binaries and they are used for caching.
+ */
+ buildId: string;
+ /**
+ * Provides information about the progress of the download.
+ */
+ downloadProgressCallback?: (
+ downloadedBytes: number,
+ totalBytes: number
+ ) => void;
+ /**
+ * Determines the host that will be used for downloading.
+ *
+ * @defaultValue Either
+ *
+ * - https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing or
+ * - https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central
+ *
+ */
+ baseUrl?: string;
+ /**
+ * Whether to unpack and install browser archives.
+ *
+ * @defaultValue `true`
+ */
+ unpack?: boolean;
+}
+
+/**
+ * @public
+ */
+export function install(
+ options: InstallOptions & {unpack?: true}
+): Promise<InstalledBrowser>;
+/**
+ * @public
+ */
+export function install(
+ options: InstallOptions & {unpack: false}
+): Promise<string>;
+export async function install(
+ options: InstallOptions
+): Promise<InstalledBrowser | string> {
+ options.platform ??= detectBrowserPlatform();
+ options.unpack ??= true;
+ if (!options.platform) {
+ throw new Error(
+ `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
+ );
+ }
+ const url = getDownloadUrl(
+ options.browser,
+ options.platform,
+ options.buildId,
+ options.baseUrl
+ );
+ const fileName = url.toString().split('/').pop();
+ assert(fileName, `A malformed download URL was found: ${url}.`);
+ const cache = new Cache(options.cacheDir);
+ const browserRoot = cache.browserRoot(options.browser);
+ const archivePath = path.join(browserRoot, `${options.buildId}-${fileName}`);
+ if (!existsSync(browserRoot)) {
+ await mkdir(browserRoot, {recursive: true});
+ }
+
+ if (!options.unpack) {
+ if (existsSync(archivePath)) {
+ return archivePath;
+ }
+ debugInstall(`Downloading binary from ${url}`);
+ debugTime('download');
+ await downloadFile(url, archivePath, options.downloadProgressCallback);
+ debugTimeEnd('download');
+ return archivePath;
+ }
+
+ const outputPath = cache.installationDir(
+ options.browser,
+ options.platform,
+ options.buildId
+ );
+ if (existsSync(outputPath)) {
+ return new InstalledBrowser(
+ cache,
+ options.browser,
+ options.buildId,
+ options.platform
+ );
+ }
+ try {
+ debugInstall(`Downloading binary from ${url}`);
+ try {
+ debugTime('download');
+ await downloadFile(url, archivePath, options.downloadProgressCallback);
+ } finally {
+ debugTimeEnd('download');
+ }
+
+ debugInstall(`Installing ${archivePath} to ${outputPath}`);
+ try {
+ debugTime('extract');
+ await unpackArchive(archivePath, outputPath);
+ } finally {
+ debugTimeEnd('extract');
+ }
+ } finally {
+ if (existsSync(archivePath)) {
+ await unlink(archivePath);
+ }
+ }
+ return new InstalledBrowser(
+ cache,
+ options.browser,
+ options.buildId,
+ options.platform
+ );
+}
+
+/**
+ * @public
+ */
+export interface UninstallOptions {
+ /**
+ * Determines the platform for the browser binary.
+ *
+ * @defaultValue **Auto-detected.**
+ */
+ platform?: BrowserPlatform;
+ /**
+ * The path to the root of the cache directory.
+ */
+ cacheDir: string;
+ /**
+ * Determines which browser to uninstall.
+ */
+ browser: Browser;
+ /**
+ * The browser build to uninstall
+ */
+ buildId: string;
+}
+
+/**
+ *
+ * @public
+ */
+export async function uninstall(options: UninstallOptions): Promise<void> {
+ options.platform ??= detectBrowserPlatform();
+ if (!options.platform) {
+ throw new Error(
+ `Cannot detect the browser platform for: ${os.platform()} (${os.arch()})`
+ );
+ }
+
+ new Cache(options.cacheDir).uninstall(
+ options.browser,
+ options.platform,
+ options.buildId
+ );
+}
+
+/**
+ * @public
+ */
+export interface GetInstalledBrowsersOptions {
+ /**
+ * The path to the root of the cache directory.
+ */
+ cacheDir: string;
+}
+
+/**
+ * Returns metadata about browsers installed in the cache directory.
+ *
+ * @public
+ */
+export async function getInstalledBrowsers(
+ options: GetInstalledBrowsersOptions
+): Promise<InstalledBrowser[]> {
+ return new Cache(options.cacheDir).getInstalledBrowsers();
+}
+
+/**
+ * @public
+ */
+export async function canDownload(options: InstallOptions): Promise<boolean> {
+ options.platform ??= detectBrowserPlatform();
+ if (!options.platform) {
+ throw new Error(
+ `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
+ );
+ }
+ return await headHttpRequest(
+ getDownloadUrl(
+ options.browser,
+ options.platform,
+ options.buildId,
+ options.baseUrl
+ )
+ );
+}
+
+function getDownloadUrl(
+ browser: Browser,
+ platform: BrowserPlatform,
+ buildId: string,
+ baseUrl?: string
+): URL {
+ return new URL(downloadUrls[browser](platform, buildId, baseUrl));
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/launch.ts b/remote/test/puppeteer/packages/browsers/src/launch.ts
new file mode 100644
index 0000000000..dfb0fbf633
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/launch.ts
@@ -0,0 +1,479 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import childProcess from 'child_process';
+import {accessSync} from 'fs';
+import os from 'os';
+import readline from 'readline';
+
+import {
+ type Browser,
+ type BrowserPlatform,
+ resolveSystemExecutablePath,
+ type ChromeReleaseChannel,
+} from './browser-data/browser-data.js';
+import {Cache} from './Cache.js';
+import {debug} from './debug.js';
+import {detectBrowserPlatform} from './detectPlatform.js';
+
+const debugLaunch = debug('puppeteer:browsers:launcher');
+
+/**
+ * @public
+ */
+export interface ComputeExecutablePathOptions {
+ /**
+ * Root path to the storage directory.
+ */
+ cacheDir: string;
+ /**
+ * Determines which platform the browser will be suited for.
+ *
+ * @defaultValue **Auto-detected.**
+ */
+ platform?: BrowserPlatform;
+ /**
+ * Determines which browser to launch.
+ */
+ browser: Browser;
+ /**
+ * Determines which buildId to download. BuildId should uniquely identify
+ * binaries and they are used for caching.
+ */
+ buildId: string;
+}
+
+/**
+ * @public
+ */
+export function computeExecutablePath(
+ options: ComputeExecutablePathOptions
+): string {
+ return new Cache(options.cacheDir).computeExecutablePath(options);
+}
+
+/**
+ * @public
+ */
+export interface SystemOptions {
+ /**
+ * Determines which platform the browser will be suited for.
+ *
+ * @defaultValue **Auto-detected.**
+ */
+ platform?: BrowserPlatform;
+ /**
+ * Determines which browser to launch.
+ */
+ browser: Browser;
+ /**
+ * Release channel to look for on the system.
+ */
+ channel: ChromeReleaseChannel;
+}
+
+/**
+ * @public
+ */
+export function computeSystemExecutablePath(options: SystemOptions): string {
+ options.platform ??= detectBrowserPlatform();
+ if (!options.platform) {
+ throw new Error(
+ `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
+ );
+ }
+ const path = resolveSystemExecutablePath(
+ options.browser,
+ options.platform,
+ options.channel
+ );
+ try {
+ accessSync(path);
+ } catch (error) {
+ throw new Error(
+ `Could not find Google Chrome executable for channel '${options.channel}' at '${path}'.`
+ );
+ }
+ return path;
+}
+
+/**
+ * @public
+ */
+export interface LaunchOptions {
+ executablePath: string;
+ pipe?: boolean;
+ dumpio?: boolean;
+ args?: string[];
+ env?: Record<string, string | undefined>;
+ handleSIGINT?: boolean;
+ handleSIGTERM?: boolean;
+ handleSIGHUP?: boolean;
+ detached?: boolean;
+ onExit?: () => Promise<void>;
+}
+
+/**
+ * @public
+ */
+export function launch(opts: LaunchOptions): Process {
+ return new Process(opts);
+}
+
+/**
+ * @public
+ */
+export const CDP_WEBSOCKET_ENDPOINT_REGEX =
+ /^DevTools listening on (ws:\/\/.*)$/;
+
+/**
+ * @public
+ */
+export const WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX =
+ /^WebDriver BiDi listening on (ws:\/\/.*)$/;
+
+/**
+ * @public
+ */
+export class Process {
+ #executablePath;
+ #args: string[];
+ #browserProcess: childProcess.ChildProcess;
+ #exited = false;
+ // The browser process can be closed externally or from the driver process. We
+ // need to invoke the hooks only once though but we don't know how many times
+ // we will be invoked.
+ #hooksRan = false;
+ #onExitHook = async () => {};
+ #browserProcessExiting: Promise<void>;
+
+ constructor(opts: LaunchOptions) {
+ this.#executablePath = opts.executablePath;
+ this.#args = opts.args ?? [];
+
+ opts.pipe ??= false;
+ opts.dumpio ??= false;
+ opts.handleSIGINT ??= true;
+ opts.handleSIGTERM ??= true;
+ opts.handleSIGHUP ??= true;
+ // On non-windows platforms, `detached: true` makes child process a
+ // leader of a new process group, making it possible to kill child
+ // process tree with `.kill(-pid)` command. @see
+ // https://nodejs.org/api/child_process.html#child_process_options_detached
+ opts.detached ??= process.platform !== 'win32';
+
+ const stdio = this.#configureStdio({
+ pipe: opts.pipe,
+ dumpio: opts.dumpio,
+ });
+
+ const env = opts.env || {};
+
+ debugLaunch(`Launching ${this.#executablePath} ${this.#args.join(' ')}`, {
+ detached: opts.detached,
+ env: Object.keys(env).reduce<Record<string, string | undefined>>(
+ (res, key) => {
+ if (key.toLowerCase().startsWith('puppeteer_')) {
+ res[key] = env[key];
+ }
+ return res;
+ },
+ {}
+ ),
+ stdio,
+ });
+
+ this.#browserProcess = childProcess.spawn(
+ this.#executablePath,
+ this.#args,
+ {
+ detached: opts.detached,
+ env,
+ stdio,
+ }
+ );
+
+ debugLaunch(`Launched ${this.#browserProcess.pid}`);
+ if (opts.dumpio) {
+ this.#browserProcess.stderr?.pipe(process.stderr);
+ this.#browserProcess.stdout?.pipe(process.stdout);
+ }
+ process.on('exit', this.#onDriverProcessExit);
+ if (opts.handleSIGINT) {
+ process.on('SIGINT', this.#onDriverProcessSignal);
+ }
+ if (opts.handleSIGTERM) {
+ process.on('SIGTERM', this.#onDriverProcessSignal);
+ }
+ if (opts.handleSIGHUP) {
+ process.on('SIGHUP', this.#onDriverProcessSignal);
+ }
+ if (opts.onExit) {
+ this.#onExitHook = opts.onExit;
+ }
+ this.#browserProcessExiting = new Promise((resolve, reject) => {
+ this.#browserProcess.once('exit', async () => {
+ debugLaunch(`Browser process ${this.#browserProcess.pid} onExit`);
+ this.#clearListeners();
+ this.#exited = true;
+ try {
+ await this.#runHooks();
+ } catch (err) {
+ reject(err);
+ return;
+ }
+ resolve();
+ });
+ });
+ }
+
+ async #runHooks() {
+ if (this.#hooksRan) {
+ return;
+ }
+ this.#hooksRan = true;
+ await this.#onExitHook();
+ }
+
+ get nodeProcess(): childProcess.ChildProcess {
+ return this.#browserProcess;
+ }
+
+ #configureStdio(opts: {
+ pipe: boolean;
+ dumpio: boolean;
+ }): Array<'ignore' | 'pipe'> {
+ if (opts.pipe) {
+ if (opts.dumpio) {
+ return ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
+ } else {
+ return ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
+ }
+ } else {
+ if (opts.dumpio) {
+ return ['pipe', 'pipe', 'pipe'];
+ } else {
+ return ['pipe', 'ignore', 'pipe'];
+ }
+ }
+ }
+
+ #clearListeners(): void {
+ process.off('exit', this.#onDriverProcessExit);
+ process.off('SIGINT', this.#onDriverProcessSignal);
+ process.off('SIGTERM', this.#onDriverProcessSignal);
+ process.off('SIGHUP', this.#onDriverProcessSignal);
+ }
+
+ #onDriverProcessExit = (_code: number) => {
+ this.kill();
+ };
+
+ #onDriverProcessSignal = (signal: string): void => {
+ switch (signal) {
+ case 'SIGINT':
+ this.kill();
+ process.exit(130);
+ case 'SIGTERM':
+ case 'SIGHUP':
+ void this.close();
+ break;
+ }
+ };
+
+ async close(): Promise<void> {
+ await this.#runHooks();
+ if (!this.#exited) {
+ this.kill();
+ }
+ return await this.#browserProcessExiting;
+ }
+
+ hasClosed(): Promise<void> {
+ return this.#browserProcessExiting;
+ }
+
+ kill(): void {
+ debugLaunch(`Trying to kill ${this.#browserProcess.pid}`);
+ // If the process failed to launch (for example if the browser executable path
+ // is invalid), then the process does not get a pid assigned. A call to
+ // `proc.kill` would error, as the `pid` to-be-killed can not be found.
+ if (
+ this.#browserProcess &&
+ this.#browserProcess.pid &&
+ pidExists(this.#browserProcess.pid)
+ ) {
+ try {
+ debugLaunch(`Browser process ${this.#browserProcess.pid} exists`);
+ if (process.platform === 'win32') {
+ try {
+ childProcess.execSync(
+ `taskkill /pid ${this.#browserProcess.pid} /T /F`
+ );
+ } catch (error) {
+ debugLaunch(
+ `Killing ${this.#browserProcess.pid} using taskkill failed`,
+ error
+ );
+ // taskkill can fail to kill the process e.g. due to missing permissions.
+ // Let's kill the process via Node API. This delays killing of all child
+ // processes of `this.proc` until the main Node.js process dies.
+ this.#browserProcess.kill();
+ }
+ } else {
+ // on linux the process group can be killed with the group id prefixed with
+ // a minus sign. The process group id is the group leader's pid.
+ const processGroupId = -this.#browserProcess.pid;
+
+ try {
+ process.kill(processGroupId, 'SIGKILL');
+ } catch (error) {
+ debugLaunch(
+ `Killing ${this.#browserProcess.pid} using process.kill failed`,
+ error
+ );
+ // Killing the process group can fail due e.g. to missing permissions.
+ // Let's kill the process via Node API. This delays killing of all child
+ // processes of `this.proc` until the main Node.js process dies.
+ this.#browserProcess.kill('SIGKILL');
+ }
+ }
+ } catch (error) {
+ throw new Error(
+ `${PROCESS_ERROR_EXPLANATION}\nError cause: ${
+ isErrorLike(error) ? error.stack : error
+ }`
+ );
+ }
+ }
+ this.#clearListeners();
+ }
+
+ waitForLineOutput(regex: RegExp, timeout = 0): Promise<string> {
+ if (!this.#browserProcess.stderr) {
+ throw new Error('`browserProcess` does not have stderr.');
+ }
+ const rl = readline.createInterface(this.#browserProcess.stderr);
+ let stderr = '';
+
+ return new Promise((resolve, reject) => {
+ rl.on('line', onLine);
+ rl.on('close', onClose);
+ this.#browserProcess.on('exit', onClose);
+ this.#browserProcess.on('error', onClose);
+ const timeoutId =
+ timeout > 0 ? setTimeout(onTimeout, timeout) : undefined;
+
+ const cleanup = (): void => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ rl.off('line', onLine);
+ rl.off('close', onClose);
+ this.#browserProcess.off('exit', onClose);
+ this.#browserProcess.off('error', onClose);
+ };
+
+ function onClose(error?: Error): void {
+ cleanup();
+ reject(
+ new Error(
+ [
+ `Failed to launch the browser process!${
+ error ? ' ' + error.message : ''
+ }`,
+ stderr,
+ '',
+ 'TROUBLESHOOTING: https://pptr.dev/troubleshooting',
+ '',
+ ].join('\n')
+ )
+ );
+ }
+
+ function onTimeout(): void {
+ cleanup();
+ reject(
+ new TimeoutError(
+ `Timed out after ${timeout} ms while waiting for the WS endpoint URL to appear in stdout!`
+ )
+ );
+ }
+
+ function onLine(line: string): void {
+ stderr += line + '\n';
+ const match = line.match(regex);
+ if (!match) {
+ return;
+ }
+ cleanup();
+ // The RegExp matches, so this will obviously exist.
+ resolve(match[1]!);
+ }
+ });
+ }
+}
+
+const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary.
+This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser.
+Please check your open processes and ensure that the browser processes that Puppeteer launched have been killed.
+If you think this is a bug, please report it on the Puppeteer issue tracker.`;
+
+/**
+ * @internal
+ */
+function pidExists(pid: number): boolean {
+ try {
+ return process.kill(pid, 0);
+ } catch (error) {
+ if (isErrnoException(error)) {
+ if (error.code && error.code === 'ESRCH') {
+ return false;
+ }
+ }
+ throw error;
+ }
+}
+
+/**
+ * @internal
+ */
+export interface ErrorLike extends Error {
+ name: string;
+ message: string;
+}
+
+/**
+ * @internal
+ */
+export function isErrorLike(obj: unknown): obj is ErrorLike {
+ return (
+ typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj
+ );
+}
+/**
+ * @internal
+ */
+export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException {
+ return (
+ isErrorLike(obj) &&
+ ('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj)
+ );
+}
+
+/**
+ * @public
+ */
+export class TimeoutError extends Error {
+ /**
+ * @internal
+ */
+ constructor(message?: string) {
+ super(message);
+ this.name = this.constructor.name;
+ Error.captureStackTrace(this, this.constructor);
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/main-cli.ts b/remote/test/puppeteer/packages/browsers/src/main-cli.ts
new file mode 100644
index 0000000000..9919a4dfb7
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/main-cli.ts
@@ -0,0 +1,11 @@
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {CLI} from './CLI.js';
+
+void new CLI().run(process.argv);
diff --git a/remote/test/puppeteer/packages/browsers/src/main.ts b/remote/test/puppeteer/packages/browsers/src/main.ts
new file mode 100644
index 0000000000..df93de530d
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/main.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export type {
+ LaunchOptions,
+ ComputeExecutablePathOptions as Options,
+ SystemOptions,
+} from './launch.js';
+export {
+ launch,
+ computeExecutablePath,
+ computeSystemExecutablePath,
+ TimeoutError,
+ CDP_WEBSOCKET_ENDPOINT_REGEX,
+ WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
+ Process,
+} from './launch.js';
+export type {
+ InstallOptions,
+ GetInstalledBrowsersOptions,
+ UninstallOptions,
+} from './install.js';
+export {
+ install,
+ getInstalledBrowsers,
+ canDownload,
+ uninstall,
+} from './install.js';
+export {detectBrowserPlatform} from './detectPlatform.js';
+export type {ProfileOptions} from './browser-data/browser-data.js';
+export {
+ resolveBuildId,
+ Browser,
+ BrowserPlatform,
+ ChromeReleaseChannel,
+ createProfile,
+} from './browser-data/browser-data.js';
+export {CLI, makeProgressCallback} from './CLI.js';
+export {Cache, InstalledBrowser} from './Cache.js';
diff --git a/remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json b/remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json
new file mode 100644
index 0000000000..acb1968862
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "../lib/cjs"
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/src/tsconfig.esm.json b/remote/test/puppeteer/packages/browsers/src/tsconfig.esm.json
new file mode 100644
index 0000000000..a824bc8cb8
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/src/tsconfig.esm.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../lib/esm"
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/chrome-headless-shell-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/chrome-headless-shell-data.spec.ts
new file mode 100644
index 0000000000..65008b5edb
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/chrome-headless-shell-data.spec.ts
@@ -0,0 +1,72 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import path from 'path';
+
+import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js';
+import {
+ resolveDownloadUrl,
+ relativeExecutablePath,
+ resolveBuildId,
+} from '../../../lib/cjs/browser-data/chrome-headless-shell.js';
+
+describe('chrome-headless-shell', () => {
+ it('should resolve download URLs', () => {
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.LINUX, '118.0.5950.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/linux64/chrome-headless-shell-linux64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC, '118.0.5950.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/mac-x64/chrome-headless-shell-mac-x64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC_ARM, '118.0.5950.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/mac-arm64/chrome-headless-shell-mac-arm64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN32, '118.0.5950.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/win32/chrome-headless-shell-win32.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN64, '118.0.5950.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/win64/chrome-headless-shell-win64.zip'
+ );
+ });
+
+ // TODO: once no new releases happen for the milestone, we can use the exact match.
+ it('should resolve milestones', async () => {
+ assert((await resolveBuildId('118'))?.startsWith('118.0'));
+ });
+
+ it('should resolve build prefix', async () => {
+ assert.strictEqual(await resolveBuildId('118.0.5950'), '118.0.5950.0');
+ });
+
+ it('should resolve executable paths', () => {
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.LINUX, '12372323'),
+ path.join('chrome-headless-shell-linux64', 'chrome-headless-shell')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC, '12372323'),
+ path.join('chrome-headless-shell-mac-x64/', 'chrome-headless-shell')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'),
+ path.join('chrome-headless-shell-mac-arm64', 'chrome-headless-shell')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN32, '12372323'),
+ path.join('chrome-headless-shell-win32', 'chrome-headless-shell.exe')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN64, '12372323'),
+ path.join('chrome-headless-shell-win64', 'chrome-headless-shell.exe')
+ );
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/cli.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/cli.spec.ts
new file mode 100644
index 0000000000..445d0f700e
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/cli.spec.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {CLI} from '../../../lib/cjs/CLI.js';
+import {
+ createMockedReadlineInterface,
+ setupTestServer,
+ getServerUrl,
+} from '../utils.js';
+import {testChromeHeadlessShellBuildId} from '../versions.js';
+
+describe('chrome-headless-shell CLI', function () {
+ this.timeout(90000);
+
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(async () => {
+ await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'clear',
+ `--path=${tmpDir}`,
+ `--base-url=${getServerUrl()}`,
+ ]);
+ });
+
+ it('should download chrome-headless-shell binaries', async () => {
+ await new CLI(tmpDir).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'install',
+ `chrome-headless-shell@${testChromeHeadlessShellBuildId}`,
+ `--path=${tmpDir}`,
+ '--platform=linux',
+ `--base-url=${getServerUrl()}`,
+ ]);
+ assert.ok(
+ fs.existsSync(
+ path.join(
+ tmpDir,
+ 'chrome-headless-shell',
+ `linux-${testChromeHeadlessShellBuildId}`,
+ 'chrome-headless-shell-linux64',
+ 'chrome-headless-shell'
+ )
+ )
+ );
+
+ await new CLI(tmpDir, createMockedReadlineInterface('no')).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'clear',
+ `--path=${tmpDir}`,
+ ]);
+ assert.ok(
+ fs.existsSync(
+ path.join(
+ tmpDir,
+ 'chrome-headless-shell',
+ `linux-${testChromeHeadlessShellBuildId}`,
+ 'chrome-headless-shell-linux64',
+ 'chrome-headless-shell'
+ )
+ )
+ );
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/install.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/install.spec.ts
new file mode 100644
index 0000000000..88f9fae7fc
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chrome-headless-shell/install.spec.ts
@@ -0,0 +1,93 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ install,
+ canDownload,
+ Browser,
+ BrowserPlatform,
+ Cache,
+} from '../../../lib/cjs/main.js';
+import {getServerUrl, setupTestServer} from '../utils.js';
+import {testChromeDriverBuildId} from '../versions.js';
+
+/**
+ * Tests in this spec use real download URLs and unpack live browser archives
+ * so it requires the network access.
+ */
+describe('ChromeDriver install', () => {
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(() => {
+ new Cache(tmpDir).clear();
+ });
+
+ it('should check if a buildId can be downloaded', async () => {
+ assert.ok(
+ await canDownload({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMEDRIVER,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeDriverBuildId,
+ baseUrl: getServerUrl(),
+ })
+ );
+ });
+
+ it('should report if a buildId is not downloadable', async () => {
+ assert.strictEqual(
+ await canDownload({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMEDRIVER,
+ platform: BrowserPlatform.LINUX,
+ buildId: 'unknown',
+ baseUrl: getServerUrl(),
+ }),
+ false
+ );
+ });
+
+ it('should download and unpack the binary', async function () {
+ this.timeout(60000);
+ const expectedOutputPath = path.join(
+ tmpDir,
+ 'chromedriver',
+ `${BrowserPlatform.LINUX}-${testChromeDriverBuildId}`
+ );
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ let browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMEDRIVER,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeDriverBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ // Second iteration should be no-op.
+ browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMEDRIVER,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeDriverBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ assert.ok(fs.existsSync(browser.executablePath));
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts
new file mode 100644
index 0000000000..510afa8454
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts
@@ -0,0 +1,119 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import path from 'path';
+
+import {
+ BrowserPlatform,
+ ChromeReleaseChannel,
+} from '../../../lib/cjs/browser-data/browser-data.js';
+import {
+ resolveDownloadUrl,
+ relativeExecutablePath,
+ resolveSystemExecutablePath,
+ resolveBuildId,
+} from '../../../lib/cjs/browser-data/chrome.js';
+
+describe('Chrome', () => {
+ it('should resolve download URLs', () => {
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.LINUX, '113.0.5672.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/linux64/chrome-linux64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC, '113.0.5672.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-x64/chrome-mac-x64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC_ARM, '113.0.5672.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-arm64/chrome-mac-arm64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN32, '113.0.5672.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win32/chrome-win32.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN64, '113.0.5672.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win64/chrome-win64.zip'
+ );
+ });
+
+ it('should resolve executable paths', () => {
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.LINUX, '12372323'),
+ path.join('chrome-linux64', 'chrome')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC, '12372323'),
+ path.join(
+ 'chrome-mac-x64',
+ 'Google Chrome for Testing.app',
+ 'Contents',
+ 'MacOS',
+ 'Google Chrome for Testing'
+ )
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'),
+ path.join(
+ 'chrome-mac-arm64',
+ 'Google Chrome for Testing.app',
+ 'Contents',
+ 'MacOS',
+ 'Google Chrome for Testing'
+ )
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN32, '12372323'),
+ path.join('chrome-win32', 'chrome.exe')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN64, '12372323'),
+ path.join('chrome-win64', 'chrome.exe')
+ );
+ });
+
+ it('should resolve system executable path', () => {
+ process.env['PROGRAMFILES'] = 'C:\\ProgramFiles';
+ try {
+ assert.strictEqual(
+ resolveSystemExecutablePath(
+ BrowserPlatform.WIN32,
+ ChromeReleaseChannel.DEV
+ ),
+ 'C:\\ProgramFiles\\Google\\Chrome Dev\\Application\\chrome.exe'
+ );
+ } finally {
+ delete process.env['PROGRAMFILES'];
+ }
+
+ assert.strictEqual(
+ resolveSystemExecutablePath(
+ BrowserPlatform.MAC,
+ ChromeReleaseChannel.BETA
+ ),
+ '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta'
+ );
+ assert.throws(() => {
+ assert.strictEqual(
+ resolveSystemExecutablePath(
+ BrowserPlatform.LINUX,
+ ChromeReleaseChannel.CANARY
+ ),
+ path.join('chrome-linux', 'chrome')
+ );
+ }, new Error(`Unable to detect browser executable path for 'canary' on linux.`));
+ });
+
+ it('should resolve milestones', async () => {
+ assert.strictEqual(await resolveBuildId('115'), '115.0.5790.170');
+ });
+
+ it('should resolve build prefix', async () => {
+ assert.strictEqual(await resolveBuildId('115.0.5790'), '115.0.5790.170');
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts
new file mode 100644
index 0000000000..bdda9d9aa9
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts
@@ -0,0 +1,94 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {CLI} from '../../../lib/cjs/CLI.js';
+import {
+ createMockedReadlineInterface,
+ setupTestServer,
+ getServerUrl,
+} from '../utils.js';
+import {testChromeBuildId} from '../versions.js';
+
+describe('Chrome CLI', function () {
+ this.timeout(90000);
+
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(async () => {
+ await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'clear',
+ `--path=${tmpDir}`,
+ `--base-url=${getServerUrl()}`,
+ ]);
+ });
+
+ it('should download Chrome binaries', async () => {
+ await new CLI(tmpDir).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'install',
+ `chrome@${testChromeBuildId}`,
+ `--path=${tmpDir}`,
+ '--platform=linux',
+ `--base-url=${getServerUrl()}`,
+ ]);
+ assert.ok(
+ fs.existsSync(
+ path.join(
+ tmpDir,
+ 'chrome',
+ `linux-${testChromeBuildId}`,
+ 'chrome-linux64',
+ 'chrome'
+ )
+ )
+ );
+
+ await new CLI(tmpDir, createMockedReadlineInterface('no')).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'clear',
+ `--path=${tmpDir}`,
+ ]);
+ assert.ok(
+ fs.existsSync(
+ path.join(
+ tmpDir,
+ 'chrome',
+ `linux-${testChromeBuildId}`,
+ 'chrome-linux64',
+ 'chrome'
+ )
+ )
+ );
+ });
+
+ // Skipped because the current latest is not published yet.
+ it.skip('should download latest Chrome binaries', async () => {
+ await new CLI(tmpDir).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'install',
+ `chrome@latest`,
+ `--path=${tmpDir}`,
+ '--platform=linux',
+ `--base-url=${getServerUrl()}`,
+ ]);
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts
new file mode 100644
index 0000000000..8103ff3612
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts
@@ -0,0 +1,233 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import http from 'http';
+import https from 'https';
+import os from 'os';
+import path from 'path';
+
+import {
+ install,
+ canDownload,
+ Browser,
+ BrowserPlatform,
+ Cache,
+} from '../../../lib/cjs/main.js';
+import {getServerUrl, setupTestServer} from '../utils.js';
+import {testChromeBuildId} from '../versions.js';
+
+/**
+ * Tests in this spec use real download URLs and unpack live browser archives
+ * so it requires the network access.
+ */
+describe('Chrome install', () => {
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(() => {
+ new Cache(tmpDir).clear();
+ });
+
+ it('should check if a buildId can be downloaded', async () => {
+ assert.ok(
+ await canDownload({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeBuildId,
+ baseUrl: getServerUrl(),
+ })
+ );
+ });
+
+ it('should report if a buildId is not downloadable', async () => {
+ assert.strictEqual(
+ await canDownload({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: 'unknown',
+ baseUrl: getServerUrl(),
+ }),
+ false
+ );
+ });
+
+ it('should download a buildId that is a zip archive', async function () {
+ this.timeout(60000);
+ const expectedOutputPath = path.join(
+ tmpDir,
+ 'chrome',
+ `${BrowserPlatform.LINUX}-${testChromeBuildId}`
+ );
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ let browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ // Second iteration should be no-op.
+ browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ // Should discover installed browsers.
+ const cache = new Cache(tmpDir);
+ const installed = cache.getInstalledBrowsers();
+ assert.deepStrictEqual(browser, installed[0]);
+ assert.deepStrictEqual(
+ browser!.executablePath,
+ installed[0]?.executablePath
+ );
+ });
+
+ it('throws on invalid URL', async function () {
+ const expectedOutputPath = path.join(
+ tmpDir,
+ 'chrome',
+ `${BrowserPlatform.LINUX}-${testChromeBuildId}`
+ );
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+
+ async function installThatThrows(): Promise<unknown> {
+ try {
+ await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeBuildId,
+ baseUrl: 'https://127.0.0.1',
+ });
+ return undefined;
+ } catch (err) {
+ return err;
+ }
+ }
+ assert.ok(await installThatThrows());
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ });
+
+ describe('with proxy', () => {
+ const proxyUrl = new URL(`http://localhost:54321`);
+ let proxyServer: http.Server;
+ let proxiedRequestUrls: string[] = [];
+ let proxiedRequestHosts: string[] = [];
+
+ beforeEach(() => {
+ proxiedRequestUrls = [];
+ proxiedRequestHosts = [];
+ proxyServer = http
+ .createServer(
+ (
+ originalRequest: http.IncomingMessage,
+ originalResponse: http.ServerResponse
+ ) => {
+ const url = originalRequest.url as string;
+ const proxyRequest = (
+ url.startsWith('http:') ? http : https
+ ).request(
+ url,
+ {
+ method: originalRequest.method,
+ rejectUnauthorized: false,
+ },
+ proxyResponse => {
+ originalResponse.writeHead(
+ proxyResponse.statusCode as number,
+ proxyResponse.headers
+ );
+ proxyResponse.pipe(originalResponse, {end: true});
+ }
+ );
+ originalRequest.pipe(proxyRequest, {end: true});
+ proxiedRequestUrls.push(url);
+ proxiedRequestHosts.push(originalRequest.headers?.host || '');
+ }
+ )
+ .listen({
+ port: proxyUrl.port,
+ hostname: proxyUrl.hostname,
+ });
+
+ process.env['HTTPS_PROXY'] = proxyUrl.toString();
+ process.env['HTTP_PROXY'] = proxyUrl.toString();
+ });
+
+ afterEach(async () => {
+ await new Promise((resolve, reject) => {
+ proxyServer.close(error => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(undefined);
+ }
+ });
+ });
+ delete process.env['HTTP_PROXY'];
+ delete process.env['HTTPS_PROXY'];
+ });
+
+ it('can send canDownload requests via a proxy', async () => {
+ assert.strictEqual(
+ await canDownload({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeBuildId,
+ baseUrl: getServerUrl(),
+ }),
+ true
+ );
+ assert.deepStrictEqual(proxiedRequestUrls, [
+ getServerUrl() + '/113.0.5672.0/linux64/chrome-linux64.zip',
+ ]);
+ assert.deepStrictEqual(proxiedRequestHosts, [
+ getServerUrl().replace('http://', ''),
+ ]);
+ });
+
+ it('can download via a proxy', async function () {
+ this.timeout(120000);
+ const expectedOutputPath = path.join(
+ tmpDir,
+ 'chrome',
+ `${BrowserPlatform.LINUX}-${testChromeBuildId}`
+ );
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ const browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ assert.deepStrictEqual(proxiedRequestUrls, [
+ getServerUrl() + '/113.0.5672.0/linux64/chrome-linux64.zip',
+ ]);
+ assert.deepStrictEqual(proxiedRequestHosts, [
+ getServerUrl().replace('http://', ''),
+ ]);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts
new file mode 100644
index 0000000000..c420d9e0b6
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts
@@ -0,0 +1,122 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ CDP_WEBSOCKET_ENDPOINT_REGEX,
+ computeExecutablePath,
+ launch,
+ install,
+ Browser,
+ BrowserPlatform,
+} from '../../../lib/cjs/main.js';
+import {getServerUrl, setupTestServer, clearCache} from '../utils.js';
+import {testChromeBuildId} from '../versions.js';
+
+describe('Chrome', () => {
+ it('should compute executable path for Chrome', () => {
+ assert.strictEqual(
+ computeExecutablePath({
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: '123',
+ cacheDir: '.cache',
+ }),
+ path.join('.cache', 'chrome', 'linux-123', 'chrome-linux64', 'chrome')
+ );
+ });
+
+ describe('launcher', function () {
+ setupTestServer();
+
+ this.timeout(60000);
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(async () => {
+ tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'puppeteer-browsers-test')
+ );
+ await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ buildId: testChromeBuildId,
+ baseUrl: getServerUrl(),
+ });
+ });
+
+ afterEach(() => {
+ clearCache(tmpDir);
+ });
+
+ function getArgs() {
+ return [
+ '--allow-pre-commit-input',
+ '--disable-background-networking',
+ '--disable-background-timer-throttling',
+ '--disable-backgrounding-occluded-windows',
+ '--disable-breakpad',
+ '--disable-client-side-phishing-detection',
+ '--disable-component-extensions-with-background-pages',
+ '--disable-component-update',
+ '--disable-default-apps',
+ '--disable-dev-shm-usage',
+ '--disable-extensions',
+ '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,DialMediaRouteProvider',
+ '--disable-hang-monitor',
+ '--disable-ipc-flooding-protection',
+ '--disable-popup-blocking',
+ '--disable-prompt-on-repost',
+ '--disable-renderer-backgrounding',
+ '--disable-sync',
+ '--enable-automation',
+ '--enable-features=NetworkServiceInProcess2',
+ '--export-tagged-pdf',
+ '--force-color-profile=srgb',
+ '--headless=new',
+ '--metrics-recording-only',
+ '--no-first-run',
+ '--password-store=basic',
+ '--remote-debugging-port=9222',
+ '--use-mock-keychain',
+ `--user-data-dir=${path.join(tmpDir, 'profile')}`,
+ 'about:blank',
+ ];
+ }
+
+ it('should launch a Chrome browser', async () => {
+ const executablePath = computeExecutablePath({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ buildId: testChromeBuildId,
+ });
+ const process = launch({
+ executablePath,
+ args: getArgs(),
+ });
+ await process.close();
+ });
+
+ it('should allow parsing stderr output of the browser process', async () => {
+ const executablePath = computeExecutablePath({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ buildId: testChromeBuildId,
+ });
+ const process = launch({
+ executablePath,
+ args: getArgs(),
+ });
+ const url = await process.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX);
+ await process.close();
+ assert.ok(url.startsWith('ws://127.0.0.1:9222/devtools/browser'));
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts
new file mode 100644
index 0000000000..62522d88f4
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts
@@ -0,0 +1,71 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import path from 'path';
+
+import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js';
+import {
+ resolveDownloadUrl,
+ relativeExecutablePath,
+ resolveBuildId,
+} from '../../../lib/cjs/browser-data/chromedriver.js';
+
+describe('ChromeDriver', () => {
+ it('should resolve download URLs', () => {
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.LINUX, '115.0.5763.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5763.0/linux64/chromedriver-linux64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC, '115.0.5763.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5763.0/mac-x64/chromedriver-mac-x64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC_ARM, '115.0.5763.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5763.0/mac-arm64/chromedriver-mac-arm64.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN32, '115.0.5763.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5763.0/win32/chromedriver-win32.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN64, '115.0.5763.0'),
+ 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5763.0/win64/chromedriver-win64.zip'
+ );
+ });
+
+ it('should resolve milestones', async () => {
+ assert.strictEqual(await resolveBuildId('115'), '115.0.5790.170');
+ });
+
+ it('should resolve build prefix', async () => {
+ assert.strictEqual(await resolveBuildId('115.0.5790'), '115.0.5790.170');
+ });
+
+ it('should resolve executable paths', () => {
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.LINUX, '12372323'),
+ path.join('chromedriver-linux64', 'chromedriver')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC, '12372323'),
+ path.join('chromedriver-mac-x64/', 'chromedriver')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'),
+ path.join('chromedriver-mac-arm64', 'chromedriver')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN32, '12372323'),
+ path.join('chromedriver-win32', 'chromedriver.exe')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN64, '12372323'),
+ path.join('chromedriver-win64', 'chromedriver.exe')
+ );
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts
new file mode 100644
index 0000000000..d407062a88
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {CLI} from '../../../lib/cjs/CLI.js';
+import {
+ createMockedReadlineInterface,
+ setupTestServer,
+ getServerUrl,
+} from '../utils.js';
+import {testChromeDriverBuildId} from '../versions.js';
+
+describe('ChromeDriver CLI', function () {
+ this.timeout(90000);
+
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(async () => {
+ await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'clear',
+ `--path=${tmpDir}`,
+ `--base-url=${getServerUrl()}`,
+ ]);
+ });
+
+ it('should download ChromeDriver binaries', async () => {
+ await new CLI(tmpDir).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'install',
+ `chromedriver@${testChromeDriverBuildId}`,
+ `--path=${tmpDir}`,
+ '--platform=linux',
+ `--base-url=${getServerUrl()}`,
+ ]);
+ assert.ok(
+ fs.existsSync(
+ path.join(
+ tmpDir,
+ 'chromedriver',
+ `linux-${testChromeDriverBuildId}`,
+ 'chromedriver-linux64',
+ 'chromedriver'
+ )
+ )
+ );
+
+ await new CLI(tmpDir, createMockedReadlineInterface('no')).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'clear',
+ `--path=${tmpDir}`,
+ ]);
+ assert.ok(
+ fs.existsSync(
+ path.join(
+ tmpDir,
+ 'chromedriver',
+ `linux-${testChromeDriverBuildId}`,
+ 'chromedriver-linux64',
+ 'chromedriver'
+ )
+ )
+ );
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts
new file mode 100644
index 0000000000..88f9fae7fc
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts
@@ -0,0 +1,93 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ install,
+ canDownload,
+ Browser,
+ BrowserPlatform,
+ Cache,
+} from '../../../lib/cjs/main.js';
+import {getServerUrl, setupTestServer} from '../utils.js';
+import {testChromeDriverBuildId} from '../versions.js';
+
+/**
+ * Tests in this spec use real download URLs and unpack live browser archives
+ * so it requires the network access.
+ */
+describe('ChromeDriver install', () => {
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(() => {
+ new Cache(tmpDir).clear();
+ });
+
+ it('should check if a buildId can be downloaded', async () => {
+ assert.ok(
+ await canDownload({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMEDRIVER,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeDriverBuildId,
+ baseUrl: getServerUrl(),
+ })
+ );
+ });
+
+ it('should report if a buildId is not downloadable', async () => {
+ assert.strictEqual(
+ await canDownload({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMEDRIVER,
+ platform: BrowserPlatform.LINUX,
+ buildId: 'unknown',
+ baseUrl: getServerUrl(),
+ }),
+ false
+ );
+ });
+
+ it('should download and unpack the binary', async function () {
+ this.timeout(60000);
+ const expectedOutputPath = path.join(
+ tmpDir,
+ 'chromedriver',
+ `${BrowserPlatform.LINUX}-${testChromeDriverBuildId}`
+ );
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ let browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMEDRIVER,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeDriverBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ // Second iteration should be no-op.
+ browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMEDRIVER,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeDriverBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ assert.ok(fs.existsSync(browser.executablePath));
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts
new file mode 100644
index 0000000000..601efccc47
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import path from 'path';
+
+import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js';
+import {
+ resolveDownloadUrl,
+ relativeExecutablePath,
+} from '../../../lib/cjs/browser-data/chromium.js';
+
+describe('Chromium', () => {
+ it('should resolve download URLs', () => {
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.LINUX, '1083080'),
+ 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1083080/chrome-linux.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC, '1083080'),
+ 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/1083080/chrome-mac.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC_ARM, '1083080'),
+ 'https://storage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1083080/chrome-mac.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN32, '1083080'),
+ 'https://storage.googleapis.com/chromium-browser-snapshots/Win/1083080/chrome-win.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN64, '1083080'),
+ 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/1083080/chrome-win.zip'
+ );
+ });
+
+ it('should resolve executable paths', () => {
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.LINUX, '12372323'),
+ path.join('chrome-linux', 'chrome')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC, '12372323'),
+ path.join('chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'),
+ path.join('chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN32, '12372323'),
+ path.join('chrome-win', 'chrome.exe')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN64, '12372323'),
+ path.join('chrome-win', 'chrome.exe')
+ );
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts
new file mode 100644
index 0000000000..8cf7c8255b
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts
@@ -0,0 +1,122 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ CDP_WEBSOCKET_ENDPOINT_REGEX,
+ computeExecutablePath,
+ launch,
+ install,
+ Browser,
+ BrowserPlatform,
+} from '../../../lib/cjs/main.js';
+import {getServerUrl, setupTestServer, clearCache} from '../utils.js';
+import {testChromiumBuildId} from '../versions.js';
+
+describe('Chromium', () => {
+ it('should compute executable path for Chromium', () => {
+ assert.strictEqual(
+ computeExecutablePath({
+ browser: Browser.CHROMIUM,
+ platform: BrowserPlatform.LINUX,
+ buildId: '123',
+ cacheDir: '.cache',
+ }),
+ path.join('.cache', 'chromium', 'linux-123', 'chrome-linux', 'chrome')
+ );
+ });
+
+ describe('launcher', function () {
+ setupTestServer();
+
+ this.timeout(120000);
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(async () => {
+ tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'puppeteer-browsers-test')
+ );
+ await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMIUM,
+ buildId: testChromiumBuildId,
+ baseUrl: getServerUrl(),
+ });
+ });
+
+ afterEach(() => {
+ clearCache(tmpDir);
+ });
+
+ function getArgs() {
+ return [
+ '--allow-pre-commit-input',
+ '--disable-background-networking',
+ '--disable-background-timer-throttling',
+ '--disable-backgrounding-occluded-windows',
+ '--disable-breakpad',
+ '--disable-client-side-phishing-detection',
+ '--disable-component-extensions-with-background-pages',
+ '--disable-component-update',
+ '--disable-default-apps',
+ '--disable-dev-shm-usage',
+ '--disable-extensions',
+ '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,DialMediaRouteProvider',
+ '--disable-hang-monitor',
+ '--disable-ipc-flooding-protection',
+ '--disable-popup-blocking',
+ '--disable-prompt-on-repost',
+ '--disable-renderer-backgrounding',
+ '--disable-sync',
+ '--enable-automation',
+ '--enable-features=NetworkServiceInProcess2',
+ '--export-tagged-pdf',
+ '--force-color-profile=srgb',
+ '--headless=new',
+ '--metrics-recording-only',
+ '--no-first-run',
+ '--password-store=basic',
+ '--remote-debugging-port=9222',
+ '--use-mock-keychain',
+ `--user-data-dir=${path.join(tmpDir, 'profile')}`,
+ 'about:blank',
+ ];
+ }
+
+ it('should launch a Chromium browser', async () => {
+ const executablePath = computeExecutablePath({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMIUM,
+ buildId: testChromiumBuildId,
+ });
+ const process = launch({
+ executablePath,
+ args: getArgs(),
+ });
+ await process.close();
+ });
+
+ it('should allow parsing stderr output of the browser process', async () => {
+ const executablePath = computeExecutablePath({
+ cacheDir: tmpDir,
+ browser: Browser.CHROMIUM,
+ buildId: testChromiumBuildId,
+ });
+ const process = launch({
+ executablePath,
+ args: getArgs(),
+ });
+ const url = await process.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX);
+ await process.close();
+ assert.ok(url.startsWith('ws://127.0.0.1:9222/devtools/browser'));
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts
new file mode 100644
index 0000000000..134b432641
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import sinon from 'sinon';
+
+import {CLI} from '../../../lib/cjs/CLI.js';
+import * as httpUtil from '../../../lib/cjs/httpUtil.js';
+import {
+ createMockedReadlineInterface,
+ getServerUrl,
+ setupTestServer,
+} from '../utils.js';
+import {testFirefoxBuildId} from '../versions.js';
+
+describe('Firefox CLI', function () {
+ this.timeout(90000);
+
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(async () => {
+ await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'clear',
+ `--path=${tmpDir}`,
+ `--base-url=${getServerUrl()}`,
+ ]);
+
+ sinon.restore();
+ });
+
+ it('should download Firefox binaries', async () => {
+ await new CLI(tmpDir).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'install',
+ `firefox@${testFirefoxBuildId}`,
+ `--path=${tmpDir}`,
+ '--platform=linux',
+ `--base-url=${getServerUrl()}`,
+ ]);
+ assert.ok(
+ fs.existsSync(
+ path.join(tmpDir, 'firefox', `linux-${testFirefoxBuildId}`, 'firefox')
+ )
+ );
+ });
+
+ it('should download latest Firefox binaries', async () => {
+ sinon
+ .stub(httpUtil, 'getJSON')
+ .returns(Promise.resolve({FIREFOX_NIGHTLY: testFirefoxBuildId}));
+ await new CLI(tmpDir).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'install',
+ `firefox@latest`,
+ `--path=${tmpDir}`,
+ '--platform=linux',
+ `--base-url=${getServerUrl()}`,
+ ]);
+
+ await new CLI(tmpDir).run([
+ 'npx',
+ '@puppeteer/browsers',
+ 'install',
+ `firefox`,
+ `--path=${tmpDir}`,
+ '--platform=linux',
+ `--base-url=${getServerUrl()}`,
+ ]);
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts
new file mode 100644
index 0000000000..d0bb056090
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js';
+import {
+ createProfile,
+ relativeExecutablePath,
+ resolveDownloadUrl,
+} from '../../../lib/cjs/browser-data/firefox.js';
+
+describe('Firefox', () => {
+ it('should resolve download URLs', () => {
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.LINUX, '111.0a1'),
+ 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.linux-x86_64.tar.bz2'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC, '111.0a1'),
+ 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.mac.dmg'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.MAC_ARM, '111.0a1'),
+ 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.mac.dmg'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN32, '111.0a1'),
+ 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.win32.zip'
+ );
+ assert.strictEqual(
+ resolveDownloadUrl(BrowserPlatform.WIN64, '111.0a1'),
+ 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.win64.zip'
+ );
+ });
+
+ it('should resolve executable paths', () => {
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.LINUX, '111.0a1'),
+ path.join('firefox', 'firefox')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC, '111.0a1'),
+ path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.MAC_ARM, '111.0a1'),
+ path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN32, '111.0a1'),
+ path.join('firefox', 'firefox.exe')
+ );
+ assert.strictEqual(
+ relativeExecutablePath(BrowserPlatform.WIN64, '111.0a1'),
+ path.join('firefox', 'firefox.exe')
+ );
+ });
+
+ describe('profile', () => {
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'puppeteer-browsers-test')
+ );
+ });
+
+ afterEach(() => {
+ fs.rmSync(tmpDir, {
+ force: true,
+ recursive: true,
+ maxRetries: 5,
+ });
+ });
+
+ it('should create a profile', async () => {
+ await createProfile({
+ preferences: {
+ test: 1,
+ },
+ path: tmpDir,
+ });
+ const text = fs.readFileSync(path.join(tmpDir, 'user.js'), 'utf-8');
+ assert.ok(
+ text.includes(`user_pref("toolkit.startup.max_resumed_crashes", -1);`)
+ ); // default preference.
+ assert.ok(text.includes(`user_pref("test", 1);`)); // custom preference.
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts
new file mode 100644
index 0000000000..1bada43729
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {install, Browser, BrowserPlatform} from '../../../lib/cjs/main.js';
+import {setupTestServer, getServerUrl, clearCache} from '../utils.js';
+import {testFirefoxBuildId} from '../versions.js';
+
+/**
+ * Tests in this spec use real download URLs and unpack live browser archives
+ * so it requires the network access.
+ */
+describe('Firefox install', () => {
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(() => {
+ clearCache(tmpDir);
+ });
+
+ it('should download a buildId that is a bzip2 archive', async function () {
+ this.timeout(90000);
+ const expectedOutputPath = path.join(
+ tmpDir,
+ 'firefox',
+ `${BrowserPlatform.LINUX}-${testFirefoxBuildId}`
+ );
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ const browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.FIREFOX,
+ platform: BrowserPlatform.LINUX,
+ buildId: testFirefoxBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ });
+
+ // install relies on the `hdiutil` utility on MacOS.
+ // The utility is not available on other platforms.
+ (os.platform() === 'darwin' ? it : it.skip)(
+ 'should download a buildId that is a dmg archive',
+ async function () {
+ this.timeout(180000);
+ const expectedOutputPath = path.join(
+ tmpDir,
+ 'firefox',
+ `${BrowserPlatform.MAC}-${testFirefoxBuildId}`
+ );
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ const browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.FIREFOX,
+ platform: BrowserPlatform.MAC,
+ buildId: testFirefoxBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+ }
+ );
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/firefox/launch.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/launch.spec.ts
new file mode 100644
index 0000000000..3c62c87448
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/launch.spec.ts
@@ -0,0 +1,92 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ computeExecutablePath,
+ launch,
+ install,
+ Browser,
+ BrowserPlatform,
+ createProfile,
+} from '../../../lib/cjs/main.js';
+import {setupTestServer, getServerUrl, clearCache} from '../utils.js';
+import {testFirefoxBuildId} from '../versions.js';
+
+describe('Firefox', () => {
+ it('should compute executable path for Firefox', () => {
+ assert.strictEqual(
+ computeExecutablePath({
+ browser: Browser.FIREFOX,
+ platform: BrowserPlatform.LINUX,
+ buildId: '123',
+ cacheDir: '.cache',
+ }),
+ path.join('.cache', 'firefox', 'linux-123', 'firefox', 'firefox')
+ );
+ });
+
+ describe('launcher', function () {
+ this.timeout(120000);
+
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(async () => {
+ tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'puppeteer-browsers-test')
+ );
+ await install({
+ cacheDir: tmpDir,
+ browser: Browser.FIREFOX,
+ buildId: testFirefoxBuildId,
+ baseUrl: getServerUrl(),
+ });
+ });
+
+ afterEach(() => {
+ clearCache(tmpDir);
+ });
+
+ it('should launch a Firefox browser', async () => {
+ const userDataDir = path.join(tmpDir, 'profile');
+ function getArgs(): string[] {
+ const firefoxArguments = ['--no-remote'];
+ switch (os.platform()) {
+ case 'darwin':
+ firefoxArguments.push('--foreground');
+ break;
+ case 'win32':
+ firefoxArguments.push('--wait-for-browser');
+ break;
+ }
+ firefoxArguments.push('--profile', userDataDir);
+ firefoxArguments.push('--headless');
+ firefoxArguments.push('about:blank');
+ return firefoxArguments;
+ }
+ await createProfile(Browser.FIREFOX, {
+ path: userDataDir,
+ preferences: {},
+ });
+ const executablePath = computeExecutablePath({
+ cacheDir: tmpDir,
+ browser: Browser.FIREFOX,
+ buildId: testFirefoxBuildId,
+ });
+ const process = launch({
+ executablePath,
+ args: getArgs(),
+ });
+ await process.close();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/mocha-utils.ts b/remote/test/puppeteer/packages/browsers/test/src/mocha-utils.ts
new file mode 100644
index 0000000000..245a0048b2
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/mocha-utils.ts
@@ -0,0 +1,8 @@
+import debug from 'debug';
+
+export const mochaHooks = {
+ async beforeAll(): Promise<void> {
+ // Enable logging for Debug
+ debug.enable('puppeteer:*');
+ },
+};
diff --git a/remote/test/puppeteer/packages/browsers/test/src/tsconfig.json b/remote/test/puppeteer/packages/browsers/test/src/tsconfig.json
new file mode 100644
index 0000000000..03eae4a458
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "../build",
+ },
+ "references": [{"path": "../../tsconfig.json"}],
+}
diff --git a/remote/test/puppeteer/packages/browsers/test/src/tsdoc.json b/remote/test/puppeteer/packages/browsers/test/src/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/tsdoc.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
+
+ "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"],
+ "tagDefinitions": [
+ {
+ "tagName": "@license",
+ "syntaxKind": "modifier",
+ "allowMultiple": false
+ }
+ ],
+ "supportForTags": {
+ "@license": true
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/test/src/uninstall.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/uninstall.spec.ts
new file mode 100644
index 0000000000..0ef8a20fde
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/uninstall.spec.ts
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ install,
+ uninstall,
+ Browser,
+ BrowserPlatform,
+ Cache,
+} from '../../lib/cjs/main.js';
+
+import {getServerUrl, setupTestServer} from './utils.js';
+import {testChromeBuildId} from './versions.js';
+
+describe('common', () => {
+ setupTestServer();
+
+ let tmpDir = '/tmp/puppeteer-browsers-test';
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
+ });
+
+ afterEach(() => {
+ new Cache(tmpDir).clear();
+ });
+
+ it('should uninstall a browser', async function () {
+ this.timeout(60000);
+ const expectedOutputPath = path.join(
+ tmpDir,
+ 'chrome',
+ `${BrowserPlatform.LINUX}-${testChromeBuildId}`
+ );
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ const browser = await install({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeBuildId,
+ baseUrl: getServerUrl(),
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.ok(fs.existsSync(expectedOutputPath));
+
+ await uninstall({
+ cacheDir: tmpDir,
+ browser: Browser.CHROME,
+ platform: BrowserPlatform.LINUX,
+ buildId: testChromeBuildId,
+ });
+ assert.strictEqual(browser.path, expectedOutputPath);
+ assert.strictEqual(fs.existsSync(expectedOutputPath), false);
+ });
+});
diff --git a/remote/test/puppeteer/packages/browsers/test/src/utils.ts b/remote/test/puppeteer/packages/browsers/test/src/utils.ts
new file mode 100644
index 0000000000..bae231423e
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/utils.ts
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {execSync} from 'child_process';
+import os from 'os';
+import path from 'path';
+import * as readline from 'readline';
+import {Writable, Readable} from 'stream';
+
+import {TestServer} from '@pptr/testserver';
+
+import {isErrorLike} from '../../lib/cjs/launch.js';
+import {Cache} from '../../lib/cjs/main.js';
+
+export function createMockedReadlineInterface(
+ input: string
+): readline.Interface {
+ const readable = Readable.from([input]);
+ const writable = new Writable({
+ write(_chunk, _encoding, callback) {
+ // Suppress the output to keep the test clean
+ callback();
+ },
+ });
+
+ return readline.createInterface({
+ input: readable,
+ output: writable,
+ });
+}
+
+const startServer = async () => {
+ const assetsPath = path.join(__dirname, '..', '.cache', 'server');
+ return await TestServer.create(assetsPath);
+};
+
+interface ServerState {
+ server: TestServer;
+}
+
+const state: Partial<ServerState> = {};
+
+export function setupTestServer(): void {
+ before(async () => {
+ state.server = await startServer();
+ });
+
+ after(async () => {
+ await state.server!.stop();
+ state.server = undefined;
+ });
+}
+
+export function getServerUrl(): string {
+ return `http://localhost:${state.server!.port}`;
+}
+
+export function clearCache(tmpDir: string): void {
+ try {
+ new Cache(tmpDir).clear();
+ } catch (err) {
+ if (os.platform() === 'win32') {
+ console.log(execSync('tasklist').toString('utf-8'));
+ // Sometimes on Windows the folder cannot be removed due to unknown reasons.
+ // We suppress the error to avoud flakiness.
+ if (isErrorLike(err) && err.message.includes('EBUSY')) {
+ return;
+ }
+ }
+ throw err;
+ }
+}
diff --git a/remote/test/puppeteer/packages/browsers/test/src/versions.ts b/remote/test/puppeteer/packages/browsers/test/src/versions.ts
new file mode 100644
index 0000000000..3e13b8fc61
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/test/src/versions.ts
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const testChromeBuildId = '113.0.5672.0';
+export const testChromiumBuildId = '1083080';
+export const testFirefoxBuildId = '123.0a1';
+export const testChromeDriverBuildId = '115.0.5763.0';
+export const testChromeHeadlessShellBuildId = '118.0.5950.0';
diff --git a/remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs b/remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs
new file mode 100644
index 0000000000..e9c4ec963a
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Downloads test browser binaries to test/.cache/server folder that
+ * mirrors the structure of the download server.
+ */
+
+import {existsSync, mkdirSync, copyFileSync, rmSync} from 'fs';
+import {normalize, join, dirname} from 'path';
+
+import {downloadPaths} from '../lib/esm/browser-data/browser-data.js';
+import * as versions from '../test/build/versions.js';
+
+import {BrowserPlatform, install} from '@puppeteer/browsers';
+
+function getBrowser(str) {
+ const regex = /test(.+)BuildId/;
+ const match = str.match(regex);
+
+ if (match && match[1]) {
+ const lowercased = match[1].toLowerCase();
+ if (lowercased === 'chromeheadlessshell') {
+ return 'chrome-headless-shell';
+ }
+ return lowercased;
+ } else {
+ return null;
+ }
+}
+
+const cacheDir = normalize(join('.', 'test', '.cache'));
+
+for (const version of Object.keys(versions)) {
+ const browser = getBrowser(version);
+ if (!browser) {
+ continue;
+ }
+
+ const buildId = versions[version];
+
+ for (const platform of Object.values(BrowserPlatform)) {
+ const targetPath = join(
+ cacheDir,
+ 'server',
+ ...downloadPaths[browser](platform, buildId)
+ );
+
+ if (existsSync(targetPath)) {
+ continue;
+ }
+
+ const archivePath = await install({
+ browser,
+ buildId,
+ platform,
+ cacheDir: join(cacheDir, 'tmp'),
+ unpack: false,
+ });
+
+ mkdirSync(dirname(targetPath), {
+ recursive: true,
+ });
+ copyFileSync(archivePath, targetPath);
+ }
+}
+
+rmSync(join(cacheDir, 'tmp'), {
+ recursive: true,
+ force: true,
+ maxRetries: 10,
+});
diff --git a/remote/test/puppeteer/packages/browsers/tools/updateVersions.mjs b/remote/test/puppeteer/packages/browsers/tools/updateVersions.mjs
new file mode 100644
index 0000000000..9fb704baf5
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/tools/updateVersions.mjs
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'node:fs/promises';
+
+import actions from '@actions/core';
+
+import {testFirefoxBuildId} from '../test/build/versions.js';
+
+const filePath = './test/src/versions.ts';
+
+const getVersion = async () => {
+ // https://stackoverflow.com/a/1732454/96656
+ const response = await fetch(
+ 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/'
+ );
+ const html = await response.text();
+ const re = /firefox-(.*)\.en-US\.langpack\.xpi">/;
+ const match = re.exec(html)[1];
+ return match;
+};
+
+const patch = (input, version) => {
+ const output = input.replace(/testFirefoxBuildId = '([^']+)';/, match => {
+ return `testFirefoxBuildId = '${version}';`;
+ });
+ return output;
+};
+
+const version = await getVersion();
+
+if (testFirefoxBuildId !== version) {
+ actions.setOutput(
+ 'commit',
+ `chore: update Firefox testing pin to ${version}`
+ );
+ const contents = await fs.readFile(filePath, 'utf8');
+ const patched = patch(contents, version);
+ fs.writeFile(filePath, patched);
+}
diff --git a/remote/test/puppeteer/packages/browsers/tsconfig.json b/remote/test/puppeteer/packages/browsers/tsconfig.json
new file mode 100644
index 0000000000..b662532a01
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "files": [],
+ "references": [
+ {"path": "src/tsconfig.esm.json"},
+ {"path": "src/tsconfig.cjs.json"},
+ ],
+}
diff --git a/remote/test/puppeteer/packages/browsers/tsdoc.json b/remote/test/puppeteer/packages/browsers/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/packages/browsers/tsdoc.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
+
+ "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"],
+ "tagDefinitions": [
+ {
+ "tagName": "@license",
+ "syntaxKind": "modifier",
+ "allowMultiple": false
+ }
+ ],
+ "supportForTags": {
+ "@license": true
+ }
+}