summaryrefslogtreecommitdiffstats
path: root/remote/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /remote/test
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/test')
-rw-r--r--remote/test/puppeteer/.editorconfig9
-rw-r--r--remote/test/puppeteer/.eslintignore52
-rw-r--r--remote/test/puppeteer/.eslintrc.js281
-rw-r--r--remote/test/puppeteer/.eslintrc.types.cjs17
-rw-r--r--remote/test/puppeteer/.mocharc.cjs25
-rw-r--r--remote/test/puppeteer/.npmrc1
-rw-r--r--remote/test/puppeteer/.nvmrc1
-rw-r--r--remote/test/puppeteer/.prettierignore56
-rw-r--r--remote/test/puppeteer/.prettierrc.cjs7
-rw-r--r--remote/test/puppeteer/.release-please-manifest.json7
-rw-r--r--remote/test/puppeteer/.vscode/extensions.json3
-rw-r--r--remote/test/puppeteer/Herebyfile.mjs96
-rw-r--r--remote/test/puppeteer/LICENSE202
-rw-r--r--remote/test/puppeteer/README.md257
-rw-r--r--remote/test/puppeteer/SECURITY.md7
-rw-r--r--remote/test/puppeteer/examples/README.md43
-rw-r--r--remote/test/puppeteer/examples/block-images.js36
-rw-r--r--remote/test/puppeteer/examples/cross-browser.js46
-rw-r--r--remote/test/puppeteer/examples/custom-event.js40
-rw-r--r--remote/test/puppeteer/examples/detect-sniff.js49
-rw-r--r--remote/test/puppeteer/examples/oopif.js39
-rw-r--r--remote/test/puppeteer/examples/pdf.js25
-rw-r--r--remote/test/puppeteer/examples/proxy.js25
-rw-r--r--remote/test/puppeteer/examples/screenshot-fullpage.js18
-rw-r--r--remote/test/puppeteer/examples/screenshot.js17
-rw-r--r--remote/test/puppeteer/examples/search.js45
-rw-r--r--remote/test/puppeteer/json-mocha-reporter.js69
-rw-r--r--remote/test/puppeteer/moz.yaml10
-rw-r--r--remote/test/puppeteer/package-lock.json11657
-rw-r--r--remote/test/puppeteer/package.json187
-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
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/.eslintignore5
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/.gitignore3
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs6
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md110
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/README.md230
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/package.json71
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts200
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json26
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts15
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json20
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/config/files/.puppeteerrc.mjs4
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/config/index.ts39
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/config/schema.json8
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/files/common/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template18
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/index.ts118
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/schema.json34
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/.gitignore.template2
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/app.__ext@dasherize__.ts.template20
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/utils.ts.template60
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tsconfig.json.template10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/jasmine.json10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js4
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts135
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json37
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts152
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts45
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts189
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts47
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/src/config.test.ts30
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/src/e2e.test.ts111
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/src/ng-add.test.ts260
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/src/utils.ts147
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/test/tsconfig.json10
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.mjs64
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tools/projects.mjs159
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tools/smoke.mjs72
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tsconfig.json17
-rw-r--r--remote/test/puppeteer/packages/ng-schematics/tsdoc.json15
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/.gitignore1
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md1926
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs112
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/api-extractor.docs.json15
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/api-extractor.json46
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/package.json136
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts454
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts224
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts121
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Dialog.ts110
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts1580
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandleSymbol.ts10
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Environment.ts16
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts1218
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts521
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPResponse.ts129
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Input.ts517
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/JSHandle.ts212
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts3090
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Realm.ts104
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/Target.ts95
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts134
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/api.ts22
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts1088
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts209
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts317
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserConnector.ts123
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts145
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts187
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.test.ts50
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts256
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts96
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts45
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts87
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts35
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts295
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts313
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts163
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts107
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts732
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts101
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts155
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts913
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts228
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts123
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts164
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts151
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts22
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts225
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts475
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts139
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts144
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts351
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts148
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts180
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts178
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserPrompt.ts137
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/core.ts15
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts119
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts81
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Accessibility.ts579
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/AriaQueryHandler.ts120
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts118
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts523
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/BrowserConnector.ts66
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/CDPSession.ts167
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/ChromeTargetManager.ts417
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Connection.ts273
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Coverage.ts513
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/DeviceRequestPrompt.test.ts471
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/DeviceRequestPrompt.ts280
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Dialog.ts37
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/ElementHandle.ts172
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts554
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts392
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/FirefoxTargetManager.ts210
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts351
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameManager.ts551
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameManagerEvents.ts39
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameTree.ts98
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts449
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts173
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts604
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/IsolatedWorld.ts273
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/IsolatedWorlds.ts20
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/JSHandle.ts109
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts298
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkEventManager.ts217
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts1531
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts710
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts1249
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts49
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts305
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/TargetManager.ts65
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/Tracing.ts140
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts83
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/cdp.ts42
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/cdp/utils.ts232
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts114
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts50
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/CallbackRegistry.ts177
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts120
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectOptions.ts77
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts15
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts113
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts207
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts128
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts1552
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts124
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts185
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts253
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts92
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts49
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts76
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts37
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManagerEvents.ts38
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts217
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts31
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts29
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts11
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts123
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts205
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts52
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts78
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts29
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts20
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts45
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts671
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Viewport.ts50
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts275
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts35
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts40
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts14
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts225
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts447
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/environment.ts10
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/ARIAQuerySelector.ts31
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/CustomQuerySelector.ts59
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/PQuerySelector.ts298
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts105
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/PierceQuerySelector.ts65
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/Poller.ts168
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts146
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/TextQuerySelector.ts46
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/XPathQuerySelector.ts39
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/injected.ts51
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts67
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.test.ts59
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts344
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.test.ts47
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts242
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts140
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/NodeWebSocketTransport.ts64
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/PipeTransport.ts86
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts451
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts356
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/ScreenRecorder.ts255
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/node.ts13
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/node/util/fs.ts27
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/puppeteer-core.ts49
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts14
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/templates/injected.ts.tmpl8
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/templates/version.ts.tmpl4
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.cjs.json9
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.esm.json7
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts46
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.test.ts68
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts122
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts66
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Function.test.ts36
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts91
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts41
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts21
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts79
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts140
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/disposable.ts275
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts11
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts8
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts61
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.cjs.json10
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json8
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/tools/ensure-correct-devtools-protocol-package.ts86
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/tsconfig.json8
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/tsdoc.json15
-rw-r--r--remote/test/puppeteer/packages/puppeteer/.gitignore1
-rw-r--r--remote/test/puppeteer/packages/puppeteer/CHANGELOG.md2096
-rw-r--r--remote/test/puppeteer/packages/puppeteer/api-extractor.docs.json15
-rw-r--r--remote/test/puppeteer/packages/puppeteer/api-extractor.json49
-rwxr-xr-xremote/test/puppeteer/packages/puppeteer/install.mjs35
-rw-r--r--remote/test/puppeteer/packages/puppeteer/package.json133
-rw-r--r--remote/test/puppeteer/packages/puppeteer/src/getConfiguration.ts138
-rw-r--r--remote/test/puppeteer/packages/puppeteer/src/node/cli.ts32
-rw-r--r--remote/test/puppeteer/packages/puppeteer/src/node/install.ts184
-rw-r--r--remote/test/puppeteer/packages/puppeteer/src/puppeteer.ts48
-rw-r--r--remote/test/puppeteer/packages/puppeteer/src/tsconfig.cjs.json8
-rw-r--r--remote/test/puppeteer/packages/puppeteer/src/tsconfig.esm.json6
-rw-r--r--remote/test/puppeteer/packages/puppeteer/tsconfig.json16
-rw-r--r--remote/test/puppeteer/packages/puppeteer/tsdoc.json15
-rw-r--r--remote/test/puppeteer/packages/testserver/CHANGELOG.md8
-rw-r--r--remote/test/puppeteer/packages/testserver/LICENSE202
-rw-r--r--remote/test/puppeteer/packages/testserver/README.md18
-rw-r--r--remote/test/puppeteer/packages/testserver/cert.pem20
-rw-r--r--remote/test/puppeteer/packages/testserver/key.pem28
-rw-r--r--remote/test/puppeteer/packages/testserver/package.json36
-rw-r--r--remote/test/puppeteer/packages/testserver/src/index.ts311
-rw-r--r--remote/test/puppeteer/packages/testserver/tsconfig.json12
-rw-r--r--remote/test/puppeteer/packages/testserver/tsdoc.json15
-rw-r--r--remote/test/puppeteer/release-please-config.json29
-rw-r--r--remote/test/puppeteer/test-d/CommonEventEmitter.test-d.ts19
-rw-r--r--remote/test/puppeteer/test-d/ElementHandle.test-d.ts1025
-rw-r--r--remote/test/puppeteer/test-d/JSHandle.test-d.ts84
-rw-r--r--remote/test/puppeteer/test-d/NodeFor.test-d.ts157
-rw-r--r--remote/test/puppeteer/test-d/puppeteer.test-d.ts13
-rw-r--r--remote/test/puppeteer/test/.eslintrc.js38
-rw-r--r--remote/test/puppeteer/test/README.md95
-rw-r--r--remote/test/puppeteer/test/TestExpectations.json3714
-rw-r--r--remote/test/puppeteer/test/TestSuites.json74
-rw-r--r--remote/test/puppeteer/test/assets/abort-request.html13
-rw-r--r--remote/test/puppeteer/test/assets/beforeunload.html10
-rw-r--r--remote/test/puppeteer/test/assets/cached/bfcache/index.html2
-rw-r--r--remote/test/puppeteer/test/assets/cached/bfcache/target.html2
-rw-r--r--remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe-container.html11
-rw-r--r--remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe.html3
-rw-r--r--remote/test/puppeteer/test/assets/cached/bfcache/worker.mjs1
-rw-r--r--remote/test/puppeteer/test/assets/cached/one-style-font.css9
-rw-r--r--remote/test/puppeteer/test/assets/cached/one-style-font.html2
-rw-r--r--remote/test/puppeteer/test/assets/cached/one-style.css3
-rw-r--r--remote/test/puppeteer/test/assets/cached/one-style.html2
-rw-r--r--remote/test/puppeteer/test/assets/consolelog.html17
-rw-r--r--remote/test/puppeteer/test/assets/credit-card.html42
-rw-r--r--remote/test/puppeteer/test/assets/csp.html1
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttfbin0 -> 136940 bytes
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/OFL.txt95
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/empty.html3
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/involved.html26
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/media.html4
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/multiple.html8
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/simple.html6
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/sourceurl.html7
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css3
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css4
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/unused.html7
-rw-r--r--remote/test/puppeteer/test/assets/detect-touch.html12
-rw-r--r--remote/test/puppeteer/test/assets/digits/0.pngbin0 -> 434 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/1.pngbin0 -> 346 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/2.pngbin0 -> 413 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/3.pngbin0 -> 434 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/4.pngbin0 -> 403 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/5.pngbin0 -> 422 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/6.pngbin0 -> 445 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/7.pngbin0 -> 387 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/8.pngbin0 -> 447 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/9.pngbin0 -> 437 bytes
-rw-r--r--remote/test/puppeteer/test/assets/dynamic-oopif.html10
-rw-r--r--remote/test/puppeteer/test/assets/empty.html0
-rw-r--r--remote/test/puppeteer/test/assets/error.html15
-rw-r--r--remote/test/puppeteer/test/assets/es6/.eslintrc5
-rw-r--r--remote/test/puppeteer/test/assets/es6/es6import.js2
-rw-r--r--remote/test/puppeteer/test/assets/es6/es6module.js1
-rw-r--r--remote/test/puppeteer/test/assets/es6/es6pathimport.js2
-rw-r--r--remote/test/puppeteer/test/assets/favicon.icobin0 -> 70 bytes
-rw-r--r--remote/test/puppeteer/test/assets/file-to-upload.txt1
-rw-r--r--remote/test/puppeteer/test/assets/frames/frame.html8
-rw-r--r--remote/test/puppeteer/test/assets/frames/frameset.html8
-rw-r--r--remote/test/puppeteer/test/assets/frames/lazy-frame.html3
-rw-r--r--remote/test/puppeteer/test/assets/frames/nested-frames.html26
-rw-r--r--remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html1
-rw-r--r--remote/test/puppeteer/test/assets/frames/one-frame.html1
-rw-r--r--remote/test/puppeteer/test/assets/frames/script.js1
-rw-r--r--remote/test/puppeteer/test/assets/frames/style.css3
-rw-r--r--remote/test/puppeteer/test/assets/frames/two-frames.html13
-rw-r--r--remote/test/puppeteer/test/assets/global-var.html3
-rw-r--r--remote/test/puppeteer/test/assets/grid.html51
-rw-r--r--remote/test/puppeteer/test/assets/historyapi.html5
-rw-r--r--remote/test/puppeteer/test/assets/idle-detector.html23
-rw-r--r--remote/test/puppeteer/test/assets/initiator.html2
-rw-r--r--remote/test/puppeteer/test/assets/initiator.js8
-rw-r--r--remote/test/puppeteer/test/assets/injectedfile.js2
-rw-r--r--remote/test/puppeteer/test/assets/injectedstyle.css3
-rw-r--r--remote/test/puppeteer/test/assets/inline-svg.html14
-rw-r--r--remote/test/puppeteer/test/assets/inner-frame1.html10
-rw-r--r--remote/test/puppeteer/test/assets/inner-frame2.html1
-rw-r--r--remote/test/puppeteer/test/assets/input/button.html16
-rw-r--r--remote/test/puppeteer/test/assets/input/checkbox.html42
-rw-r--r--remote/test/puppeteer/test/assets/input/drag-and-drop.html43
-rw-r--r--remote/test/puppeteer/test/assets/input/fileupload.html9
-rw-r--r--remote/test/puppeteer/test/assets/input/keyboard.html42
-rw-r--r--remote/test/puppeteer/test/assets/input/mouse-helper.js74
-rw-r--r--remote/test/puppeteer/test/assets/input/rotatedButton.html21
-rw-r--r--remote/test/puppeteer/test/assets/input/scrollable.html37
-rw-r--r--remote/test/puppeteer/test/assets/input/select.html70
-rw-r--r--remote/test/puppeteer/test/assets/input/textarea.html15
-rw-r--r--remote/test/puppeteer/test/assets/input/touchscreen.html122
-rw-r--r--remote/test/puppeteer/test/assets/input/wheel.html43
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/eval.html1
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/involved.html16
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/multiple.html2
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/ranges.html2
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/script1.js1
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/script2.js1
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/simple.html2
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/sourceurl.html4
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/unused.html1
-rw-r--r--remote/test/puppeteer/test/assets/lazy-oopif-frame.html3
-rw-r--r--remote/test/puppeteer/test/assets/main-frame.html10
-rw-r--r--remote/test/puppeteer/test/assets/mobile.html1
-rw-r--r--remote/test/puppeteer/test/assets/modernizr.js3
-rw-r--r--remote/test/puppeteer/test/assets/networkidle.html19
-rw-r--r--remote/test/puppeteer/test/assets/offscreenbuttons.html40
-rw-r--r--remote/test/puppeteer/test/assets/one-style.css3
-rw-r--r--remote/test/puppeteer/test/assets/one-style.html2
-rw-r--r--remote/test/puppeteer/test/assets/oopif.html5
-rw-r--r--remote/test/puppeteer/test/assets/p-selectors.html15
-rw-r--r--remote/test/puppeteer/test/assets/pdf.html11
-rw-r--r--remote/test/puppeteer/test/assets/picture.html6
-rw-r--r--remote/test/puppeteer/test/assets/playground.html15
-rw-r--r--remote/test/puppeteer/test/assets/popup/popup.html9
-rw-r--r--remote/test/puppeteer/test/assets/popup/window-open.html11
-rw-r--r--remote/test/puppeteer/test/assets/pptr.pngbin0 -> 6138 bytes
-rw-r--r--remote/test/puppeteer/test/assets/prerender/index.html21
-rw-r--r--remote/test/puppeteer/test/assets/prerender/target.html5
-rw-r--r--remote/test/puppeteer/test/assets/resetcss.html50
-rw-r--r--remote/test/puppeteer/test/assets/resolution.html23
-rw-r--r--remote/test/puppeteer/test/assets/self-request.html5
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html3
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js0
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/extension/background.js1
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json9
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css3
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html5
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js7
-rw-r--r--remote/test/puppeteer/test/assets/shadow.html17
-rw-r--r--remote/test/puppeteer/test/assets/simple-extension/content-script.js2
-rw-r--r--remote/test/puppeteer/test/assets/simple-extension/index.js2
-rw-r--r--remote/test/puppeteer/test/assets/simple-extension/manifest.json14
-rw-r--r--remote/test/puppeteer/test/assets/simple.json1
-rw-r--r--remote/test/puppeteer/test/assets/tamperable.html3
-rw-r--r--remote/test/puppeteer/test/assets/title.html1
-rw-r--r--remote/test/puppeteer/test/assets/worker/worker.html14
-rw-r--r--remote/test/puppeteer/test/assets/worker/worker.js16
-rw-r--r--remote/test/puppeteer/test/assets/wrappedlink.html32
-rw-r--r--remote/test/puppeteer/test/fixtures/closeme.js5
-rw-r--r--remote/test/puppeteer/test/fixtures/dumpio.js10
-rw-r--r--remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt20
-rw-r--r--remote/test/puppeteer/test/golden-chrome/device-pixel-ratio1.pngbin0 -> 3249 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/device-pixel-ratio2.pngbin0 -> 10259 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/device-pixel-ratio3.pngbin0 -> 20942 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/grid-cell-0.pngbin0 -> 436 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/grid-cell-1.pngbin0 -> 276 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/grid-cell-2.pngbin0 -> 428 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/grid-cell-3.pngbin0 -> 448 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt36
-rw-r--r--remote/test/puppeteer/test/golden-chrome/mock-binary-response.pngbin0 -> 6789 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.pngbin0 -> 81 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.pngbin0 -> 8472 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.pngbin0 -> 1962 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.pngbin0 -> 461 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.pngbin0 -> 138 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.pngbin0 -> 138 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.pngbin0 -> 2807 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.pngbin0 -> 168 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.pngbin0 -> 2355 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.pngbin0 -> 168 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage-2.pngbin0 -> 74889 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.pngbin0 -> 74972 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip-2.pngbin0 -> 188 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.pngbin0 -> 346 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/screenshot-sanity.pngbin0 -> 36252 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/transparent.pngbin0 -> 119 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.pngbin0 -> 33569 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.pngbin0 -> 81174 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.pngbin0 -> 37483 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.pngbin0 -> 36282 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.pngbin0 -> 37282 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chrome/white.jpgbin0 -> 357 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/device-pixel-ratio1.pngbin0 -> 9701 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/device-pixel-ratio2.pngbin0 -> 36194 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/device-pixel-ratio3.pngbin0 -> 79723 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/grid-cell-0.pngbin0 -> 550 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/grid-cell-1.pngbin0 -> 340 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.pngbin0 -> 80 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.pngbin0 -> 10361 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.pngbin0 -> 2501 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.pngbin0 -> 514 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.pngbin0 -> 113 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.pngbin0 -> 151 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.pngbin0 -> 7703 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.pngbin0 -> 234 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.pngbin0 -> 1800 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.pngbin0 -> 234 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage-2.pngbin0 -> 55662 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.pngbin0 -> 55662 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip-2.pngbin0 -> 204 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.pngbin0 -> 459 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-sanity.pngbin0 -> 46034 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/transparent.pngbin0 -> 119 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/white.jpgbin0 -> 823 bytes
-rw-r--r--remote/test/puppeteer/test/installation/.mocharc.cjs13
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js9
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js22
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs9
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/basic.js15
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/basic.ts15
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/bidi.js17
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs8
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/configuration/puppeteer.config.ts6
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/imports.js10
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/installCanary.js24
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs10
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/trimCache.js11
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/tsconfig.json7
-rw-r--r--remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js16
-rw-r--r--remote/test/puppeteer/test/installation/package.json50
-rw-r--r--remote/test/puppeteer/test/installation/src/browsers.spec.ts30
-rw-r--r--remote/test/puppeteer/test/installation/src/constants.ts25
-rw-r--r--remote/test/puppeteer/test/installation/src/puppeteer-cli.spec.ts58
-rw-r--r--remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts73
-rw-r--r--remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts34
-rw-r--r--remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts51
-rw-r--r--remote/test/puppeteer/test/installation/src/puppeteer-typescript.spec.ts49
-rw-r--r--remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts47
-rw-r--r--remote/test/puppeteer/test/installation/src/puppeteer.spec.ts104
-rw-r--r--remote/test/puppeteer/test/installation/src/sandbox.ts131
-rw-r--r--remote/test/puppeteer/test/installation/src/util.ts17
-rw-r--r--remote/test/puppeteer/test/installation/tsconfig.json10
-rw-r--r--remote/test/puppeteer/test/installation/tsdoc.json15
-rw-r--r--remote/test/puppeteer/test/package.json37
-rw-r--r--remote/test/puppeteer/test/src/accessibility.spec.ts567
-rw-r--r--remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts721
-rw-r--r--remote/test/puppeteer/test/src/autofill.spec.ts38
-rw-r--r--remote/test/puppeteer/test/src/browser.spec.ts81
-rw-r--r--remote/test/puppeteer/test/src/browsercontext.spec.ts368
-rw-r--r--remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts147
-rw-r--r--remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts96
-rw-r--r--remote/test/puppeteer/test/src/cdp/bfcache.spec.ts65
-rw-r--r--remote/test/puppeteer/test/src/cdp/devtools.spec.ts123
-rw-r--r--remote/test/puppeteer/test/src/cdp/extensions.spec.ts120
-rw-r--r--remote/test/puppeteer/test/src/cdp/prerender.spec.ts181
-rw-r--r--remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts108
-rw-r--r--remote/test/puppeteer/test/src/chromiumonly.spec.ts168
-rw-r--r--remote/test/puppeteer/test/src/click.spec.ts478
-rw-r--r--remote/test/puppeteer/test/src/cookies.spec.ts557
-rw-r--r--remote/test/puppeteer/test/src/coverage.spec.ts343
-rw-r--r--remote/test/puppeteer/test/src/debugInfo.spec.ts36
-rw-r--r--remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts104
-rw-r--r--remote/test/puppeteer/test/src/device-request-prompt.spec.ts53
-rw-r--r--remote/test/puppeteer/test/src/dialog.spec.ts64
-rw-r--r--remote/test/puppeteer/test/src/diffstyle.css13
-rw-r--r--remote/test/puppeteer/test/src/drag-and-drop.spec.ts154
-rw-r--r--remote/test/puppeteer/test/src/elementhandle.spec.ts953
-rw-r--r--remote/test/puppeteer/test/src/emulation.spec.ts553
-rw-r--r--remote/test/puppeteer/test/src/evaluation.spec.ts607
-rw-r--r--remote/test/puppeteer/test/src/fixtures.spec.ts114
-rw-r--r--remote/test/puppeteer/test/src/frame.spec.ts297
-rw-r--r--remote/test/puppeteer/test/src/golden-utils.ts169
-rw-r--r--remote/test/puppeteer/test/src/headful.spec.ts91
-rw-r--r--remote/test/puppeteer/test/src/idle_override.spec.ts79
-rw-r--r--remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts128
-rw-r--r--remote/test/puppeteer/test/src/injected.spec.ts49
-rw-r--r--remote/test/puppeteer/test/src/input.spec.ts394
-rw-r--r--remote/test/puppeteer/test/src/jshandle.spec.ts373
-rw-r--r--remote/test/puppeteer/test/src/keyboard.spec.ts550
-rw-r--r--remote/test/puppeteer/test/src/launcher.spec.ts1025
-rw-r--r--remote/test/puppeteer/test/src/locator.spec.ts763
-rw-r--r--remote/test/puppeteer/test/src/mocha-utils.ts507
-rw-r--r--remote/test/puppeteer/test/src/mouse.spec.ts472
-rw-r--r--remote/test/puppeteer/test/src/navigation.spec.ts918
-rw-r--r--remote/test/puppeteer/test/src/network.spec.ts917
-rw-r--r--remote/test/puppeteer/test/src/oopif.spec.ts527
-rw-r--r--remote/test/puppeteer/test/src/page.spec.ts2287
-rw-r--r--remote/test/puppeteer/test/src/proxy.spec.ts236
-rw-r--r--remote/test/puppeteer/test/src/queryhandler.spec.ts653
-rw-r--r--remote/test/puppeteer/test/src/queryselector.spec.ts491
-rw-r--r--remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts969
-rw-r--r--remote/test/puppeteer/test/src/requestinterception.spec.ts920
-rw-r--r--remote/test/puppeteer/test/src/screencast.spec.ts99
-rw-r--r--remote/test/puppeteer/test/src/screenshot.spec.ts453
-rw-r--r--remote/test/puppeteer/test/src/stacktrace.spec.ts157
-rw-r--r--remote/test/puppeteer/test/src/target.spec.ts343
-rw-r--r--remote/test/puppeteer/test/src/touchscreen.spec.ts79
-rw-r--r--remote/test/puppeteer/test/src/tracing.spec.ts149
-rw-r--r--remote/test/puppeteer/test/src/utils.ts171
-rw-r--r--remote/test/puppeteer/test/src/waittask.spec.ts867
-rw-r--r--remote/test/puppeteer/test/src/worker.spec.ts109
-rw-r--r--remote/test/puppeteer/test/tsconfig.json10
-rw-r--r--remote/test/puppeteer/test/tsdoc.json15
-rwxr-xr-xremote/test/puppeteer/tools/analyze_issue.mjs281
-rwxr-xr-xremote/test/puppeteer/tools/assets/verify_issue.ts68
-rw-r--r--remote/test/puppeteer/tools/chmod.ts16
-rwxr-xr-xremote/test/puppeteer/tools/clean.js12
-rw-r--r--remote/test/puppeteer/tools/cp.ts12
-rw-r--r--remote/test/puppeteer/tools/docgen/package.json33
-rw-r--r--remote/test/puppeteer/tools/docgen/src/custom_markdown_documenter.ts1495
-rw-r--r--remote/test/puppeteer/tools/docgen/src/docgen.ts38
-rw-r--r--remote/test/puppeteer/tools/docgen/tsconfig.json11
-rw-r--r--remote/test/puppeteer/tools/docgen/tsdoc.json15
-rw-r--r--remote/test/puppeteer/tools/doctest/package.json39
-rw-r--r--remote/test/puppeteer/tools/doctest/src/doctest.ts349
-rw-r--r--remote/test/puppeteer/tools/doctest/tsconfig.json11
-rw-r--r--remote/test/puppeteer/tools/doctest/tsdoc.json15
-rw-r--r--remote/test/puppeteer/tools/download_chrome_bidi.mjs56
-rw-r--r--remote/test/puppeteer/tools/ensure-pinned-deps.ts52
-rw-r--r--remote/test/puppeteer/tools/eslint/package.json37
-rw-r--r--remote/test/puppeteer/tools/eslint/src/check-license.ts83
-rw-r--r--remote/test/puppeteer/tools/eslint/src/extensions.ts48
-rw-r--r--remote/test/puppeteer/tools/eslint/src/prettier-comments.js99
-rw-r--r--remote/test/puppeteer/tools/eslint/src/use-using.ts85
-rw-r--r--remote/test/puppeteer/tools/eslint/tsconfig.json14
-rw-r--r--remote/test/puppeteer/tools/eslint/tsdoc.json15
-rw-r--r--remote/test/puppeteer/tools/generate_module_package_json.ts15
-rw-r--r--remote/test/puppeteer/tools/get_deprecated_version_range.js18
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/README.md103
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/package.json43
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/src/interface.ts191
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/src/mocha-runner.ts330
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/src/reporter.ts16
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/src/test.ts212
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/src/types.ts57
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/src/utils.ts291
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/tsconfig.json13
-rw-r--r--remote/test/puppeteer/tools/mocha-runner/tsdoc.json15
-rw-r--r--remote/test/puppeteer/tools/sort-test-expectations.mjs65
-rw-r--r--remote/test/puppeteer/tools/third_party/validate-licenses.ts154
-rw-r--r--remote/test/puppeteer/tools/tsconfig.json4
-rw-r--r--remote/test/puppeteer/tools/tsdoc.json15
-rw-r--r--remote/test/puppeteer/tools/update_chrome_revision.mjs162
-rw-r--r--remote/test/puppeteer/tsconfig.base.json33
-rw-r--r--remote/test/puppeteer/tsdoc.json15
-rw-r--r--remote/test/puppeteer/versions.js76
650 files changed, 100908 insertions, 0 deletions
diff --git a/remote/test/puppeteer/.editorconfig b/remote/test/puppeteer/.editorconfig
new file mode 100644
index 0000000000..c6c8b36219
--- /dev/null
+++ b/remote/test/puppeteer/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/remote/test/puppeteer/.eslintignore b/remote/test/puppeteer/.eslintignore
new file mode 100644
index 0000000000..77ccb9293d
--- /dev/null
+++ b/remote/test/puppeteer/.eslintignore
@@ -0,0 +1,52 @@
+## [START] Keep in sync with .gitignore
+# Dependencies
+node_modules
+
+# Production
+build/
+lib/
+bin/
+
+# Generated files
+**/*.tsbuildinfo
+*.api.json
+*.tgz
+yarn.lock
+.docusaurus/
+.cache-loader
+test/output-*/
+.dev_profile*
+coverage/
+generated/
+.eslintcache
+.cache/
+
+# IDE Artifacts
+.vscode
+!.vscode/extensions.json
+!.vscode/*.template.json
+.devcontainer
+
+# Misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Wireit
+.wireit
+## [END] Keep in sync with .gitignore
+
+# ESLint ignores.
+assets/
+third_party/
+
+# ng-schematics
+packages/ng-schematics/sandbox/**
+packages/ng-schematics/multi/**
+packages/ng-schematics/src/**/files/
diff --git a/remote/test/puppeteer/.eslintrc.js b/remote/test/puppeteer/.eslintrc.js
new file mode 100644
index 0000000000..250aa0c169
--- /dev/null
+++ b/remote/test/puppeteer/.eslintrc.js
@@ -0,0 +1,281 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const {readdirSync} = require('fs');
+const {join} = require('path');
+
+const rulesDirPlugin = require('eslint-plugin-rulesdir');
+
+rulesDirPlugin.RULES_DIR = 'tools/eslint/lib';
+
+function getThirdPartyPackages() {
+ return readdirSync(join(__dirname, 'packages/puppeteer-core/third_party'), {
+ withFileTypes: true,
+ })
+ .filter(dirent => {
+ return dirent.isDirectory();
+ })
+ .map(({name}) => {
+ return {
+ name,
+ message: `Import \`${name}\` from the vendored location: third_party/${name}/index.js`,
+ };
+ });
+}
+
+module.exports = {
+ root: true,
+ env: {
+ node: true,
+ es6: true,
+ },
+
+ parser: '@typescript-eslint/parser',
+
+ plugins: ['mocha', '@typescript-eslint', 'import', 'rulesdir'],
+
+ extends: ['plugin:prettier/recommended', 'plugin:import/typescript'],
+
+ settings: {
+ 'import/resolver': {
+ typescript: true,
+ },
+ },
+
+ rules: {
+ // Brackets keep code readable.
+ curly: ['error', 'all'],
+ // Brackets keep code readable and `return` intentions clear.
+ 'arrow-body-style': ['error', 'always'],
+ // Error if files are not formatted with Prettier correctly.
+ 'prettier/prettier': 'error',
+ // syntax preferences
+ 'spaced-comment': [
+ 'error',
+ 'always',
+ {
+ markers: ['*'],
+ },
+ ],
+ eqeqeq: ['error'],
+ 'accessor-pairs': [
+ 'error',
+ {
+ getWithoutSet: false,
+ setWithoutGet: false,
+ },
+ ],
+ 'new-parens': 'error',
+ 'func-call-spacing': 'error',
+ 'prefer-const': 'error',
+
+ 'max-len': [
+ 'error',
+ {
+ /* this setting doesn't impact things as we use Prettier to format
+ * our code and hence dictate the line length.
+ * Prettier aims for 80 but sometimes makes the decision to go just
+ * over 80 chars as it decides that's better than wrapping. ESLint's
+ * rule defaults to 80 but therefore conflicts with Prettier. So we
+ * set it to something far higher than Prettier would allow to avoid
+ * it causing issues and conflicting with Prettier.
+ */
+ code: 200,
+ comments: 90,
+ ignoreTemplateLiterals: true,
+ ignoreUrls: true,
+ ignoreStrings: true,
+ ignoreRegExpLiterals: true,
+ },
+ ],
+ // anti-patterns
+ 'no-var': 'error',
+ 'no-with': 'error',
+ 'no-multi-str': 'error',
+ 'no-caller': 'error',
+ 'no-implied-eval': 'error',
+ 'no-labels': 'error',
+ 'no-new-object': 'error',
+ 'no-octal-escape': 'error',
+ 'no-self-compare': 'error',
+ 'no-shadow-restricted-names': 'error',
+ 'no-cond-assign': 'error',
+ 'no-debugger': 'error',
+ 'no-dupe-keys': 'error',
+ 'no-duplicate-case': 'error',
+ 'no-empty-character-class': 'error',
+ 'no-unreachable': 'error',
+ 'no-unsafe-negation': 'error',
+ radix: 'error',
+ 'valid-typeof': 'error',
+ 'no-unused-vars': [
+ 'error',
+ {
+ args: 'none',
+ vars: 'local',
+ varsIgnorePattern:
+ '([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)',
+ },
+ ],
+ 'no-implicit-globals': ['error'],
+
+ // es2015 features
+ 'require-yield': 'error',
+ 'template-curly-spacing': ['error', 'never'],
+
+ // ensure we don't have any it.only or describe.only in prod
+ 'mocha/no-exclusive-tests': 'error',
+
+ 'import/order': [
+ 'error',
+ {
+ 'newlines-between': 'always',
+ alphabetize: {order: 'asc', caseInsensitive: true},
+ },
+ ],
+
+ 'import/no-cycle': ['error', {maxDepth: Infinity}],
+
+ 'no-restricted-syntax': [
+ 'error',
+ // Don't allow underscored declarations on camelCased variables/properties.
+ // ...RESTRICTED_UNDERSCORED_IDENTIFIERS,
+ ],
+
+ // Keeps comments formatted.
+ 'rulesdir/prettier-comments': 'error',
+ // Enforces consistent file extension
+ 'rulesdir/extensions': 'error',
+ // Enforces license headers on files
+ 'rulesdir/check-license': 'warn',
+ },
+ overrides: [
+ {
+ files: ['*.ts'],
+ parserOptions: {
+ allowAutomaticSingleRunInference: true,
+ project: './tsconfig.base.json',
+ },
+ extends: [
+ 'plugin:@typescript-eslint/eslint-recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:@typescript-eslint/stylistic',
+ ],
+ plugins: ['eslint-plugin-tsdoc'],
+ rules: {
+ // Enforces clean up of used resources.
+ 'rulesdir/use-using': 'error',
+ // Brackets keep code readable.
+ curly: ['error', 'all'],
+ // Brackets keep code readable and `return` intentions clear.
+ 'arrow-body-style': ['error', 'always'],
+ // Error if comments do not adhere to `tsdoc`.
+ 'tsdoc/syntax': 'error',
+ // Keeps array types simple only when they are simple for readability.
+ '@typescript-eslint/array-type': ['error', {default: 'array-simple'}],
+ 'no-unused-vars': 'off',
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {argsIgnorePattern: '^_', varsIgnorePattern: '^_'},
+ ],
+ 'func-call-spacing': 'off',
+ '@typescript-eslint/func-call-spacing': 'error',
+ semi: 'off',
+ '@typescript-eslint/semi': 'error',
+ '@typescript-eslint/no-empty-function': 'off',
+ '@typescript-eslint/no-use-before-define': 'off',
+ // We have to use any on some types so the warning isn't valuable.
+ '@typescript-eslint/no-explicit-any': 'off',
+ // We don't require explicit return types on basic functions or
+ // dummy functions in tests, for example
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ // We allow non-null assertions if the value was asserted using `assert` API.
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ '@typescript-eslint/no-useless-template-literals': 'error',
+ /**
+ * This is the default options (as per
+ * https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/docs/rules/ban-types.md),
+ *
+ * Unfortunately there's no way to
+ */
+ '@typescript-eslint/ban-types': [
+ 'error',
+ {
+ extendDefaults: true,
+ types: {
+ /*
+ * Puppeteer's API accepts generic functions in many places so it's
+ * not a useful linting rule to ban the `Function` type. This turns off
+ * the banning of the `Function` type which is a default rule.
+ */
+ Function: false,
+ },
+ },
+ ],
+ // By default this is a warning but we want it to error.
+ '@typescript-eslint/explicit-module-boundary-types': 'error',
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ // Never use `require` in TypeScript since they are transpiled out.
+ selector: "CallExpression[callee.name='require']",
+ message: '`require` statements are not allowed. Use `import`.',
+ },
+ {
+ // We need this as NodeJS will run until all the timers have resolved
+ message: 'Use method `Deferred.race()` instead.',
+ selector:
+ 'MemberExpression[object.name="Promise"][property.name="race"]',
+ },
+ {
+ message:
+ 'Deferred `valueOrThrow` should not be called in `Deferred.race()` pass deferred directly',
+ selector:
+ 'CallExpression[callee.object.name="Deferred"][callee.property.name="race"] > ArrayExpression > CallExpression[callee.property.name="valueOrThrow"]',
+ },
+ ],
+ '@typescript-eslint/no-floating-promises': [
+ 'error',
+ {ignoreVoid: true, ignoreIIFE: true},
+ ],
+ '@typescript-eslint/prefer-ts-expect-error': 'error',
+ // This is more performant; see https://v8.dev/blog/fast-async.
+ '@typescript-eslint/return-await': ['error', 'always'],
+ // This optimizes the dependency tracking for type-only files.
+ '@typescript-eslint/consistent-type-imports': 'error',
+ // So type-only exports get elided.
+ '@typescript-eslint/consistent-type-exports': 'error',
+ // Don't want to trigger unintended side-effects.
+ '@typescript-eslint/no-import-type-side-effects': 'error',
+ },
+ overrides: [
+ {
+ files: 'packages/puppeteer-core/src/**/*.ts',
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ patterns: ['*Events', '*.test.js'],
+ paths: [...getThirdPartyPackages()],
+ },
+ ],
+ },
+ },
+ {
+ files: [
+ 'packages/puppeteer-core/src/**/*.test.ts',
+ 'tools/mocha-runner/src/test.ts',
+ ],
+ rules: {
+ // With the Node.js test runner, `describe` and `it` are technically
+ // promises, but we don't need to await them.
+ '@typescript-eslint/no-floating-promises': 'off',
+ },
+ },
+ ],
+ },
+ ],
+};
diff --git a/remote/test/puppeteer/.eslintrc.types.cjs b/remote/test/puppeteer/.eslintrc.types.cjs
new file mode 100644
index 0000000000..f266ee754b
--- /dev/null
+++ b/remote/test/puppeteer/.eslintrc.types.cjs
@@ -0,0 +1,17 @@
+module.exports = {
+ plugins: ['unused-imports'],
+ parser: '@typescript-eslint/parser',
+ rules: {
+ '@typescript-eslint/no-unused-vars': 'off',
+ 'unused-imports/no-unused-imports': 'error',
+ 'unused-imports/no-unused-vars': [
+ 'warn',
+ {
+ vars: 'all',
+ varsIgnorePattern: '^_',
+ args: 'after-used',
+ argsIgnorePattern: '^_',
+ },
+ ],
+ },
+};
diff --git a/remote/test/puppeteer/.mocharc.cjs b/remote/test/puppeteer/.mocharc.cjs
new file mode 100644
index 0000000000..79c6c3bf65
--- /dev/null
+++ b/remote/test/puppeteer/.mocharc.cjs
@@ -0,0 +1,25 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+let timeout = process.platform === 'win32' ? 20_000 : 10_000;
+if (!!process.env.DEBUGGER_ATTACHED) {
+ timeout = 0;
+}
+module.exports = {
+ reporter: 'dot',
+ logLevel: 'debug',
+ require: ['./test/build/mocha-utils.js', 'source-map-support/register'],
+ exit: !!process.env.CI,
+ retries: process.env.CI ? 3 : 0,
+ parallel: !!process.env.PARALLEL,
+ timeout: timeout,
+ reporter: process.env.CI ? 'spec' : 'dot',
+ // This should make mocha crash on uncaught errors.
+ // See https://github.com/mochajs/mocha/blob/master/docs/index.md#--allow-uncaught.
+ allowUncaught: true,
+ // See https://github.com/mochajs/mocha/blob/master/docs/index.md#--async-only--a.
+ asyncOnly: true,
+};
diff --git a/remote/test/puppeteer/.npmrc b/remote/test/puppeteer/.npmrc
new file mode 100644
index 0000000000..94a06c2180
--- /dev/null
+++ b/remote/test/puppeteer/.npmrc
@@ -0,0 +1 @@
+access=public
diff --git a/remote/test/puppeteer/.nvmrc b/remote/test/puppeteer/.nvmrc
new file mode 100644
index 0000000000..85aee5a534
--- /dev/null
+++ b/remote/test/puppeteer/.nvmrc
@@ -0,0 +1 @@
+v20 \ No newline at end of file
diff --git a/remote/test/puppeteer/.prettierignore b/remote/test/puppeteer/.prettierignore
new file mode 100644
index 0000000000..9da3d6ad79
--- /dev/null
+++ b/remote/test/puppeteer/.prettierignore
@@ -0,0 +1,56 @@
+## [START] Keep in sync with .gitignore
+# Dependencies
+node_modules
+
+# Production
+build/
+lib/
+bin/
+
+# Generated files
+**/*.tsbuildinfo
+*.api.json
+*.tgz
+yarn.lock
+.docusaurus/
+.cache-loader
+test/output-*/
+.dev_profile*
+coverage/
+generated/
+.eslintcache
+.cache/
+
+# IDE Artifacts
+.vscode/*
+!.vscode/extensions.json
+!.vscode/*.template.json
+.devcontainer
+
+# Misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Wireit
+.wireit
+## [END] Keep in sync with .gitignore
+
+# Prettier-only ignores.
+CHANGELOG.md
+package-lock.json
+test/assets/
+docs/api
+docs/browsers-api
+versioned_*/
+
+# Ng-schematics
+/packages/ng-schematics/files/
+/packages/ng-schematics/sandbox/
+/packages/ng-schematics/multi/
diff --git a/remote/test/puppeteer/.prettierrc.cjs b/remote/test/puppeteer/.prettierrc.cjs
new file mode 100644
index 0000000000..46c608ced5
--- /dev/null
+++ b/remote/test/puppeteer/.prettierrc.cjs
@@ -0,0 +1,7 @@
+/**
+ * @type {import('prettier').Config}
+ */
+module.exports = {
+ ...require('gts/.prettierrc.json'),
+ // proseWrap: 'always', // Uncomment this while working on Markdown documents. MAKE SURE TO COMMENT THIS BEFORE RUNNING CHECKS/FORMATS OR EVERYTHING WILL BE MODIFIED.
+};
diff --git a/remote/test/puppeteer/.release-please-manifest.json b/remote/test/puppeteer/.release-please-manifest.json
new file mode 100644
index 0000000000..1237fb11dd
--- /dev/null
+++ b/remote/test/puppeteer/.release-please-manifest.json
@@ -0,0 +1,7 @@
+{
+ "packages/puppeteer": "21.10.0",
+ "packages/puppeteer-core": "21.10.0",
+ "packages/testserver": "0.6.0",
+ "packages/ng-schematics": "0.5.6",
+ "packages/browsers": "1.9.1"
+}
diff --git a/remote/test/puppeteer/.vscode/extensions.json b/remote/test/puppeteer/.vscode/extensions.json
new file mode 100644
index 0000000000..4084e393e9
--- /dev/null
+++ b/remote/test/puppeteer/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["google.wireit", "GitHub.vscode-github-actions"]
+}
diff --git a/remote/test/puppeteer/Herebyfile.mjs b/remote/test/puppeteer/Herebyfile.mjs
new file mode 100644
index 0000000000..30f9c75262
--- /dev/null
+++ b/remote/test/puppeteer/Herebyfile.mjs
@@ -0,0 +1,96 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/* eslint-disable import/order */
+
+import {copyFile, readFile, writeFile} from 'fs/promises';
+
+import {docgen, spliceIntoSection} from '@puppeteer/docgen';
+import {execa} from 'execa';
+import {task} from 'hereby';
+import semver from 'semver';
+
+export const docsNgSchematicsTask = task({
+ name: 'docs:ng-schematics',
+ run: async () => {
+ const readme = await readFile('packages/ng-schematics/README.md', 'utf-8');
+ await writeFile('docs/integrations/ng-schematics.md', readme);
+ },
+});
+
+/**
+ * This logic should match the one in `website/docusaurus.config.js`.
+ */
+function getApiUrl(version) {
+ if (semver.gte(version, '19.3.0')) {
+ return `https://github.com/puppeteer/puppeteer/blob/puppeteer-${version}/docs/api/index.md`;
+ } else if (semver.gte(version, '15.3.0')) {
+ return `https://github.com/puppeteer/puppeteer/blob/${version}/docs/api/index.md`;
+ } else {
+ return `https://github.com/puppeteer/puppeteer/blob/${version}/docs/api.md`;
+ }
+}
+
+export const docsChromiumSupportTask = task({
+ name: 'docs:chromium-support',
+ run: async () => {
+ const content = await readFile('docs/chromium-support.md', {
+ encoding: 'utf8',
+ });
+ const {versionsPerRelease} = await import('./versions.js');
+ const buffer = [];
+ for (const [chromiumVersion, puppeteerVersion] of versionsPerRelease) {
+ if (puppeteerVersion === 'NEXT') {
+ continue;
+ }
+ if (semver.gte(puppeteerVersion, '20.0.0')) {
+ buffer.push(
+ ` * [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](${getApiUrl(
+ puppeteerVersion
+ )})`
+ );
+ } else {
+ buffer.push(
+ ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](${getApiUrl(
+ puppeteerVersion
+ )})`
+ );
+ }
+ }
+ await writeFile(
+ 'docs/chromium-support.md',
+ spliceIntoSection('version', content, buffer.join('\n'))
+ );
+ },
+});
+
+export const docsTask = task({
+ name: 'docs',
+ dependencies: [docsNgSchematicsTask, docsChromiumSupportTask],
+ run: async () => {
+ // Copy main page.
+ await copyFile('README.md', 'docs/index.md');
+
+ // Generate documentation
+ for (const [name, folder] of [
+ ['browsers', 'browsers-api'],
+ ['puppeteer', 'api'],
+ ]) {
+ docgen(`docs/${name}.api.json`, `docs/${folder}`);
+ }
+
+ // Update main @puppeteer/browsers page.
+ const readme = await readFile('packages/browsers/README.md', 'utf-8');
+ const index = await readFile('docs/browsers-api/index.md', 'utf-8');
+ await writeFile(
+ 'docs/browsers-api/index.md',
+ index.replace('# API Reference', readme)
+ );
+
+ // Format everything.
+ await execa('prettier', ['--ignore-path', 'none', '--write', 'docs']);
+ },
+});
diff --git a/remote/test/puppeteer/LICENSE b/remote/test/puppeteer/LICENSE
new file mode 100644
index 0000000000..d2c171df74
--- /dev/null
+++ b/remote/test/puppeteer/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/remote/test/puppeteer/README.md b/remote/test/puppeteer/README.md
new file mode 100644
index 0000000000..74a15c6eb9
--- /dev/null
+++ b/remote/test/puppeteer/README.md
@@ -0,0 +1,257 @@
+# Puppeteer
+
+[![Build status](https://github.com/puppeteer/puppeteer/workflows/CI/badge.svg)](https://github.com/puppeteer/puppeteer/actions?query=workflow%3ACI)
+[![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer)
+
+<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"/>
+
+#### [Guides](https://pptr.dev/category/guides) | [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting)
+
+> Puppeteer is a Node.js library which provides a high-level API to control
+> Chrome/Chromium over the
+> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/).
+> Puppeteer runs in
+> [headless](https://developer.chrome.com/articles/new-headless/)
+> mode by default, but can be configured to run in full ("headful")
+> Chrome/Chromium.
+
+#### What can I do?
+
+Most things that you can do manually in the browser can be done using Puppeteer!
+Here are a few examples to get you started:
+
+- Generate screenshots and PDFs of pages.
+- Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e.
+ "SSR" (Server-Side Rendering)).
+- Automate form submission, UI testing, keyboard input, etc.
+- Create an automated testing environment using the latest JavaScript and
+ browser features.
+- Capture a
+ [timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference)
+ of your site to help diagnose performance issues.
+- [Test Chrome Extensions](https://pptr.dev/guides/chrome-extensions).
+
+## Getting Started
+
+### Installation
+
+To use Puppeteer in your project, run:
+
+```bash
+npm i puppeteer
+# or using yarn
+yarn add puppeteer
+# or using pnpm
+pnpm i puppeteer
+```
+
+When you install Puppeteer, it automatically downloads a recent version of
+[Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) (~170MB macOS, ~282MB Linux, ~280MB Windows) and a `chrome-headless-shell` binary (starting with Puppeteer v21.6.0) that is [guaranteed to
+work](https://pptr.dev/faq#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy)
+with Puppeteer. The browser is downloaded to the `$HOME/.cache/puppeteer` folder
+by default (starting with Puppeteer v19.0.0). See [configuration](https://pptr.dev/api/puppeteer.configuration) for configuration options and environmental variables to control the download behavor.
+
+If you deploy a project using Puppeteer to a hosting provider, such as Render or
+Heroku, you might need to reconfigure the location of the cache to be within
+your project folder (see an example below) because not all hosting providers
+include `$HOME/.cache` into the project's deployment.
+
+For a version of Puppeteer without the browser installation, see
+[`puppeteer-core`](#puppeteer-core).
+
+If used with TypeScript, the minimum supported TypeScript version is `4.7.4`.
+
+#### Configuration
+
+Puppeteer uses several defaults that can be customized through configuration
+files.
+
+For example, to change the default cache directory Puppeteer uses to install
+browsers, you can add a `.puppeteerrc.cjs` (or `puppeteer.config.cjs`) at the
+root of your application with the contents
+
+```js
+const {join} = require('path');
+
+/**
+ * @type {import("puppeteer").Configuration}
+ */
+module.exports = {
+ // Changes the cache location for Puppeteer.
+ cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
+};
+```
+
+After adding the configuration file, you will need to remove and reinstall
+`puppeteer` for it to take effect.
+
+See the [configuration guide](https://pptr.dev/guides/configuration) for more
+information.
+
+#### `puppeteer-core`
+
+For every release since v1.7.0 we publish two packages:
+
+- [`puppeteer`](https://www.npmjs.com/package/puppeteer)
+- [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core)
+
+`puppeteer` is a _product_ for browser automation. When installed, it downloads
+a version of Chrome, which it then drives using `puppeteer-core`. Being an
+end-user product, `puppeteer` automates several workflows using reasonable
+defaults [that can be customized](https://pptr.dev/guides/configuration).
+
+`puppeteer-core` is a _library_ to help drive anything that supports DevTools
+protocol. Being a library, `puppeteer-core` is fully driven through its
+programmatic interface implying no defaults are assumed and `puppeteer-core`
+will not download Chrome when installed.
+
+You should use `puppeteer-core` if you are
+[connecting to a remote browser](https://pptr.dev/api/puppeteer.puppeteer.connect)
+or [managing browsers yourself](https://pptr.dev/browsers-api/).
+If you are managing browsers yourself, you will need to call
+[`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) with
+an explicit
+[`executablePath`](https://pptr.dev/api/puppeteer.launchoptions)
+(or [`channel`](https://pptr.dev/api/puppeteer.launchoptions) if it's
+installed in a standard location).
+
+When using `puppeteer-core`, remember to change the import:
+
+```ts
+import puppeteer from 'puppeteer-core';
+```
+
+### Usage
+
+Puppeteer follows the latest
+[maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of
+Node.
+
+Puppeteer will be familiar to people using other browser testing frameworks. You
+[launch](https://pptr.dev/api/puppeteer.puppeteernode.launch)/[connect](https://pptr.dev/api/puppeteer.puppeteernode.connect)
+a [browser](https://pptr.dev/api/puppeteer.browser),
+[create](https://pptr.dev/api/puppeteer.browser.newpage) some
+[pages](https://pptr.dev/api/puppeteer.page), and then manipulate them with
+[Puppeteer's API](https://pptr.dev/api).
+
+For more in-depth usage, check our [guides](https://pptr.dev/category/guides)
+and [examples](https://github.com/puppeteer/puppeteer/tree/main/examples).
+
+#### Example
+
+The following example searches [developer.chrome.com](https://developer.chrome.com/) for blog posts with text "automate beyond recorder", click on the first result and print the full title of the blog post.
+
+```ts
+import puppeteer from 'puppeteer';
+
+(async () => {
+ // Launch the browser and open a new blank page
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ // Navigate the page to a URL
+ await page.goto('https://developer.chrome.com/');
+
+ // Set screen size
+ await page.setViewport({width: 1080, height: 1024});
+
+ // Type into search box
+ await page.type('.devsite-search-field', 'automate beyond recorder');
+
+ // Wait and click on first result
+ const searchResultSelector = '.devsite-result-item-link';
+ await page.waitForSelector(searchResultSelector);
+ await page.click(searchResultSelector);
+
+ // Locate the full title with a unique string
+ const textSelector = await page.waitForSelector(
+ 'text/Customize and automate'
+ );
+ const fullTitle = await textSelector?.evaluate(el => el.textContent);
+
+ // Print the full title
+ console.log('The title of this blog post is "%s".', fullTitle);
+
+ await browser.close();
+})();
+```
+
+### Default runtime settings
+
+**1. Uses Headless mode**
+
+By default Puppeteer launches Chrome in
+[old Headless mode](https://developer.chrome.com/articles/new-headless/).
+
+```ts
+const browser = await puppeteer.launch();
+// Equivalent to
+const browser = await puppeteer.launch({headless: true});
+```
+
+[Chrome 112 launched a new Headless mode](https://developer.chrome.com/articles/new-headless/) that might cause some differences in behavior compared to the old Headless implementation.
+In the future Puppeteer will start defaulting to new implementation.
+We recommend you try it out before the switch:
+
+```ts
+const browser = await puppeteer.launch({headless: 'new'});
+```
+
+To launch a "headful" version of Chrome, set the
+[`headless`](https://pptr.dev/api/puppeteer.browserlaunchargumentoptions) to `false`
+option when launching a browser:
+
+```ts
+const browser = await puppeteer.launch({headless: false});
+```
+
+**2. Runs a bundled version of Chrome**
+
+By default, Puppeteer downloads and uses a specific version of Chrome so its
+API is guaranteed to work out of the box. To use Puppeteer with a different
+version of Chrome or Chromium, pass in the executable's path when creating a
+`Browser` instance:
+
+```ts
+const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
+```
+
+You can also use Puppeteer with Firefox. See
+[status of cross-browser support](https://pptr.dev/faq/#q-what-is-the-status-of-cross-browser-support) for
+more information.
+
+See
+[`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/)
+for a description of the differences between Chromium and Chrome.
+[`This article`](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/chromium_browser_vs_google_chrome.md)
+describes some differences for Linux users.
+
+**3. Creates a fresh user profile**
+
+Puppeteer creates its own browser user profile which it **cleans up on every
+run**.
+
+#### Using Docker
+
+See our [Docker guide](https://pptr.dev/guides/docker).
+
+#### Using Chrome Extensions
+
+See our [Chrome extensions guide](https://pptr.dev/guides/chrome-extensions).
+
+## Resources
+
+- [API Documentation](https://pptr.dev/api)
+- [Guides](https://pptr.dev/category/guides)
+- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples)
+- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer)
+
+## Contributing
+
+Check out our [contributing guide](https://pptr.dev/contributing) to get an
+overview of Puppeteer development.
+
+## FAQ
+
+Our [FAQ](https://pptr.dev/faq) has migrated to
+[our site](https://pptr.dev/faq).
diff --git a/remote/test/puppeteer/SECURITY.md b/remote/test/puppeteer/SECURITY.md
new file mode 100644
index 0000000000..a202138d31
--- /dev/null
+++ b/remote/test/puppeteer/SECURITY.md
@@ -0,0 +1,7 @@
+# Security Policy
+
+The Puppeteer project takes security very seriously. Please use Chromium's process to report security issues.
+
+## Reporting a Vulnerability
+
+See https://www.chromium.org/Home/chromium-security/reporting-security-bugs/
diff --git a/remote/test/puppeteer/examples/README.md b/remote/test/puppeteer/examples/README.md
new file mode 100644
index 0000000000..f20c2e7162
--- /dev/null
+++ b/remote/test/puppeteer/examples/README.md
@@ -0,0 +1,43 @@
+# Running the examples
+
+Assuming you have a checkout of the Puppeteer repo and have run `npm i` (or `yarn`) to install the dependencies, and `npm run build` (or `yarn run build`) to build the project, the examples can be run from the root folder like so:
+
+```bash
+NODE_PATH=../ node examples/search.js
+```
+
+## Larger examples
+
+More complex and use case driven examples can be found at [github.com/GoogleChromeLabs/puppeteer-examples](https://github.com/GoogleChromeLabs/puppeteer-examples).
+
+# Other resources
+
+Other useful tools, articles, and projects that use Puppeteer.
+
+## Rendering and web scraping
+
+- [Puppetron](https://github.com/cheeaun/puppetron) - Demo site that shows how to use Puppeteer and Headless Chrome to render pages. Inspired by [GoogleChrome/rendertron](https://github.com/GoogleChrome/rendertron).
+- [Thal](https://medium.com/@e_mad_ehsan/getting-started-with-puppeteer-and-chrome-headless-for-web-scrapping-6bf5979dee3e 'An article on medium') - Getting started with Puppeteer and Chrome Headless for Web Scraping.
+- [pupperender](https://github.com/LasaleFamine/pupperender) - Express middleware that checks the User-Agent header of incoming requests, and if it matches one of a configurable set of bots, render the page using Puppeteer. Useful for PWA rendering.
+- [headless-chrome-crawler](https://github.com/yujiosaka/headless-chrome-crawler) - Crawler that provides simple APIs to manipulate Headless Chrome and allows you to crawl dynamic websites.
+- [puppeteer-examples](https://github.com/checkly/puppeteer-examples) - Puppeteer Headless Chrome examples for real life use cases such as getting useful info from the web pages or common login scenarios.
+- [browserless](https://github.com/joelgriffith/browserless) - Headless Chrome as a service letting you execute Puppeteer scripts remotely. Provides a docker image with configuration for concurrency, launch arguments and more.
+- [Puppeteer on AWS Lambda](https://github.com/jay-deshmukh/headless-chrome-with-puppeteer-on-AWS-lambda-with-serverless-framework) - Running puppeteer on AWS Lambda with Serverless framework
+- [Apify SDK](https://github.com/apifytech/apify-js) - The scalable web crawling and scraping library for JavaScript. Automatically manages a pool of Puppeteer browsers and provides easy error handling, task management, proxy rotation and more.
+
+## Testing
+
+- [angular-puppeteer-demo](https://github.com/Quramy/angular-puppeteer-demo) - Demo repository explaining how to use Puppeteer in Karma.
+- [mocha-headless-chrome](https://github.com/direct-adv-interfaces/mocha-headless-chrome) - Tool which runs client-side **mocha** tests in the command line through headless Chrome.
+- [puppeteer-to-istanbul-example](https://github.com/bcoe/puppeteer-to-istanbul-example) - Demo repository demonstrating how to output Puppeteer coverage in Istanbul format.
+- [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer) - (almost) Zero configuration tool for setting up and running Jest and Puppeteer easily. Also includes an assertion library for Puppeteer.
+- [puppeteer-har](https://github.com/Everettss/puppeteer-har) - Generate HAR file with puppeteer.
+- [puppetry](https://puppetry.app/) - A desktop app to build Puppeteer/Jest driven tests without coding.
+- [puppeteer-loadtest](https://github.com/svenkatreddy/puppeteer-loadtest) - commandline interface for performing load test on puppeteer scripts.
+- [cucumber-puppeteer-example](https://github.com/mlampedx/cucumber-puppeteer-example) - Example repository demonstrating how to use Puppeeteer and Cucumber for integration testing.
+
+## Services
+
+- [Checkly](https://checklyhq.com) - Monitoring SaaS that uses Puppeteer to check availability and correctness of web pages and apps.
+- [Doppio](https://doppio.sh) - SaaS API to create screenshots or PDFs from HTML/CSS/JS
+- [Doczilla](https://www.doczilla.app) - SaaS API empowering the generation of screenshots or PDFs directly from HTML/CSS/JS code.
diff --git a/remote/test/puppeteer/examples/block-images.js b/remote/test/puppeteer/examples/block-images.js
new file mode 100644
index 0000000000..73a87eb089
--- /dev/null
+++ b/remote/test/puppeteer/examples/block-images.js
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2017 Google Inc., PhantomJS Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.resourceType() === 'image') {
+ request.abort();
+ } else {
+ request.continue();
+ }
+ });
+ await page.goto('https://news.google.com/news/');
+ await page.screenshot({path: 'news.png', fullPage: true});
+
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/cross-browser.js b/remote/test/puppeteer/examples/cross-browser.js
new file mode 100644
index 0000000000..0f972a0b70
--- /dev/null
+++ b/remote/test/puppeteer/examples/cross-browser.js
@@ -0,0 +1,46 @@
+const puppeteer = require('puppeteer');
+
+/**
+ * To have Puppeteer fetch a Firefox binary for you, first run:
+ *
+ * PUPPETEER_PRODUCT=firefox npm install
+ *
+ * To get additional logging about which browser binary is executed,
+ * run this example as:
+ *
+ * DEBUG=puppeteer:launcher NODE_PATH=../ node examples/cross-browser.js
+ *
+ * You can set a custom binary with the `executablePath` launcher option.
+ */
+
+const firefoxOptions = {
+ product: 'firefox',
+ extraPrefsFirefox: {
+ // Enable additional Firefox logging from its protocol implementation
+ // 'remote.log.level': 'Trace',
+ },
+ // Make browser logs visible
+ dumpio: true,
+};
+
+(async () => {
+ const browser = await puppeteer.launch(firefoxOptions);
+
+ const page = await browser.newPage();
+ console.log(await browser.version());
+
+ await page.goto('https://news.ycombinator.com/');
+
+ // Extract articles from the page.
+ const resultsSelector = '.titleline > a';
+ const links = await page.evaluate(resultsSelector => {
+ const anchors = Array.from(document.querySelectorAll(resultsSelector));
+ return anchors.map(anchor => {
+ const title = anchor.textContent.trim();
+ return `${title} - ${anchor.href}`;
+ });
+ }, resultsSelector);
+ console.log(links.join('\n'));
+
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/custom-event.js b/remote/test/puppeteer/examples/custom-event.js
new file mode 100644
index 0000000000..960bca34ee
--- /dev/null
+++ b/remote/test/puppeteer/examples/custom-event.js
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ // Define a window.onCustomEvent function on the page.
+ await page.exposeFunction('onCustomEvent', e => {
+ console.log(`${e.type} fired`, e.detail || '');
+ });
+
+ /**
+ * Attach an event listener to page to capture a custom event on page load/navigation.
+ * @param {string} type Event name.
+ * @returns {!Promise}
+ */
+ function listenFor(type) {
+ return page.evaluateOnNewDocument(type => {
+ document.addEventListener(type, e => {
+ window.onCustomEvent({type, detail: e.detail});
+ });
+ }, type);
+ }
+
+ await listenFor('app-ready'); // Listen for "app-ready" custom event on page load.
+
+ await page.goto('https://www.chromestatus.com/features', {
+ waitUntil: 'networkidle0',
+ });
+
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/detect-sniff.js b/remote/test/puppeteer/examples/detect-sniff.js
new file mode 100644
index 0000000000..2900236fb8
--- /dev/null
+++ b/remote/test/puppeteer/examples/detect-sniff.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2017 Google Inc., PhantomJS Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+function sniffDetector() {
+ const userAgent = window.navigator.userAgent;
+ const platform = window.navigator.platform;
+
+ window.navigator.__defineGetter__('userAgent', function () {
+ window.navigator.sniffed = true;
+ return userAgent;
+ });
+
+ window.navigator.__defineGetter__('platform', function () {
+ window.navigator.sniffed = true;
+ return platform;
+ });
+}
+
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+ await page.evaluateOnNewDocument(sniffDetector);
+ await page.goto('https://www.google.com', {waitUntil: 'networkidle2'});
+ console.log(
+ 'Sniffed: ' +
+ (await page.evaluate(() => {
+ return !!navigator.sniffed;
+ }))
+ );
+
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/oopif.js b/remote/test/puppeteer/examples/oopif.js
new file mode 100644
index 0000000000..6ed79f9ced
--- /dev/null
+++ b/remote/test/puppeteer/examples/oopif.js
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+async function attachFrame(frameId, url) {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ frame.id = frameId;
+ document.body.appendChild(frame);
+ await new Promise(x => {
+ return (frame.onload = x);
+ });
+ return frame;
+}
+
+(async () => {
+ // Launch browser in non-headless mode.
+ const browser = await puppeteer.launch({headless: false});
+ const page = await browser.newPage();
+
+ // Load a page from one origin:
+ await page.goto('http://example.org/');
+
+ // Inject iframe with the another origin.
+ await page.evaluateHandle(attachFrame, 'frame1', 'https://example.com/');
+
+ // At this point there should be a message in the output:
+ // puppeteer:frame The frame '...' moved to another session. Out-of-process
+ // iframes (OOPIF) are not supported by Puppeteer yet.
+ // https://github.com/puppeteer/puppeteer/issues/2548
+
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/pdf.js b/remote/test/puppeteer/examples/pdf.js
new file mode 100644
index 0000000000..e97cc53cdb
--- /dev/null
+++ b/remote/test/puppeteer/examples/pdf.js
@@ -0,0 +1,25 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+ await page.goto('https://news.ycombinator.com', {
+ waitUntil: 'networkidle2',
+ });
+ // page.pdf() is currently supported only in headless mode.
+ // @see https://bugs.chromium.org/p/chromium/issues/detail?id=753118
+ await page.pdf({
+ path: 'hn.pdf',
+ format: 'letter',
+ });
+
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/proxy.js b/remote/test/puppeteer/examples/proxy.js
new file mode 100644
index 0000000000..e41d0d8cd1
--- /dev/null
+++ b/remote/test/puppeteer/examples/proxy.js
@@ -0,0 +1,25 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+(async () => {
+ const browser = await puppeteer.launch({
+ // Launch chromium using a proxy server on port 9876.
+ // More on proxying:
+ // https://www.chromium.org/developers/design-documents/network-settings
+ args: [
+ '--proxy-server=127.0.0.1:9876',
+ // Use proxy for localhost URLs
+ '--proxy-bypass-list=<-loopback>',
+ ],
+ });
+ const page = await browser.newPage();
+ await page.goto('https://google.com');
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/screenshot-fullpage.js b/remote/test/puppeteer/examples/screenshot-fullpage.js
new file mode 100644
index 0000000000..cbc3d5e782
--- /dev/null
+++ b/remote/test/puppeteer/examples/screenshot-fullpage.js
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+ await page.emulate(puppeteer.devices['iPhone 6']);
+ await page.goto('https://www.nytimes.com/');
+ await page.screenshot({path: 'full.png', fullPage: true});
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/screenshot.js b/remote/test/puppeteer/examples/screenshot.js
new file mode 100644
index 0000000000..85c8462cb5
--- /dev/null
+++ b/remote/test/puppeteer/examples/screenshot.js
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+ await page.goto('http://example.com');
+ await page.screenshot({path: 'example.png'});
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/examples/search.js b/remote/test/puppeteer/examples/search.js
new file mode 100644
index 0000000000..7c2a081808
--- /dev/null
+++ b/remote/test/puppeteer/examples/search.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @fileoverview Search developers.google.com/web for articles tagged
+ * "Headless Chrome" and scrape results from the results page.
+ */
+
+'use strict';
+
+const puppeteer = require('puppeteer');
+
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ await page.goto('https://developers.google.com/web/');
+
+ // Type into search box.
+ await page.type('.devsite-search-field', 'Headless Chrome');
+
+ // Wait for suggest overlay to appear and click "show all results".
+ const allResultsSelector = '.devsite-suggest-all-results';
+ await page.waitForSelector(allResultsSelector);
+ await page.click(allResultsSelector);
+
+ // Wait for the results page to load and display the results.
+ const resultsSelector = '.gsc-table-result a.gs-title[href]';
+ await page.waitForSelector(resultsSelector);
+
+ // Extract the results from the page.
+ const links = await page.evaluate(resultsSelector => {
+ const anchors = Array.from(document.querySelectorAll(resultsSelector));
+ return anchors.map(anchor => {
+ const title = anchor.textContent.split('|')[0].trim();
+ return `${title} - ${anchor.href}`;
+ });
+ }, resultsSelector);
+ console.log(links.join('\n'));
+
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/json-mocha-reporter.js b/remote/test/puppeteer/json-mocha-reporter.js
new file mode 100644
index 0000000000..ffe1d60675
--- /dev/null
+++ b/remote/test/puppeteer/json-mocha-reporter.js
@@ -0,0 +1,69 @@
+const mocha = require('mocha');
+module.exports = JSONExtra;
+
+const constants = mocha.Runner.constants;
+
+/*
+
+This is a copy of
+https://github.com/mochajs/mocha/blob/master/lib/reporters/json-stream.js
+with more event hooks. mocha does not support extending reporters or using
+multiple reporters so a custom reporter is needed and it must be local
+to the project.
+
+*/
+
+function JSONExtra(runner, options) {
+ mocha.reporters.Base.call(this, runner, options);
+ mocha.reporters.JSON.call(this, runner, options);
+ const self = this;
+
+ runner.once(constants.EVENT_RUN_BEGIN, function () {
+ writeEvent(['start', {total: runner.total}]);
+ });
+
+ runner.on(constants.EVENT_TEST_PASS, function (test) {
+ writeEvent(['pass', clean(test)]);
+ });
+
+ runner.on(constants.EVENT_TEST_FAIL, function (test, err) {
+ test = clean(test);
+ test.err = err.message;
+ test.stack = err.stack || null;
+ writeEvent(['fail', test]);
+ });
+
+ runner.once(constants.EVENT_RUN_END, function () {
+ writeEvent(['end', self.stats]);
+ });
+
+ runner.on(constants.EVENT_TEST_BEGIN, function (test) {
+ writeEvent(['test-start', clean(test)]);
+ });
+
+ runner.on(constants.EVENT_TEST_PENDING, function (test) {
+ writeEvent(['pending', clean(test)]);
+ });
+}
+
+function writeEvent(event) {
+ process.stdout.write(JSON.stringify(event) + '\n');
+}
+
+/**
+ * Returns an object literal representation of `test`
+ * free of cyclic properties, etc.
+ *
+ * @private
+ * @param {Object} test - Instance used as data source.
+ * @return {Object} object containing pared-down test instance data
+ */
+function clean(test) {
+ return {
+ title: test.title,
+ fullTitle: test.fullTitle(),
+ file: test.file,
+ duration: test.duration,
+ currentRetry: test.currentRetry(),
+ };
+}
diff --git a/remote/test/puppeteer/moz.yaml b/remote/test/puppeteer/moz.yaml
new file mode 100644
index 0000000000..5f732140c9
--- /dev/null
+++ b/remote/test/puppeteer/moz.yaml
@@ -0,0 +1,10 @@
+bugzilla:
+ component: Agent
+ product: Remote Protocol
+origin:
+ description: Headless Chrome Node API
+ license: Apache-2.0
+ name: puppeteer
+ release: puppeteer-v21.10.0
+ url: ../puppeteer
+schema: 1
diff --git a/remote/test/puppeteer/package-lock.json b/remote/test/puppeteer/package-lock.json
new file mode 100644
index 0000000000..76878ce829
--- /dev/null
+++ b/remote/test/puppeteer/package-lock.json
@@ -0,0 +1,11657 @@
+{
+ "name": "puppeteer-repo",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "puppeteer-repo",
+ "hasInstallScript": true,
+ "workspaces": [
+ "packages/*",
+ "test",
+ "test/installation",
+ "tools/eslint",
+ "tools/doctest",
+ "tools/docgen",
+ "tools/mocha-runner"
+ ],
+ "devDependencies": {
+ "@actions/core": "1.10.1",
+ "@types/mocha": "10.0.6",
+ "@types/node": "20.8.4",
+ "@types/semver": "7.5.6",
+ "@types/sinon": "17.0.3",
+ "@typescript-eslint/eslint-plugin": "6.19.1",
+ "@typescript-eslint/parser": "6.19.1",
+ "esbuild": "0.20.0",
+ "eslint": "8.56.0",
+ "eslint-config-prettier": "9.1.0",
+ "eslint-import-resolver-typescript": "3.6.1",
+ "eslint-plugin-import": "2.29.1",
+ "eslint-plugin-mocha": "10.2.0",
+ "eslint-plugin-prettier": "5.1.3",
+ "eslint-plugin-rulesdir": "0.2.2",
+ "eslint-plugin-tsdoc": "0.2.17",
+ "eslint-plugin-unused-imports": "3.0.0",
+ "execa": "8.0.1",
+ "expect": "29.7.0",
+ "gts": "5.2.0",
+ "hereby": "1.8.9",
+ "license-checker": "25.0.1",
+ "mocha": "10.2.0",
+ "npm-run-all": "4.1.5",
+ "prettier": "3.2.4",
+ "semver": "7.5.4",
+ "sinon": "17.0.1",
+ "source-map-support": "0.5.21",
+ "spdx-satisfies": "5.0.1",
+ "tsd": "0.30.4",
+ "tsx": "4.7.0",
+ "typescript": "5.3.3",
+ "wireit": "0.14.4"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@actions/core": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz",
+ "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==",
+ "dev": true,
+ "dependencies": {
+ "@actions/http-client": "^2.0.1",
+ "uuid": "^8.3.2"
+ }
+ },
+ "node_modules/@actions/http-client": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz",
+ "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==",
+ "dev": true,
+ "dependencies": {
+ "tunnel": "^0.0.6",
+ "undici": "^5.25.4"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+ "dependencies": {
+ "@babel/highlight": "^7.23.4",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+ },
+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz",
+ "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz",
+ "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz",
+ "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz",
+ "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz",
+ "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz",
+ "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz",
+ "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz",
+ "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz",
+ "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz",
+ "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz",
+ "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz",
+ "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz",
+ "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz",
+ "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz",
+ "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz",
+ "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz",
+ "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz",
+ "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz",
+ "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz",
+ "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz",
+ "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz",
+ "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz",
+ "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.56.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
+ "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@fastify/busboy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
+ "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.14",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
+ "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.2",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
+ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
+ "dev": true
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
+ "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
+ "dev": true,
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+ "dev": true,
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
+ "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+ "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
+ "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@ljharb/through": {
+ "version": "2.3.12",
+ "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz",
+ "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@microsoft/api-documenter": {
+ "version": "7.23.20",
+ "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.23.20.tgz",
+ "integrity": "sha512-61V6sukyYZ5jQEdyvDFzInaIRTd0wgT2ECKPanr2ba0fc+Mien+KIr5shz9EAqJMZz0GifTnw9HmJqsfR688xA==",
+ "dev": true,
+ "dependencies": {
+ "@microsoft/api-extractor-model": "7.28.7",
+ "@microsoft/tsdoc": "0.14.2",
+ "@rushstack/node-core-library": "3.64.2",
+ "@rushstack/ts-command-line": "4.17.1",
+ "colors": "~1.2.1",
+ "js-yaml": "~3.13.1",
+ "resolve": "~1.22.1"
+ },
+ "bin": {
+ "api-documenter": "bin/api-documenter"
+ }
+ },
+ "node_modules/@microsoft/api-documenter/node_modules/js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@microsoft/api-extractor": {
+ "version": "7.39.4",
+ "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.39.4.tgz",
+ "integrity": "sha512-6YvfkpbEqRQ0UPdVBc+lOiq7VlXi9kw8U3w+RcXCFDVc/UljlXU5l9fHEyuBAW1GGO2opUe+yf9OscWhoHANhg==",
+ "dev": true,
+ "dependencies": {
+ "@microsoft/api-extractor-model": "7.28.7",
+ "@microsoft/tsdoc": "0.14.2",
+ "@microsoft/tsdoc-config": "~0.16.1",
+ "@rushstack/node-core-library": "3.64.2",
+ "@rushstack/rig-package": "0.5.1",
+ "@rushstack/ts-command-line": "4.17.1",
+ "colors": "~1.2.1",
+ "lodash": "~4.17.15",
+ "resolve": "~1.22.1",
+ "semver": "~7.5.4",
+ "source-map": "~0.6.1",
+ "typescript": "5.3.3"
+ },
+ "bin": {
+ "api-extractor": "bin/api-extractor"
+ }
+ },
+ "node_modules/@microsoft/api-extractor-model": {
+ "version": "7.28.7",
+ "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.7.tgz",
+ "integrity": "sha512-4gCGGEQGHmbQmarnDcEWS2cjj0LtNuD3D6rh3ZcAyAYTkceAugAk2eyQHGdTcGX8w3qMjWCTU1TPb8xHnMM+Kg==",
+ "dev": true,
+ "dependencies": {
+ "@microsoft/tsdoc": "0.14.2",
+ "@microsoft/tsdoc-config": "~0.16.1",
+ "@rushstack/node-core-library": "3.64.2"
+ }
+ },
+ "node_modules/@microsoft/tsdoc": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
+ "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==",
+ "dev": true
+ },
+ "node_modules/@microsoft/tsdoc-config": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz",
+ "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==",
+ "dev": true,
+ "dependencies": {
+ "@microsoft/tsdoc": "0.14.2",
+ "ajv": "~6.12.6",
+ "jju": "~1.4.0",
+ "resolve": "~1.19.0"
+ }
+ },
+ "node_modules/@microsoft/tsdoc-config/node_modules/resolve": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
+ "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.1.0",
+ "path-parse": "^1.0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@npmcli/agent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.0.tgz",
+ "integrity": "sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.1",
+ "lru-cache": "^10.0.1",
+ "socks-proxy-agent": "^8.0.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/agent/node_modules/lru-cache": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
+ "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
+ "dev": true,
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz",
+ "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.4.tgz",
+ "integrity": "sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/promise-spawn": "^7.0.0",
+ "lru-cache": "^10.0.1",
+ "npm-pick-manifest": "^9.0.0",
+ "proc-log": "^3.0.0",
+ "promise-inflight": "^1.0.1",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^4.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/lru-cache": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
+ "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
+ "dev": true,
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/which": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/installed-package-contents": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz",
+ "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==",
+ "dev": true,
+ "dependencies": {
+ "npm-bundled": "^3.0.0",
+ "npm-normalize-package-bin": "^3.0.0"
+ },
+ "bin": {
+ "installed-package-contents": "lib/index.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/installed-package-contents/node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/node-gyp": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz",
+ "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.0.tgz",
+ "integrity": "sha512-OI2zdYBLhQ7kpNPaJxiflofYIpkNLi+lnGdzqUOfRmCF3r2l1nadcjtCYMJKv/Utm/ZtlffaUuTiAktPHbc17g==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/git": "^5.0.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^7.0.0",
+ "json-parse-even-better-errors": "^3.0.0",
+ "normalize-package-data": "^6.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/hosted-git-info": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz",
+ "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/json-parse-even-better-errors": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz",
+ "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/lru-cache": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
+ "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
+ "dev": true,
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/normalize-package-data": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz",
+ "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^7.0.0",
+ "is-core-module": "^2.8.1",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz",
+ "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==",
+ "dev": true,
+ "dependencies": {
+ "which": "^4.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn/node_modules/which": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/run-script": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz",
+ "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/node-gyp": "^3.0.0",
+ "@npmcli/package-json": "^5.0.0",
+ "@npmcli/promise-spawn": "^7.0.0",
+ "node-gyp": "^10.0.0",
+ "which": "^4.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/which": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
+ "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@pptr/testserver": {
+ "resolved": "packages/testserver",
+ "link": true
+ },
+ "node_modules/@prettier/sync": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@prettier/sync/-/sync-0.5.0.tgz",
+ "integrity": "sha512-1a6veNypZYkSbU33anha4Pdna9Jz3HXUc0aru7sgN7HuyJHPIVNdCTfjhm1S+mG9yXmWuAO+a6I+Cznp9Ogt3A==",
+ "dev": true,
+ "dependencies": {
+ "make-synchronized": "^0.2.5"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier-synchronized?sponsor=1"
+ },
+ "peerDependencies": {
+ "prettier": "*"
+ }
+ },
+ "node_modules/@puppeteer-test/installation": {
+ "resolved": "test/installation",
+ "link": true
+ },
+ "node_modules/@puppeteer-test/test": {
+ "resolved": "test",
+ "link": true
+ },
+ "node_modules/@puppeteer/browsers": {
+ "resolved": "packages/browsers",
+ "link": true
+ },
+ "node_modules/@puppeteer/docgen": {
+ "resolved": "tools/docgen",
+ "link": true
+ },
+ "node_modules/@puppeteer/doctest": {
+ "resolved": "tools/doctest",
+ "link": true
+ },
+ "node_modules/@puppeteer/eslint": {
+ "resolved": "tools/eslint",
+ "link": true
+ },
+ "node_modules/@puppeteer/mocha-runner": {
+ "resolved": "tools/mocha-runner",
+ "link": true
+ },
+ "node_modules/@puppeteer/ng-schematics": {
+ "resolved": "packages/ng-schematics",
+ "link": true
+ },
+ "node_modules/@rushstack/node-core-library": {
+ "version": "3.64.2",
+ "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.64.2.tgz",
+ "integrity": "sha512-n1S2VYEklONiwKpUyBq/Fym6yAsfsCXrqFabuOMcCuj4C+zW+HyaspSHXJCKqkMxfjviwe/c9+DUqvRWIvSN9Q==",
+ "dev": true,
+ "dependencies": {
+ "colors": "~1.2.1",
+ "fs-extra": "~7.0.1",
+ "import-lazy": "~4.0.0",
+ "jju": "~1.4.0",
+ "resolve": "~1.22.1",
+ "semver": "~7.5.4",
+ "z-schema": "~5.0.2"
+ },
+ "peerDependencies": {
+ "@types/node": "*"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rushstack/rig-package": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz",
+ "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "~1.22.1",
+ "strip-json-comments": "~3.1.1"
+ }
+ },
+ "node_modules/@rushstack/ts-command-line": {
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.17.1.tgz",
+ "integrity": "sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==",
+ "dev": true,
+ "dependencies": {
+ "@types/argparse": "1.0.38",
+ "argparse": "~1.0.9",
+ "colors": "~1.2.1",
+ "string-argv": "~0.3.1"
+ }
+ },
+ "node_modules/@sigstore/bundle": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.1.tgz",
+ "integrity": "sha512-v3/iS+1nufZdKQ5iAlQKcCsoh0jffQyABvYIxKsZQFWc4ubuGjwZklFHpDgV6O6T7vvV78SW5NHI91HFKEcxKg==",
+ "dev": true,
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.2.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sigstore/core": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-0.2.0.tgz",
+ "integrity": "sha512-THobAPPZR9pDH2CAvDLpkrYedt7BlZnsyxDe+Isq4ZmGfPy5juOFZq487vCU2EgKD7aHSiTfE/i7sN7aEdzQnA==",
+ "dev": true,
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sigstore/protobuf-specs": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz",
+ "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sigstore/sign": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.1.tgz",
+ "integrity": "sha512-U5sKQEj+faE1MsnLou1f4DQQHeFZay+V9s9768lw48J4pKykPj34rWyI1lsMOGJ3Mae47Ye6q3HAJvgXO21rkQ==",
+ "dev": true,
+ "dependencies": {
+ "@sigstore/bundle": "^2.1.1",
+ "@sigstore/core": "^0.2.0",
+ "@sigstore/protobuf-specs": "^0.2.1",
+ "make-fetch-happen": "^13.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sigstore/tuf": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.0.tgz",
+ "integrity": "sha512-S98jo9cpJwO1mtQ+2zY7bOdcYyfVYCUaofCG6wWRzk3pxKHVAkSfshkfecto2+LKsx7Ovtqbgb2LS8zTRhxJ9Q==",
+ "dev": true,
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.2.1",
+ "tuf-js": "^2.2.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sigstore/verify": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-0.1.0.tgz",
+ "integrity": "sha512-2UzMNYAa/uaz11NhvgRnIQf4gpLTJ59bhb8ESXaoSS5sxedfS+eLak8bsdMc+qpNQfITUTFoSKFx5h8umlRRiA==",
+ "dev": true,
+ "dependencies": {
+ "@sigstore/bundle": "^2.1.1",
+ "@sigstore/core": "^0.2.0",
+ "@sigstore/protobuf-specs": "^0.2.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "11.2.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
+ "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@sinonjs/samsam": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
+ "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^2.0.0",
+ "lodash.get": "^4.4.2",
+ "type-detect": "^4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+ "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/text-encoding": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
+ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
+ "dev": true
+ },
+ "node_modules/@swc/core": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz",
+ "integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@swc/counter": "^0.1.1",
+ "@swc/types": "^0.1.5"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.3.107",
+ "@swc/core-darwin-x64": "1.3.107",
+ "@swc/core-linux-arm-gnueabihf": "1.3.107",
+ "@swc/core-linux-arm64-gnu": "1.3.107",
+ "@swc/core-linux-arm64-musl": "1.3.107",
+ "@swc/core-linux-x64-gnu": "1.3.107",
+ "@swc/core-linux-x64-musl": "1.3.107",
+ "@swc/core-win32-arm64-msvc": "1.3.107",
+ "@swc/core-win32-ia32-msvc": "1.3.107",
+ "@swc/core-win32-x64-msvc": "1.3.107"
+ },
+ "peerDependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz",
+ "integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz",
+ "integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz",
+ "integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz",
+ "integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz",
+ "integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz",
+ "integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz",
+ "integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz",
+ "integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz",
+ "integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.3.107",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz",
+ "integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz",
+ "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==",
+ "dev": true
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
+ "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
+ "dev": true
+ },
+ "node_modules/@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="
+ },
+ "node_modules/@tsd/typescript": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.3.3.tgz",
+ "integrity": "sha512-CQlfzol0ldaU+ftWuG52vH29uRoKboLinLy84wS8TQOu+m+tWoaUfk4svL4ij2V8M5284KymJBlHUusKj6k34w==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/@tufjs/canonical-json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
+ "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==",
+ "dev": true,
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@tufjs/models": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz",
+ "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==",
+ "dev": true,
+ "dependencies": {
+ "@tufjs/canonical-json": "2.0.0",
+ "minimatch": "^9.0.3"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/argparse": {
+ "version": "1.0.38",
+ "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz",
+ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==",
+ "dev": true
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/diff": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.9.tgz",
+ "integrity": "sha512-RWVEhh/zGXpAVF/ZChwNnv7r4rvqzJ7lYNSmZSVTxjV0PBLf6Qu7RNg+SUtkpzxmiNkjCx0Xn2tPp7FIkshJwQ==",
+ "dev": true
+ },
+ "node_modules/@types/doctrine": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz",
+ "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==",
+ "dev": true
+ },
+ "node_modules/@types/eslint": {
+ "version": "8.56.2",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
+ "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "dev": true
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+ "dev": true
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
+ "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
+ "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@types/mime": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.4.tgz",
+ "integrity": "sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==",
+ "dev": true
+ },
+ "node_modules/@types/minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
+ "dev": true
+ },
+ "node_modules/@types/mocha": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz",
+ "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==",
+ "dev": true
+ },
+ "node_modules/@types/ms": {
+ "version": "0.7.34",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
+ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "20.8.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.4.tgz",
+ "integrity": "sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==",
+ "devOptional": true,
+ "dependencies": {
+ "undici-types": "~5.25.1"
+ }
+ },
+ "node_modules/@types/normalize-package-data": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+ "dev": true
+ },
+ "node_modules/@types/pixelmatch": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.6.tgz",
+ "integrity": "sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/pngjs": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.4.tgz",
+ "integrity": "sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/progress": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.7.tgz",
+ "integrity": "sha512-iadjw02vte8qWx7U0YM++EybBha2CQLPGu9iJ97whVgJUT5Zq9MjAPYUnbfRI2Kpehimf1QjFJYxD0t8nqzu5w==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.5.6",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
+ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
+ "dev": true
+ },
+ "node_modules/@types/sinon": {
+ "version": "17.0.3",
+ "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz",
+ "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==",
+ "dev": true,
+ "dependencies": {
+ "@types/sinonjs__fake-timers": "*"
+ }
+ },
+ "node_modules/@types/sinonjs__fake-timers": {
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz",
+ "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==",
+ "dev": true
+ },
+ "node_modules/@types/source-map-support": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz",
+ "integrity": "sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA==",
+ "dev": true,
+ "dependencies": {
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
+ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
+ "dev": true
+ },
+ "node_modules/@types/tar-fs": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/tar-fs/-/tar-fs-2.0.4.tgz",
+ "integrity": "sha512-ipPec0CjTmVDWE+QKr9cTmIIoTl7dFG/yARCM5MqK8i6CNLIG1P8x4kwDsOQY1ChZOZjH0wO9nvfgBvWl4R3kA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tar-stream": "*"
+ }
+ },
+ "node_modules/@types/tar-stream": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.3.tgz",
+ "integrity": "sha512-Zbnx4wpkWBMBSu5CytMbrT5ZpMiF55qgM+EpHzR4yIDu7mv52cej8hTkOc6K+LzpkOAbxwn/m7j3iO+/l42YkQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/through": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz",
+ "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/unbzip2-stream": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/@types/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
+ "integrity": "sha512-D8X5uuJRISqc8YtwL8jNW2FpPdUOCYXbfD6zNROCTbVXK9nawucxh10tVXE3MPjnHdRA1LvB0zDxVya/lBsnYw==",
+ "dev": true,
+ "dependencies": {
+ "@types/through": "*"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
+ "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.32",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
+ "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
+ "dev": true,
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.3",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+ "dev": true
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz",
+ "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.19.1",
+ "@typescript-eslint/type-utils": "6.19.1",
+ "@typescript-eslint/utils": "6.19.1",
+ "@typescript-eslint/visitor-keys": "6.19.1",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz",
+ "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "6.19.1",
+ "@typescript-eslint/types": "6.19.1",
+ "@typescript-eslint/typescript-estree": "6.19.1",
+ "@typescript-eslint/visitor-keys": "6.19.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz",
+ "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.19.1",
+ "@typescript-eslint/visitor-keys": "6.19.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz",
+ "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "6.19.1",
+ "@typescript-eslint/utils": "6.19.1",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz",
+ "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz",
+ "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.19.1",
+ "@typescript-eslint/visitor-keys": "6.19.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz",
+ "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.19.1",
+ "@typescript-eslint/types": "6.19.1",
+ "@typescript-eslint/typescript-estree": "6.19.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz",
+ "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.19.1",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
+ "node_modules/@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+ "dev": true
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "node_modules/acorn": {
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
+ "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/array-back": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz",
+ "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
+ "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-find-index": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+ "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
+ "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz",
+ "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0",
+ "get-intrinsic": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
+ "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
+ "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
+ "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "is-array-buffer": "^3.0.2",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true
+ },
+ "node_modules/ast-types": {
+ "version": "0.13.4",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ast-types/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/b4a": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
+ "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/basic-ftp": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz",
+ "integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "node_modules/builtins": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
+ "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.0.0"
+ }
+ },
+ "node_modules/c8": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz",
+ "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==",
+ "dev": true,
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@istanbuljs/schema": "^0.1.3",
+ "find-up": "^5.0.0",
+ "foreground-child": "^3.1.1",
+ "istanbul-lib-coverage": "^3.2.0",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.1.6",
+ "test-exclude": "^6.0.0",
+ "v8-to-istanbul": "^9.0.0",
+ "yargs": "^17.7.2",
+ "yargs-parser": "^21.1.1"
+ },
+ "bin": {
+ "c8": "bin/c8.js"
+ },
+ "engines": {
+ "node": ">=14.14.0"
+ }
+ },
+ "node_modules/c8/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/c8/node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/c8/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "18.0.2",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz",
+ "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/fs": "^3.1.0",
+ "fs-minipass": "^3.0.0",
+ "glob": "^10.2.2",
+ "lru-cache": "^10.0.1",
+ "minipass": "^7.0.3",
+ "minipass-collect": "^2.0.1",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "p-map": "^4.0.0",
+ "ssri": "^10.0.0",
+ "tar": "^6.1.11",
+ "unique-filename": "^3.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
+ "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
+ "dev": true,
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+ "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.1",
+ "set-function-length": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-keys": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
+ "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "map-obj": "^4.0.0",
+ "quick-lru": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chromium-bidi": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.6.tgz",
+ "integrity": "sha512-ber8smgoAs4EqSUHRb0I8fpx371ZmvsdQav8HRM9oO4fk5Ox16vQiNYXlsZkRj4FfvVL2dCef+zBFQixp+79CA==",
+ "dependencies": {
+ "mitt": "3.0.1",
+ "urlpattern-polyfill": "10.0.0"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
+ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/colors": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz",
+ "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/command-line-usage": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz",
+ "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==",
+ "dev": true,
+ "dependencies": {
+ "array-back": "^4.0.2",
+ "chalk": "^2.4.2",
+ "table-layout": "^1.0.2",
+ "typical": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/command-line-usage/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/command-line-usage/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/command-line-usage/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/command-line-usage/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/command-line-usage/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/command-line-usage/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/command-line-usage/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/cross-fetch": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
+ "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
+ "dependencies": {
+ "node-fetch": "^2.6.12"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz",
+ "integrity": "sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/debuglog": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
+ "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decamelize-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
+ "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
+ "dev": true,
+ "dependencies": {
+ "decamelize": "^1.1.0",
+ "map-obj": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decamelize-keys/node_modules/map-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+ "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+ "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/degenerator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+ "dependencies": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1232444",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1232444.tgz",
+ "integrity": "sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg=="
+ },
+ "node_modules/dezalgo": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+ "dev": true,
+ "dependencies": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+ "dev": true,
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.15.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+ "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.22.3",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
+ "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "arraybuffer.prototype.slice": "^1.0.2",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.5",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.6",
+ "get-intrinsic": "^1.2.2",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.12",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.13.1",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.1",
+ "safe-array-concat": "^1.0.1",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.8",
+ "string.prototype.trimend": "^1.0.7",
+ "string.prototype.trimstart": "^1.0.7",
+ "typed-array-buffer": "^1.0.0",
+ "typed-array-byte-length": "^1.0.0",
+ "typed-array-byte-offset": "^1.0.0",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
+ "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.2",
+ "has-tostringtag": "^1.0.0",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+ "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz",
+ "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.20.0",
+ "@esbuild/android-arm": "0.20.0",
+ "@esbuild/android-arm64": "0.20.0",
+ "@esbuild/android-x64": "0.20.0",
+ "@esbuild/darwin-arm64": "0.20.0",
+ "@esbuild/darwin-x64": "0.20.0",
+ "@esbuild/freebsd-arm64": "0.20.0",
+ "@esbuild/freebsd-x64": "0.20.0",
+ "@esbuild/linux-arm": "0.20.0",
+ "@esbuild/linux-arm64": "0.20.0",
+ "@esbuild/linux-ia32": "0.20.0",
+ "@esbuild/linux-loong64": "0.20.0",
+ "@esbuild/linux-mips64el": "0.20.0",
+ "@esbuild/linux-ppc64": "0.20.0",
+ "@esbuild/linux-riscv64": "0.20.0",
+ "@esbuild/linux-s390x": "0.20.0",
+ "@esbuild/linux-x64": "0.20.0",
+ "@esbuild/netbsd-x64": "0.20.0",
+ "@esbuild/openbsd-x64": "0.20.0",
+ "@esbuild/sunos-x64": "0.20.0",
+ "@esbuild/win32-arm64": "0.20.0",
+ "@esbuild/win32-ia32": "0.20.0",
+ "@esbuild/win32-x64": "0.20.0"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.56.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
+ "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.56.0",
+ "@humanwhocodes/config-array": "^0.11.13",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-formatter-pretty": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz",
+ "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/eslint": "^7.2.13",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.0",
+ "eslint-rule-docs": "^1.1.5",
+ "log-symbols": "^4.0.0",
+ "plur": "^4.0.0",
+ "string-width": "^4.2.0",
+ "supports-hyperlinks": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-formatter-pretty/node_modules/@types/eslint": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz",
+ "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz",
+ "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4",
+ "enhanced-resolve": "^5.12.0",
+ "eslint-module-utils": "^2.7.4",
+ "fast-glob": "^3.3.1",
+ "get-tsconfig": "^4.5.0",
+ "is-core-module": "^2.11.0",
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
+ "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-es": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
+ "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==",
+ "dev": true,
+ "dependencies": {
+ "eslint-utils": "^2.0.0",
+ "regexpp": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=4.19.1"
+ }
+ },
+ "node_modules/eslint-plugin-es/node_modules/eslint-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
+ "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.7",
+ "array.prototype.findlastindex": "^1.2.3",
+ "array.prototype.flat": "^1.3.2",
+ "array.prototype.flatmap": "^1.3.2",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.8.0",
+ "hasown": "^2.0.0",
+ "is-core-module": "^2.13.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.7",
+ "object.groupby": "^1.0.1",
+ "object.values": "^1.1.7",
+ "semver": "^6.3.1",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-mocha": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.2.0.tgz",
+ "integrity": "sha512-ZhdxzSZnd1P9LqDPF0DBcFLpRIGdh1zkF2JHnQklKQOvrQtT73kdP5K9V2mzvbLR+cCAO9OI48NXK/Ax9/ciCQ==",
+ "dev": true,
+ "dependencies": {
+ "eslint-utils": "^3.0.0",
+ "rambda": "^7.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-node": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
+ "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==",
+ "dev": true,
+ "dependencies": {
+ "eslint-plugin-es": "^3.0.0",
+ "eslint-utils": "^2.0.0",
+ "ignore": "^5.1.1",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.10.1",
+ "semver": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=5.16.0"
+ }
+ },
+ "node_modules/eslint-plugin-node/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-node/node_modules/eslint-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/eslint-plugin-node/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eslint-plugin-node/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-node/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
+ "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
+ "dev": true,
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.8.6"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": "*",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-rulesdir": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.2.tgz",
+ "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-tsdoc": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz",
+ "integrity": "sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==",
+ "dev": true,
+ "dependencies": {
+ "@microsoft/tsdoc": "0.14.2",
+ "@microsoft/tsdoc-config": "0.16.2"
+ }
+ },
+ "node_modules/eslint-plugin-unused-imports": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz",
+ "integrity": "sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==",
+ "dev": true,
+ "dependencies": {
+ "eslint-rule-composer": "^0.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "eslint": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@typescript-eslint/eslint-plugin": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-rule-composer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz",
+ "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/eslint-rule-docs": {
+ "version": "1.1.235",
+ "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz",
+ "integrity": "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==",
+ "dev": true
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/exponential-backoff": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz",
+ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==",
+ "dev": true
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extract-zip/node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true
+ },
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
+ "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/figures/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/find-up-simple": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz",
+ "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+ "dev": true
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
+ "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
+ "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
+ "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+ "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.7.2",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz",
+ "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==",
+ "dev": true,
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/get-uri": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.2.tgz",
+ "integrity": "sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==",
+ "dependencies": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.0",
+ "debug": "^4.3.4",
+ "fs-extra": "^8.1.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/get-uri/node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.3.10",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+ "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^2.3.5",
+ "minimatch": "^9.0.1",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+ "path-scurry": "^1.10.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/gts": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/gts/-/gts-5.2.0.tgz",
+ "integrity": "sha512-25qOnePUUX7upFc4ycqWersDBq+o1X6hXUTW56JOWCxPYKJXQ1RWzqT9q+2SU3LfPKJf+4sz4Dw3VT0p96Kv6g==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "5.62.0",
+ "@typescript-eslint/parser": "5.62.0",
+ "chalk": "^4.1.2",
+ "eslint": "8.50.0",
+ "eslint-config-prettier": "9.0.0",
+ "eslint-plugin-node": "11.1.0",
+ "eslint-plugin-prettier": "5.0.0",
+ "execa": "^5.0.0",
+ "inquirer": "^7.3.3",
+ "json5": "^2.1.3",
+ "meow": "^9.0.0",
+ "ncp": "^2.0.0",
+ "prettier": "3.0.3",
+ "rimraf": "3.0.2",
+ "write-file-atomic": "^4.0.0"
+ },
+ "bin": {
+ "gts": "build/src/cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "typescript": ">=3"
+ }
+ },
+ "node_modules/gts/node_modules/@eslint/js": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz",
+ "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
+ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.4.0",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/type-utils": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/parser": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
+ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/type-utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
+ "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/types": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
+ "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/utils/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/gts/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/gts/node_modules/eslint": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
+ "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.2",
+ "@eslint/js": "8.50.0",
+ "@humanwhocodes/config-array": "^0.11.11",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/gts/node_modules/eslint-config-prettier": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
+ "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/gts/node_modules/eslint-plugin-prettier": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz",
+ "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==",
+ "dev": true,
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.8.5"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gts/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/gts/node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/gts/node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gts/node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/gts/node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gts/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/gts/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/gts/node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/gts/node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gts/node_modules/prettier": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
+ "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/gts/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/gts/node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/hard-rejection": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
+ "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+ "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+ "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hereby": {
+ "version": "1.8.9",
+ "resolved": "https://registry.npmjs.org/hereby/-/hereby-1.8.9.tgz",
+ "integrity": "sha512-BM/Btsy77GGhuHujCdr2e0jBh3ubrjJcq8M2E/BGQ0O8M9K2uB1PfVSh/LtItAuf9TFe0l8XStaKugMjbYgs4Q==",
+ "dev": true,
+ "dependencies": {
+ "command-line-usage": "^6.1.3",
+ "fastest-levenshtein": "^1.0.16",
+ "import-meta-resolve": "^2.2.2",
+ "minimist": "^1.2.8",
+ "picocolors": "^1.0.0",
+ "pretty-ms": "^8.0.0"
+ },
+ "bin": {
+ "hereby": "bin/hereby.js"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
+ "dev": true
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
+ "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/ignore": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
+ "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/ignore-walk": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz",
+ "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==",
+ "dev": true,
+ "dependencies": {
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-lazy": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
+ "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-meta-resolve": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz",
+ "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ini": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
+ "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/inquirer": {
+ "version": "7.3.3",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
+ "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.19",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.4.0",
+ "rxjs": "^6.6.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
+ "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.2",
+ "hasown": "^2.0.0",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ip": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
+ "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
+ },
+ "node_modules/irregular-plurals": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz",
+ "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
+ "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+ "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-lambda": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+ "dev": true
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
+ "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+ "dev": true,
+ "dependencies": {
+ "which-typed-array": "^1.1.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz",
+ "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==",
+ "dev": true,
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+ "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
+ "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
+ "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
+ "dev": true,
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
+ "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
+ "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
+ "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jju": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
+ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==",
+ "dev": true
+ },
+ "node_modules/jpeg-js": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
+ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/js-yaml/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonc-parser": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
+ "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
+ "dev": true
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+ "dev": true,
+ "engines": [
+ "node >= 0.2.0"
+ ]
+ },
+ "node_modules/just-extend": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
+ "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==",
+ "dev": true
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/license-checker": {
+ "version": "25.0.1",
+ "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz",
+ "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.1",
+ "debug": "^3.1.0",
+ "mkdirp": "^0.5.1",
+ "nopt": "^4.0.1",
+ "read-installed": "~4.0.3",
+ "semver": "^5.5.0",
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-satisfies": "^4.0.0",
+ "treeify": "^1.1.0"
+ },
+ "bin": {
+ "license-checker": "bin/license-checker"
+ }
+ },
+ "node_modules/license-checker/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/license-checker/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/license-checker/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/license-checker/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/license-checker/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/license-checker/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/license-checker/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/license-checker/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/license-checker/node_modules/spdx-satisfies": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz",
+ "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==",
+ "dev": true,
+ "dependencies": {
+ "spdx-compare": "^1.0.0",
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-ranges": "^2.0.0"
+ }
+ },
+ "node_modules/license-checker/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+ },
+ "node_modules/load-json-file": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+ "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^4.0.0",
+ "pify": "^3.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "dev": true
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "dev": true
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.5",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
+ "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz",
+ "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/agent": "^2.0.0",
+ "cacache": "^18.0.0",
+ "http-cache-semantics": "^4.1.1",
+ "is-lambda": "^1.0.1",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^3.0.0",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.3",
+ "promise-retry": "^2.0.1",
+ "ssri": "^10.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/make-synchronized": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/make-synchronized/-/make-synchronized-0.2.7.tgz",
+ "integrity": "sha512-tbTJaNgmKV3E6yYxEN5djObcMt0j1WB2ltn8JteZYczrdFkGMor3KAraPGUf4NJsf5u+FvJbgbGGL35N3J6VVw==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/fisker/make-synchronized?sponsor=1"
+ }
+ },
+ "node_modules/map-obj": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
+ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/meow": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
+ "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/minimist": "^1.2.0",
+ "camelcase-keys": "^6.2.2",
+ "decamelize": "^1.2.0",
+ "decamelize-keys": "^1.1.0",
+ "hard-rejection": "^2.1.0",
+ "minimist-options": "4.1.0",
+ "normalize-package-data": "^3.0.0",
+ "read-pkg-up": "^7.0.1",
+ "redent": "^3.0.0",
+ "trim-newlines": "^3.0.0",
+ "type-fest": "^0.18.0",
+ "yargs-parser": "^20.2.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/meow/node_modules/type-fest": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz",
+ "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minimist-options": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
+ "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
+ "dev": true,
+ "dependencies": {
+ "arrify": "^1.0.1",
+ "is-plain-obj": "^1.1.0",
+ "kind-of": "^6.0.3"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
+ "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
+ "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz",
+ "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^7.0.3",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.1.2"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-json-stream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz",
+ "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==",
+ "dev": true,
+ "dependencies": {
+ "jsonparse": "^1.3.1",
+ "minipass": "^3.0.0"
+ }
+ },
+ "node_modules/minipass-json-stream/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+ },
+ "node_modules/mocha": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
+ "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+ "dependencies": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.5.3",
+ "debug": "4.3.4",
+ "diff": "5.0.0",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.2.0",
+ "he": "1.2.0",
+ "js-yaml": "4.1.0",
+ "log-symbols": "4.1.0",
+ "minimatch": "5.0.1",
+ "ms": "2.1.3",
+ "nanoid": "3.3.3",
+ "serialize-javascript": "6.0.0",
+ "strip-json-comments": "3.1.1",
+ "supports-color": "8.1.1",
+ "workerpool": "6.2.1",
+ "yargs": "16.2.0",
+ "yargs-parser": "20.2.4",
+ "yargs-unparser": "2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mochajs"
+ }
+ },
+ "node_modules/mocha/node_modules/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mocha/node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/mocha/node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
+ "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mocha/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/mocha/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/mocha/node_modules/yargs-parser": {
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+ "dev": true
+ },
+ "node_modules/ncp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
+ "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==",
+ "dev": true,
+ "bin": {
+ "ncp": "bin/ncp"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/netmask": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
+ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "node_modules/nise": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.7.tgz",
+ "integrity": "sha512-wWtNUhkT7k58uvWTB/Gy26eA/EJKtPZFVAhEilN5UYVmmGRYOURbejRUyKm0Uu9XVEW7K5nBOZfR8VMB4QR2RQ==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0",
+ "@sinonjs/fake-timers": "^11.2.2",
+ "@sinonjs/text-encoding": "^0.7.2",
+ "just-extend": "^6.2.0",
+ "path-to-regexp": "^6.2.1"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz",
+ "integrity": "sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==",
+ "dev": true,
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "glob": "^10.3.10",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^13.0.0",
+ "nopt": "^7.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^4.0.0"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/abbrev": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/node-gyp/node_modules/nopt": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",
+ "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "^2.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/which": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
+ "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
+ "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^4.0.1",
+ "is-core-module": "^2.5.0",
+ "semver": "^7.3.4",
+ "validate-npm-package-license": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-bundled": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz",
+ "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==",
+ "dev": true,
+ "dependencies": {
+ "npm-normalize-package-bin": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-bundled/node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz",
+ "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
+ "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
+ "dev": true
+ },
+ "node_modules/npm-package-arg": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz",
+ "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^7.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^5.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg/node_modules/hosted-git-info": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz",
+ "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg/node_modules/lru-cache": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
+ "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
+ "dev": true,
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "node_modules/npm-packlist": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz",
+ "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==",
+ "dev": true,
+ "dependencies": {
+ "ignore-walk": "^6.0.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz",
+ "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==",
+ "dev": true,
+ "dependencies": {
+ "npm-install-checks": "^6.0.0",
+ "npm-normalize-package-bin": "^3.0.0",
+ "npm-package-arg": "^11.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-registry-fetch": {
+ "version": "16.1.0",
+ "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.1.0.tgz",
+ "integrity": "sha512-PQCELXKt8Azvxnt5Y85GseQDJJlglTFM9L9U9gkv2y4e9s0k3GVDdOx3YoB6gm2Do0hlkzC39iCGXby+Wve1Bw==",
+ "dev": true,
+ "dependencies": {
+ "make-fetch-happen": "^13.0.0",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^3.0.0",
+ "minipass-json-stream": "^1.0.1",
+ "minizlib": "^2.1.2",
+ "npm-package-arg": "^11.0.0",
+ "proc-log": "^3.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-run-all": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
+ "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "chalk": "^2.4.1",
+ "cross-spawn": "^6.0.5",
+ "memorystream": "^0.3.1",
+ "minimatch": "^3.0.4",
+ "pidtree": "^0.3.0",
+ "read-pkg": "^3.0.0",
+ "shell-quote": "^1.6.1",
+ "string.prototype.padend": "^3.0.0"
+ },
+ "bin": {
+ "npm-run-all": "bin/npm-run-all/index.js",
+ "run-p": "bin/run-p/index.js",
+ "run-s": "bin/run-s/index.js"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/npm-run-all/node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
+ "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+ "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
+ "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz",
+ "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
+ "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "dev": true,
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "dependencies": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pac-proxy-agent": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz",
+ "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==",
+ "dependencies": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.2",
+ "pac-resolver": "^7.0.0",
+ "socks-proxy-agent": "^8.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-resolver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz",
+ "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==",
+ "dependencies": {
+ "degenerator": "^5.0.0",
+ "ip": "^1.1.8",
+ "netmask": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
+ "dev": true,
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/parse-ms": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz",
+ "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parsel-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.2.tgz",
+ "integrity": "sha512-D66DG2nKx4Yoq66TMEyCUHlR2STGqO7vsBrX7tgyS9cfQyO6XD5JyzOiflwmWN6a4wbUAqpmHqmrxlTQVGZcbA==",
+ "dev": true
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-scurry": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
+ "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+ "dependencies": {
+ "lru-cache": "^9.1.1 || ^10.0.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
+ "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
+ "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==",
+ "dev": true
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
+ "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
+ "dev": true,
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pixelmatch": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
+ "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
+ "dependencies": {
+ "pngjs": "^6.0.0"
+ },
+ "bin": {
+ "pixelmatch": "bin/pixelmatch"
+ }
+ },
+ "node_modules/pixelmatch/node_modules/pngjs": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
+ "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-8.0.0.tgz",
+ "integrity": "sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==",
+ "dev": true,
+ "dependencies": {
+ "find-up-simple": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/plur": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz",
+ "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==",
+ "dev": true,
+ "dependencies": {
+ "irregular-plurals": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+ "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
+ "engines": {
+ "node": ">=14.19.0"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
+ "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/pretty-ms": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz",
+ "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==",
+ "dev": true,
+ "dependencies": {
+ "parse-ms": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/proc-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
+ "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "dev": true
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proper-lockfile": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/proper-lockfile/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/proxy-agent": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz",
+ "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.2",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.0.1",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/puppeteer": {
+ "resolved": "packages/puppeteer",
+ "link": true
+ },
+ "node_modules/puppeteer-core": {
+ "resolved": "packages/puppeteer-core",
+ "link": true
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/queue-tick": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
+ },
+ "node_modules/quick-lru": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
+ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/rambda": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz",
+ "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==",
+ "dev": true
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
+ "dev": true
+ },
+ "node_modules/read-installed": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz",
+ "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==",
+ "dev": true,
+ "dependencies": {
+ "debuglog": "^1.0.1",
+ "read-package-json": "^2.0.0",
+ "readdir-scoped-modules": "^1.0.0",
+ "semver": "2 || 3 || 4 || 5",
+ "slide": "~1.1.3",
+ "util-extend": "^1.0.1"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.2"
+ }
+ },
+ "node_modules/read-installed/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/read-package-json": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz",
+ "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "normalize-package-data": "^2.0.0",
+ "npm-normalize-package-bin": "^1.0.0"
+ }
+ },
+ "node_modules/read-package-json-fast": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz",
+ "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==",
+ "dev": true,
+ "dependencies": {
+ "json-parse-even-better-errors": "^3.0.0",
+ "npm-normalize-package-bin": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz",
+ "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/read-package-json-fast/node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/read-package-json/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/read-package-json/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/read-package-json/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "node_modules/read-package-json/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/read-package-json/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/read-package-json/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+ "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
+ "dev": true,
+ "dependencies": {
+ "load-json-file": "^4.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg-up": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+ "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.1.0",
+ "read-pkg": "^5.2.0",
+ "type-fest": "^0.8.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "node_modules/read-pkg-up/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/read-pkg": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+ "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+ "dev": true,
+ "dependencies": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+ "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "node_modules/read-pkg/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/read-pkg/node_modules/path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "dependencies": {
+ "pify": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdir-scoped-modules": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz",
+ "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==",
+ "deprecated": "This functionality has been moved to @npmcli/fs",
+ "dev": true,
+ "dependencies": {
+ "debuglog": "^1.0.1",
+ "dezalgo": "^1.0.0",
+ "graceful-fs": "^4.1.2",
+ "once": "^1.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/reduce-flatten": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
+ "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
+ "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "set-function-name": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "6.6.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+ "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz",
+ "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "get-intrinsic": "^1.2.2",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
+ "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "get-intrinsic": "^1.2.2",
+ "is-regex": "^1.1.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "devOptional": true
+ },
+ "node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+ "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
+ "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.2",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
+ "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
+ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sigstore": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.2.0.tgz",
+ "integrity": "sha512-fcU9clHwEss2/M/11FFM8Jwc4PjBgbhXoNskoK5guoK0qGQBSeUbQZRJ+B2fDFIvhyf0gqCaPrel9mszbhAxug==",
+ "dev": true,
+ "dependencies": {
+ "@sigstore/bundle": "^2.1.1",
+ "@sigstore/core": "^0.2.0",
+ "@sigstore/protobuf-specs": "^0.2.1",
+ "@sigstore/sign": "^2.2.1",
+ "@sigstore/tuf": "^2.3.0",
+ "@sigstore/verify": "^0.1.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/sinon": {
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
+ "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0",
+ "@sinonjs/fake-timers": "^11.2.2",
+ "@sinonjs/samsam": "^8.0.0",
+ "diff": "^5.1.0",
+ "nise": "^5.1.5",
+ "supports-color": "^7.2.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sinon"
+ }
+ },
+ "node_modules/sinon/node_modules/diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slide": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
+ "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
+ "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
+ "dependencies": {
+ "ip": "^2.0.0",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz",
+ "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "socks": "^2.7.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/socks/node_modules/ip": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
+ "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/spdx-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
+ "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==",
+ "dev": true,
+ "dependencies": {
+ "array-find-index": "^1.0.2",
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-ranges": "^2.0.0"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "dev": true,
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+ "dev": true
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.16",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
+ "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
+ "dev": true
+ },
+ "node_modules/spdx-ranges": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz",
+ "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==",
+ "dev": true
+ },
+ "node_modules/spdx-satisfies": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz",
+ "integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==",
+ "dev": true,
+ "dependencies": {
+ "spdx-compare": "^1.0.0",
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-ranges": "^2.0.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true
+ },
+ "node_modules/ssri": {
+ "version": "10.0.5",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz",
+ "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/streamx": {
+ "version": "2.15.6",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz",
+ "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==",
+ "dependencies": {
+ "fast-fifo": "^1.1.0",
+ "queue-tick": "^1.0.1"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.padend": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz",
+ "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
+ "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
+ "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
+ "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
+ "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/symbol-observable": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
+ "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/synckit": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
+ "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==",
+ "dev": true,
+ "dependencies": {
+ "@pkgr/core": "^0.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/synckit/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "dev": true
+ },
+ "node_modules/table-layout": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz",
+ "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==",
+ "dev": true,
+ "dependencies": {
+ "array-back": "^4.0.1",
+ "deep-extend": "~0.6.0",
+ "typical": "^5.2.0",
+ "wordwrapjs": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
+ "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+ "dev": true,
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
+ "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
+ "dependencies": {
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/tar/node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
+ },
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "node_modules/treeify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz",
+ "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/trim-newlines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
+ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+ "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.13.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/tsd": {
+ "version": "0.30.4",
+ "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.30.4.tgz",
+ "integrity": "sha512-ncC4SwAeUk0OTcXt5h8l0/gOLHJSp9ogosvOADT6QYzrl0ITm398B3wkz8YESqefIsEEwvYAU8bvo7/rcN/M0Q==",
+ "dev": true,
+ "dependencies": {
+ "@tsd/typescript": "~5.3.3",
+ "eslint-formatter-pretty": "^4.1.0",
+ "globby": "^11.0.1",
+ "jest-diff": "^29.0.3",
+ "meow": "^9.0.0",
+ "path-exists": "^4.0.0",
+ "read-pkg-up": "^7.0.0"
+ },
+ "bin": {
+ "tsd": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.0.tgz",
+ "integrity": "sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "~0.19.10",
+ "get-tsconfig": "^4.7.2"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
+ "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+ "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+ "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+ "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
+ "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+ "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+ "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+ "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+ "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+ "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+ "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+ "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+ "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+ "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+ "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+ "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+ "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+ "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+ "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+ "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+ "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+ "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+ "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
+ "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.19.12",
+ "@esbuild/android-arm": "0.19.12",
+ "@esbuild/android-arm64": "0.19.12",
+ "@esbuild/android-x64": "0.19.12",
+ "@esbuild/darwin-arm64": "0.19.12",
+ "@esbuild/darwin-x64": "0.19.12",
+ "@esbuild/freebsd-arm64": "0.19.12",
+ "@esbuild/freebsd-x64": "0.19.12",
+ "@esbuild/linux-arm": "0.19.12",
+ "@esbuild/linux-arm64": "0.19.12",
+ "@esbuild/linux-ia32": "0.19.12",
+ "@esbuild/linux-loong64": "0.19.12",
+ "@esbuild/linux-mips64el": "0.19.12",
+ "@esbuild/linux-ppc64": "0.19.12",
+ "@esbuild/linux-riscv64": "0.19.12",
+ "@esbuild/linux-s390x": "0.19.12",
+ "@esbuild/linux-x64": "0.19.12",
+ "@esbuild/netbsd-x64": "0.19.12",
+ "@esbuild/openbsd-x64": "0.19.12",
+ "@esbuild/sunos-x64": "0.19.12",
+ "@esbuild/win32-arm64": "0.19.12",
+ "@esbuild/win32-ia32": "0.19.12",
+ "@esbuild/win32-x64": "0.19.12"
+ }
+ },
+ "node_modules/tuf-js": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz",
+ "integrity": "sha512-ZSDngmP1z6zw+FIkIBjvOp/II/mIub/O7Pp12j1WNsiCpg5R5wAc//i555bBQsE44O94btLt0xM/Zr2LQjwdCg==",
+ "dev": true,
+ "dependencies": {
+ "@tufjs/models": "2.0.0",
+ "debug": "^4.3.4",
+ "make-fetch-happen": "^13.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
+ "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
+ "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
+ "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+ "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "devOptional": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typical": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
+ "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unbzip2-stream": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
+ "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
+ "dependencies": {
+ "buffer": "^5.2.1",
+ "through": "^2.3.8"
+ }
+ },
+ "node_modules/undici": {
+ "version": "5.28.2",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz",
+ "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==",
+ "dev": true,
+ "dependencies": {
+ "@fastify/busboy": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.25.3",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
+ "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
+ "devOptional": true
+ },
+ "node_modules/unique-filename": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
+ "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==",
+ "dev": true,
+ "dependencies": {
+ "unique-slug": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz",
+ "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/urlpattern-polyfill": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
+ "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/util-extend": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
+ "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==",
+ "dev": true
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",
+ "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz",
+ "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==",
+ "dev": true,
+ "dependencies": {
+ "builtins": "^5.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.11.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
+ "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
+ "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.4",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wireit": {
+ "version": "0.14.4",
+ "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.14.4.tgz",
+ "integrity": "sha512-WNAXEw2cJs1nSRNJNRcPypARZNumgtsRTJFTNpd6turCA6JZ6cEwl4ZU3C1IHc/3IaXoPu9LdxcI5TBTdD6/pg==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "chokidar": "^3.5.3",
+ "fast-glob": "^3.2.11",
+ "jsonc-parser": "^3.0.0",
+ "proper-lockfile": "^4.1.2"
+ },
+ "bin": {
+ "wireit": "bin/wireit.js"
+ },
+ "engines": {
+ "node": ">=14.14.0"
+ }
+ },
+ "node_modules/wordwrapjs": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz",
+ "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==",
+ "dev": true,
+ "dependencies": {
+ "reduce-flatten": "^2.0.0",
+ "typical": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
+ "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw=="
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/write-file-atomic": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
+ "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/write-file-atomic/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/ws": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
+ "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/z-schema": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
+ "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
+ "dev": true,
+ "dependencies": {
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "validator": "^13.7.0"
+ },
+ "bin": {
+ "z-schema": "bin/z-schema"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "commander": "^9.4.1"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
+ "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "packages/browsers": {
+ "name": "@puppeteer/browsers",
+ "version": "1.9.1",
+ "license": "Apache-2.0",
+ "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"
+ },
+ "bin": {
+ "browsers": "lib/cjs/main-cli.js"
+ },
+ "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"
+ },
+ "engines": {
+ "node": ">=16.3.0"
+ }
+ },
+ "packages/browsers/node_modules/cliui": {
+ "version": "8.0.1",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "packages/browsers/node_modules/yargs": {
+ "version": "17.7.2",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "packages/browsers/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "packages/ng-schematics": {
+ "name": "@puppeteer/ng-schematics",
+ "version": "0.5.6",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@angular-devkit/architect": "^0.1701.1",
+ "@angular-devkit/core": "^17.0.7",
+ "@angular-devkit/schematics": "^17.0.7"
+ },
+ "devDependencies": {
+ "@angular/cli": "^17.0.7",
+ "@schematics/angular": "^17.0.7"
+ },
+ "engines": {
+ "node": ">=16.13.2"
+ }
+ },
+ "packages/ng-schematics/node_modules/@angular-devkit/architect": {
+ "version": "0.1700.6",
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "17.0.6",
+ "rxjs": "7.8.1"
+ },
+ "engines": {
+ "node": "^18.13.0 || >=20.9.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/@angular-devkit/core": {
+ "version": "17.0.6",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "8.12.0",
+ "ajv-formats": "2.1.1",
+ "jsonc-parser": "3.2.0",
+ "picomatch": "3.0.1",
+ "rxjs": "7.8.1",
+ "source-map": "0.7.4"
+ },
+ "engines": {
+ "node": "^18.13.0 || >=20.9.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "chokidar": "^3.5.2"
+ },
+ "peerDependenciesMeta": {
+ "chokidar": {
+ "optional": true
+ }
+ }
+ },
+ "packages/ng-schematics/node_modules/@angular-devkit/schematics": {
+ "version": "17.0.6",
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "17.0.6",
+ "jsonc-parser": "3.2.0",
+ "magic-string": "0.30.5",
+ "ora": "5.4.1",
+ "rxjs": "7.8.1"
+ },
+ "engines": {
+ "node": "^18.13.0 || >=20.9.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/@angular/cli": {
+ "version": "17.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/architect": "0.1700.6",
+ "@angular-devkit/core": "17.0.6",
+ "@angular-devkit/schematics": "17.0.6",
+ "@schematics/angular": "17.0.6",
+ "@yarnpkg/lockfile": "1.1.0",
+ "ansi-colors": "4.1.3",
+ "ini": "4.1.1",
+ "inquirer": "9.2.11",
+ "jsonc-parser": "3.2.0",
+ "npm-package-arg": "11.0.1",
+ "npm-pick-manifest": "9.0.0",
+ "open": "8.4.2",
+ "ora": "5.4.1",
+ "pacote": "17.0.4",
+ "resolve": "1.22.8",
+ "semver": "7.5.4",
+ "symbol-observable": "4.0.0",
+ "yargs": "17.7.2"
+ },
+ "bin": {
+ "ng": "bin/ng.js"
+ },
+ "engines": {
+ "node": "^18.13.0 || >=20.9.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/@schematics/angular": {
+ "version": "17.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "17.0.6",
+ "@angular-devkit/schematics": "17.0.6",
+ "jsonc-parser": "3.2.0"
+ },
+ "engines": {
+ "node": "^18.13.0 || >=20.9.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/ajv": {
+ "version": "8.12.0",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "packages/ng-schematics/node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "packages/ng-schematics/node_modules/chalk": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "packages/ng-schematics/node_modules/cli-width": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "packages/ng-schematics/node_modules/cliui": {
+ "version": "8.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "packages/ng-schematics/node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "packages/ng-schematics/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "packages/ng-schematics/node_modules/figures": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^5.0.0",
+ "is-unicode-supported": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "packages/ng-schematics/node_modules/hosted-git-info": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/inquirer": {
+ "version": "9.2.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ljharb/through": "^2.3.9",
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^5.3.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^4.1.0",
+ "external-editor": "^3.1.0",
+ "figures": "^5.0.0",
+ "lodash": "^4.17.21",
+ "mute-stream": "1.0.0",
+ "ora": "^5.4.1",
+ "run-async": "^3.0.0",
+ "rxjs": "^7.8.1",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "packages/ng-schematics/node_modules/json-parse-even-better-errors": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "license": "MIT"
+ },
+ "packages/ng-schematics/node_modules/jsonc-parser": {
+ "version": "3.2.0",
+ "license": "MIT"
+ },
+ "packages/ng-schematics/node_modules/lru-cache": {
+ "version": "10.1.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "packages/ng-schematics/node_modules/mute-stream": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/normalize-package-data": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^7.0.0",
+ "is-core-module": "^2.8.1",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/pacote": {
+ "version": "17.0.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^5.0.0",
+ "@npmcli/installed-package-contents": "^2.0.1",
+ "@npmcli/promise-spawn": "^7.0.0",
+ "@npmcli/run-script": "^7.0.0",
+ "cacache": "^18.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^7.0.2",
+ "npm-package-arg": "^11.0.0",
+ "npm-packlist": "^8.0.0",
+ "npm-pick-manifest": "^9.0.0",
+ "npm-registry-fetch": "^16.0.0",
+ "proc-log": "^3.0.0",
+ "promise-retry": "^2.0.1",
+ "read-package-json": "^7.0.0",
+ "read-package-json-fast": "^3.0.0",
+ "sigstore": "^2.0.0",
+ "ssri": "^10.0.0",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "pacote": "lib/bin.js"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/picomatch": {
+ "version": "3.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "packages/ng-schematics/node_modules/read-package-json": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^10.2.2",
+ "json-parse-even-better-errors": "^3.0.0",
+ "normalize-package-data": "^6.0.0",
+ "npm-normalize-package-bin": "^3.0.0"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/run-async": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/rxjs": {
+ "version": "7.8.1",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "packages/ng-schematics/node_modules/source-map": {
+ "version": "0.7.4",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "packages/ng-schematics/node_modules/tslib": {
+ "version": "2.6.2",
+ "license": "0BSD"
+ },
+ "packages/ng-schematics/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "packages/ng-schematics/node_modules/yargs": {
+ "version": "17.7.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "packages/ng-schematics/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "packages/puppeteer": {
+ "version": "21.10.0",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "1.9.1",
+ "cosmiconfig": "9.0.0",
+ "puppeteer-core": "21.10.0"
+ },
+ "bin": {
+ "puppeteer": "lib/esm/puppeteer/node/cli.js"
+ },
+ "devDependencies": {
+ "@types/node": "18.17.15"
+ },
+ "engines": {
+ "node": ">=16.13.2"
+ }
+ },
+ "packages/puppeteer-core": {
+ "version": "21.10.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "1.9.1",
+ "chromium-bidi": "0.5.6",
+ "cross-fetch": "4.0.0",
+ "debug": "4.3.4",
+ "devtools-protocol": "0.0.1232444",
+ "ws": "8.16.0"
+ },
+ "devDependencies": {
+ "@types/debug": "4.1.12",
+ "@types/node": "18.17.15",
+ "@types/ws": "8.5.10",
+ "mitt": "3.0.1",
+ "parsel-js": "1.1.2",
+ "rxjs": "7.8.1"
+ },
+ "engines": {
+ "node": ">=16.13.2"
+ }
+ },
+ "packages/puppeteer-core/node_modules/@types/node": {
+ "version": "18.17.15",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/puppeteer-core/node_modules/rxjs": {
+ "version": "7.8.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "packages/puppeteer-core/node_modules/tslib": {
+ "version": "2.6.2",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "packages/puppeteer/node_modules/@types/node": {
+ "version": "18.17.15",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/puppeteer/node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "packages/puppeteer/node_modules/parse-json": {
+ "version": "5.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "packages/testserver": {
+ "name": "@pptr/testserver",
+ "version": "0.6.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mime": "3.0.0",
+ "ws": "8.16.0"
+ },
+ "devDependencies": {
+ "@types/mime": "3.0.4"
+ }
+ },
+ "test": {
+ "name": "@puppeteer-test/test",
+ "version": "latest",
+ "dependencies": {
+ "diff": "5.1.0",
+ "jpeg-js": "0.4.4",
+ "pixelmatch": "5.3.0",
+ "pngjs": "7.0.0"
+ },
+ "devDependencies": {
+ "@types/diff": "5.0.9",
+ "@types/pixelmatch": "5.2.6",
+ "@types/pngjs": "6.0.4"
+ }
+ },
+ "test/installation": {
+ "name": "@puppeteer-test/installation",
+ "version": "latest",
+ "dependencies": {
+ "glob": "10.3.10",
+ "mocha": "10.2.0"
+ }
+ },
+ "test/node_modules/diff": {
+ "version": "5.1.0",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "tools/docgen": {
+ "name": "@puppeteer/docgen",
+ "version": "0.1.0",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@microsoft/api-documenter": "7.23.20",
+ "@microsoft/api-extractor": "7.39.4",
+ "@microsoft/api-extractor-model": "7.28.7",
+ "@microsoft/tsdoc": "0.14.2",
+ "@rushstack/node-core-library": "3.64.2"
+ }
+ },
+ "tools/doctest": {
+ "name": "@puppeteer/doctest",
+ "version": "0.1.0",
+ "license": "Apache-2.0",
+ "bin": {
+ "doctest": "bin/doctest.js"
+ },
+ "devDependencies": {
+ "@swc/core": "1.3.107",
+ "@types/doctrine": "0.0.9",
+ "@types/source-map-support": "0.5.10",
+ "@types/yargs": "17.0.32",
+ "acorn": "8.11.3",
+ "doctrine": "3.0.0",
+ "glob": "10.3.10",
+ "pkg-dir": "8.0.0",
+ "source-map": "0.7.4",
+ "source-map-support": "0.5.21",
+ "yargs": "17.7.2"
+ }
+ },
+ "tools/doctest/node_modules/cliui": {
+ "version": "8.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "tools/doctest/node_modules/source-map": {
+ "version": "0.7.4",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "tools/doctest/node_modules/yargs": {
+ "version": "17.7.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "tools/doctest/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "tools/eslint": {
+ "name": "@puppeteer/eslint",
+ "version": "0.1.0",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@prettier/sync": "0.5.0"
+ }
+ },
+ "tools/mocha-runner": {
+ "name": "@puppeteer/mocha-runner",
+ "version": "0.1.0",
+ "license": "Apache-2.0",
+ "bin": {
+ "mocha-runner": "bin/mocha-runner.js"
+ },
+ "devDependencies": {
+ "@types/yargs": "17.0.32",
+ "c8": "9.1.0",
+ "glob": "10.3.10",
+ "yargs": "17.7.2",
+ "zod": "3.22.4"
+ }
+ },
+ "tools/mocha-runner/node_modules/cliui": {
+ "version": "8.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "tools/mocha-runner/node_modules/yargs": {
+ "version": "17.7.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "tools/mocha-runner/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/remote/test/puppeteer/package.json b/remote/test/puppeteer/package.json
new file mode 100644
index 0000000000..612f5ac369
--- /dev/null
+++ b/remote/test/puppeteer/package.json
@@ -0,0 +1,187 @@
+{
+ "name": "puppeteer-repo",
+ "private": true,
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/puppeteer/puppeteer"
+ },
+ "scripts": {
+ "build": "wireit",
+ "build:tools": "wireit",
+ "check": "npm run check --workspaces --if-present && run-p check:*",
+ "check:pinned-deps": "tsx tools/ensure-pinned-deps",
+ "clean": "npm run clean --workspaces --if-present",
+ "debug": "mocha --inspect-brk",
+ "docs": "wireit",
+ "doctest": "wireit",
+ "format": "run-s format:*",
+ "format:eslint": "eslint --ext js --ext mjs --ext ts --fix .",
+ "format:expectations": "node tools/sort-test-expectations.mjs",
+ "format:prettier": "prettier --write .",
+ "lint": "run-s lint:*",
+ "lint:eslint": "eslint --ext js --ext mjs --ext ts .",
+ "lint:prettier": "prettier --check .",
+ "lint:expectations": "node tools/sort-test-expectations.mjs --lint",
+ "postinstall": "npm run postinstall --workspaces --if-present",
+ "prepare": "npm run prepare --workspaces --if-present",
+ "test": "wireit",
+ "test-install": "npm run test --workspace @puppeteer-test/installation",
+ "test-types": "wireit",
+ "test:chrome": "wireit",
+ "test:chrome:bidi": "wireit",
+ "test:chrome:bidi-local": "wireit",
+ "test:chrome:headful": "wireit",
+ "test:chrome:headless": "wireit",
+ "test:chrome:new-headless": "wireit",
+ "test:firefox": "wireit",
+ "test:firefox:bidi": "wireit",
+ "test:firefox:bidi:headful": "wireit",
+ "test:firefox:headful": "wireit",
+ "test:firefox:headless": "wireit",
+ "validate-licenses": "tsx tools/third_party/validate-licenses.ts",
+ "unit": "npm run unit --workspaces --if-present"
+ },
+ "wireit": {
+ "build": {
+ "dependencies": [
+ "./packages/browsers:build",
+ "./packages/ng-schematics:build",
+ "./packages/puppeteer-core:build",
+ "./packages/puppeteer:build",
+ "./packages/testserver:build",
+ "./test:build",
+ "./test/installation:build"
+ ]
+ },
+ "build:tools": {
+ "dependencies": [
+ "./tools/docgen:build",
+ "./tools/doctest:build",
+ "./tools/mocha-runner:build",
+ "./tools/eslint:build",
+ "./packages/testserver:build"
+ ]
+ },
+ "docs": {
+ "command": "hereby docs",
+ "dependencies": [
+ "./packages/browsers:build:docs",
+ "./packages/puppeteer:build:docs",
+ "./packages/puppeteer-core:build:docs",
+ "./tools/docgen:build"
+ ]
+ },
+ "doctest": {
+ "command": "npx ./tools/doctest 'packages/puppeteer-core/lib/esm/**/*.js'",
+ "dependencies": [
+ "./packages/puppeteer-core:build",
+ "./tools/doctest:build"
+ ]
+ },
+ "test:chrome": {
+ "dependencies": [
+ "test:chrome:bidi",
+ "test:chrome:headful",
+ "test:chrome:headless",
+ "test:chrome:new-headless"
+ ]
+ },
+ "test:chrome:bidi": {
+ "command": "npm test -- --test-suite chrome-bidi"
+ },
+ "test:chrome:bidi-local": {
+ "command": "PUPPETEER_EXECUTABLE_PATH=$(node tools/download_chrome_bidi.mjs ~/.cache/puppeteer/chrome-canary --shell) npm test -- --test-suite chrome-bidi"
+ },
+ "test:chrome:headful": {
+ "command": "npm test -- --test-suite chrome-headful"
+ },
+ "test:chrome:headless": {
+ "command": "npm test -- --test-suite chrome-headless"
+ },
+ "test:chrome:new-headless": {
+ "command": "npm test -- --test-suite chrome-new-headless"
+ },
+ "test:firefox:bidi": {
+ "command": "npm test -- --test-suite firefox-bidi"
+ },
+ "test:firefox:bidi:headful": {
+ "command": "npm test -- --test-suite firefox-bidi-headful"
+ },
+ "test:firefox:headful": {
+ "command": "npm test -- --test-suite firefox-headful"
+ },
+ "test:firefox:headless": {
+ "command": "npm test -- --test-suite firefox-headless"
+ },
+ "test:firefox": {
+ "dependencies": [
+ "test:firefox:bidi",
+ "test:firefox:bidi:headful",
+ "test:firefox:headful",
+ "test:firefox:headless"
+ ]
+ },
+ "test": {
+ "command": "npx ./tools/mocha-runner --min-tests 1003",
+ "dependencies": [
+ "./test:build",
+ "./tools/mocha-runner:build"
+ ]
+ },
+ "test-types": {
+ "command": "tsd -t packages/puppeteer",
+ "dependencies": [
+ "./packages/puppeteer:build"
+ ]
+ }
+ },
+ "devDependencies": {
+ "@actions/core": "1.10.1",
+ "@types/mocha": "10.0.6",
+ "@types/node": "20.8.4",
+ "@types/semver": "7.5.6",
+ "@types/sinon": "17.0.3",
+ "@typescript-eslint/eslint-plugin": "6.19.1",
+ "@typescript-eslint/parser": "6.19.1",
+ "esbuild": "0.20.0",
+ "eslint-config-prettier": "9.1.0",
+ "eslint-import-resolver-typescript": "3.6.1",
+ "eslint-plugin-import": "2.29.1",
+ "eslint-plugin-mocha": "10.2.0",
+ "eslint-plugin-prettier": "5.1.3",
+ "eslint-plugin-rulesdir": "0.2.2",
+ "eslint-plugin-tsdoc": "0.2.17",
+ "eslint-plugin-unused-imports": "3.0.0",
+ "eslint": "8.56.0",
+ "execa": "8.0.1",
+ "expect": "29.7.0",
+ "gts": "5.2.0",
+ "hereby": "1.8.9",
+ "license-checker": "25.0.1",
+ "mocha": "10.2.0",
+ "npm-run-all": "4.1.5",
+ "prettier": "3.2.4",
+ "semver": "7.5.4",
+ "sinon": "17.0.1",
+ "source-map-support": "0.5.21",
+ "spdx-satisfies": "5.0.1",
+ "tsd": "0.30.4",
+ "tsx": "4.7.0",
+ "typescript": "5.3.3",
+ "wireit": "0.14.4"
+ },
+ "overrides": {
+ "@microsoft/api-extractor": {
+ "typescript": "$typescript"
+ }
+ },
+ "workspaces": [
+ "packages/*",
+ "test",
+ "test/installation",
+ "tools/eslint",
+ "tools/doctest",
+ "tools/docgen",
+ "tools/mocha-runner"
+ ]
+}
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
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/.eslintignore b/remote/test/puppeteer/packages/ng-schematics/.eslintignore
new file mode 100644
index 0000000000..8424d7004d
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/.eslintignore
@@ -0,0 +1,5 @@
+# Ignore File that will be copied to Angular
+/files/
+
+# Ignore sandbox enviroment
+./sandbox/
diff --git a/remote/test/puppeteer/packages/ng-schematics/.gitignore b/remote/test/puppeteer/packages/ng-schematics/.gitignore
new file mode 100644
index 0000000000..9dad45cdd5
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/.gitignore
@@ -0,0 +1,3 @@
+
+# Sandbox
+sandbox/
diff --git a/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs b/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs
new file mode 100644
index 0000000000..be9bc29919
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ logLevel: 'debug',
+ spec: 'test/build/**/*.spec.js',
+ exit: !!process.env.CI,
+ reporter: process.env.CI ? 'spec' : 'dot',
+};
diff --git a/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md b/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md
new file mode 100644
index 0000000000..a483c4f2fb
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md
@@ -0,0 +1,110 @@
+# Changelog
+
+## [0.5.6](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.5...ng-schematics-v0.5.6) (2024-01-16)
+
+
+### Bug Fixes
+
+* jest config issue on Windows ([3711f86](https://github.com/puppeteer/puppeteer/commit/3711f86dca4140da9e830bd7a46f4eca43cd5f4b))
+
+## [0.5.5](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.4...ng-schematics-v0.5.5) (2023-12-19)
+
+
+### Bug Fixes
+
+* update documentation for ng-schematics ([#11533](https://github.com/puppeteer/puppeteer/issues/11533)) ([744e894](https://github.com/puppeteer/puppeteer/commit/744e8944ac62b9d7284fa260c5c796fa1b83b5ef))
+
+## [0.5.4](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.3...ng-schematics-v0.5.4) (2023-12-06)
+
+
+### Bug Fixes
+
+* get port from created server ([#11495](https://github.com/puppeteer/puppeteer/issues/11495)) ([d2f4b9c](https://github.com/puppeteer/puppeteer/commit/d2f4b9ca53642ac9ccae9a22fd3138698990387b))
+
+## [0.5.3](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.2...ng-schematics-v0.5.3) (2023-12-04)
+
+
+### Bug Fixes
+
+* ng-schematics install Windows ([#11487](https://github.com/puppeteer/puppeteer/issues/11487)) ([02af748](https://github.com/puppeteer/puppeteer/commit/02af7482d9bf2163b90dfe623b0af18c513d5a3b))
+
+## [0.5.2](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.1...ng-schematics-v0.5.2) (2023-11-16)
+
+
+### Bug Fixes
+
+* run post-install hooks ([#11403](https://github.com/puppeteer/puppeteer/issues/11403)) ([3f6ca24](https://github.com/puppeteer/puppeteer/commit/3f6ca249ed898eee25015a6fd0ce7cf774ad31b2))
+
+## [0.5.1](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.0...ng-schematics-v0.5.1) (2023-11-13)
+
+
+### Bug Fixes
+
+* multi-app project extend root `tsconfig.json` ([#11374](https://github.com/puppeteer/puppeteer/issues/11374)) ([1b2d920](https://github.com/puppeteer/puppeteer/commit/1b2d920fe638f3aad704ab8f21d1e4f4099b6d44))
+* support Angular 17 new template ([#11375](https://github.com/puppeteer/puppeteer/issues/11375)) ([64f7bf0](https://github.com/puppeteer/puppeteer/commit/64f7bf0af442369a07352b11555ec3f612eb62b8))
+
+## [0.5.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.4.0...ng-schematics-v0.5.0) (2023-08-22)
+
+
+### Features
+
+* **ng-schematics:** reduce the user options and better defaults ([35dc2d8](https://github.com/puppeteer/puppeteer/commit/35dc2d884052b27a3f9c70b8646f95743be7b84d))
+* **ng-schematics:** release version 0.5.0 ([#10768](https://github.com/puppeteer/puppeteer/issues/10768)) ([42fdd0a](https://github.com/puppeteer/puppeteer/commit/42fdd0a733acb2a9af3878bfa8927252f68ed465))
+
+
+### Bug Fixes
+
+* **ng-schematics:** builder is responsible for resolving commands ([683e181](https://github.com/puppeteer/puppeteer/commit/683e18189c0aedad7deb9007055a1a38801bbf08))
+* **ng-schematics:** don't install for library projects ([1376b77](https://github.com/puppeteer/puppeteer/commit/1376b77a7ab2260c2fd236c3cf31abbd544193e8))
+
+## [0.4.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.4.0) (2023-08-08)
+
+
+### Features
+
+* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
+
+## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.3.0) (2023-08-03)
+
+
+### Features
+
+* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
+
+## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.3.0) (2023-08-02)
+
+
+### Features
+
+* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
+
+## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.2.0...ng-schematics-v0.3.0) (2023-06-29)
+
+
+### Features
+
+* add Test command ([#10443](https://github.com/puppeteer/puppeteer/issues/10443)) ([2d8993b](https://github.com/puppeteer/puppeteer/commit/2d8993b45b0a0c5943907fe69f865e1064a23d3c))
+
+
+### Bug Fixes
+
+* `port` option to run dev and e2e side-by-side ([#10458](https://github.com/puppeteer/puppeteer/issues/10458)) ([a43b346](https://github.com/puppeteer/puppeteer/commit/a43b346bfc7f0071fcead1abb7d7b46dcf3c27f9))
+* use Node test reporter ([#10464](https://github.com/puppeteer/puppeteer/issues/10464)) ([f778b1e](https://github.com/puppeteer/puppeteer/commit/f778b1e2a70f3d507ab2012d2918f5ed241a8d21))
+
+## [0.2.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.1.0...ng-schematics-v0.2.0) (2023-05-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019))
+
+### Features
+
+* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) ([7405d65](https://github.com/puppeteer/puppeteer/commit/7405d6585aa09b240fbab09aa360674d4442b3d9))
+
+## 0.1.0 (2022-11-23)
+
+
+### Features
+
+* **ng-schematics:** Release @puppeteer/ng-schematics ([#9244](https://github.com/puppeteer/puppeteer/issues/9244)) ([be33929](https://github.com/puppeteer/puppeteer/commit/be33929770e473992ad49029e6d038d36591e108))
diff --git a/remote/test/puppeteer/packages/ng-schematics/README.md b/remote/test/puppeteer/packages/ng-schematics/README.md
new file mode 100644
index 0000000000..975f74a704
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/README.md
@@ -0,0 +1,230 @@
+# Puppeteer Angular Schematic
+
+Adds Puppeteer-based e2e tests to your Angular project.
+
+## Getting started
+
+Run the command below in an Angular CLI app directory and follow the prompts.
+
+> Note this will add the schematic as a dependency to your project.
+
+```bash
+ng add @puppeteer/ng-schematics
+```
+
+Or you can use the same command followed by the [options](#options) below.
+
+Currently, this schematic supports the following test runners:
+
+- [**Jasmine**](https://jasmine.github.io/)
+- [**Jest**](https://jestjs.io/)
+- [**Mocha**](https://mochajs.org/)
+- [**Node Test Runner**](https://nodejs.org/api/test.html)
+
+With the schematics installed you can run E2E tests:
+
+```bash
+ng e2e
+```
+
+### Options
+
+When adding schematics to your project you can to provide following options:
+
+| Option | Description | Value | Required |
+| --------------- | ------------------------------------------------------ | ------------------------------------------ | -------- |
+| `--test-runner` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` |
+
+## Creating a single test file
+
+Puppeteer Angular Schematic exposes a method to create a single test file.
+
+```bash
+ng generate @puppeteer/ng-schematics:e2e "<TestName>"
+```
+
+### Running test server and dev server at the same time
+
+By default the E2E test will run the app on the same port as `ng start`.
+To avoid this you can specify the port the an the `angular.json`
+Update either `e2e` or `puppeteer` (depending on the initial setup) to:
+
+```json
+{
+ "e2e": {
+ "builder": "@puppeteer/ng-schematics:puppeteer",
+ "options": {
+ "commands": [...],
+ "devServerTarget": "sandbox:serve",
+ "testRunner": "<TestRunner>",
+ "port": 8080
+ },
+ ...
+}
+```
+
+Now update the E2E test file `utils.ts` baseUrl to:
+
+```ts
+const baseUrl = 'http://localhost:8080';
+```
+
+## Contributing
+
+Check out our [contributing guide](https://pptr.dev/contributing) to get an overview of what you need to develop in the Puppeteer repo.
+
+### Sandbox smoke tests
+
+To make integration easier smoke test can be run with a single command, that will create a fresh install of Angular (single application and a milti application projects). Then it will install the schematics inside them and run the initial e2e tests:
+
+```bash
+node tools/smoke.mjs
+```
+
+### Unit Testing
+
+The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit:
+
+```bash
+npm run test
+```
+
+## Migrating from Protractor
+
+### Entry point
+
+Puppeteer has its own [`browser`](https://pptr.dev/api/puppeteer.browser) that exposes the browser process.
+A more closes comparison for Protractor's `browser` would be Puppeteer's [`page`](https://pptr.dev/api/puppeteer.page).
+
+```ts
+// Testing framework specific imports
+
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('<Test Name>', function () {
+ setupBrowserHooks();
+ it('is running', async function () {
+ const {page} = getBrowserState();
+ // Query elements
+ await page
+ .locator('my-component')
+ // Click on the element once found
+ .click();
+ });
+});
+```
+
+### Getting element properties
+
+You can easily get any property of the element.
+
+```ts
+// Testing framework specific imports
+
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('<Test Name>', function () {
+ setupBrowserHooks();
+ it('is running', async function () {
+ const {page} = getBrowserState();
+ // Query elements
+ const elementText = await page
+ .locator('.my-component')
+ .map(button => button.innerText)
+ // Wait for element to show up
+ .wait();
+
+ // Assert via assertion library
+ });
+});
+```
+
+### Query Selectors
+
+Puppeteer supports multiple types of selectors, namely, the CSS, ARIA, text, XPath and pierce selectors.
+The following table shows Puppeteer's equivalents to [Protractor By](https://www.protractortest.org/#/api?view=ProtractorBy).
+
+> For improved reliability and reduced flakiness try our
+> **Experimental** [Locators API](https://pptr.dev/guides/locators)
+
+| By | Protractor code | Puppeteer querySelector |
+| ----------------- | --------------------------------------------- | ------------------------------------------------------------ |
+| CSS (Single) | `$(by.css('<CSS>'))` | `page.$('<CSS>')` |
+| CSS (Multiple) | `$$(by.css('<CSS>'))` | `page.$$('<CSS>')` |
+| Id | `$(by.id('<ID>'))` | `page.$('#<ID>')` |
+| CssContainingText | `$(by.cssContainingText('<CSS>', '<TEXT>'))` | `page.$('<CSS> ::-p-text(<TEXT>)')` ` |
+| DeepCss | `$(by.deepCss('<CSS>'))` | `page.$(':scope >>> <CSS>')` |
+| XPath | `$(by.xpath('<XPATH>'))` | `page.$('::-p-xpath(<XPATH>)')` |
+| JS | `$(by.js('document.querySelector("<CSS>")'))` | `page.evaluateHandle(() => document.querySelector('<CSS>'))` |
+
+> For advanced use cases such as Protractor's `by.addLocator` you can check Puppeteer's [Custom selectors](https://pptr.dev/guides/query-selectors#custom-selectors).
+
+### Actions Selectors
+
+Puppeteer allows you to all necessary actions to allow test your application.
+
+```ts
+// Click on the element.
+element(locator).click();
+// Puppeteer equivalent
+await page.locator(locator).click();
+
+// Send keys to the element (usually an input).
+element(locator).sendKeys('my text');
+// Puppeteer equivalent
+await page.locator(locator).fill('my text');
+
+// Clear the text in an element (usually an input).
+element(locator).clear();
+// Puppeteer equivalent
+await page.locator(locator).fill('');
+
+// Get the value of an attribute, for example, get the value of an input.
+element(locator).getAttribute('value');
+// Puppeteer equivalent
+const element = await page.locator(locator).waitHandle();
+const value = await element.getProperty('value');
+```
+
+### Example
+
+Sample Protractor test:
+
+```ts
+describe('Protractor Demo', function () {
+ it('should add one and two', function () {
+ browser.get('http://juliemr.github.io/protractor-demo/');
+ element(by.model('first')).sendKeys(1);
+ element(by.model('second')).sendKeys(2);
+
+ element(by.id('gobutton')).click();
+
+ expect(element(by.binding('latest')).getText()).toEqual('3');
+ });
+});
+```
+
+Sample Puppeteer migration:
+
+```ts
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('Puppeteer Demo', function () {
+ setupBrowserHooks();
+ it('should add one and two', function () {
+ const {page} = getBrowserState();
+ await page.goto('http://juliemr.github.io/protractor-demo/');
+
+ await page.locator('.form-inline > input:nth-child(1)').fill('1');
+ await page.locator('.form-inline > input:nth-child(2)').fill('2');
+ await page.locator('#gobutton').fill('2');
+
+ const result = await page
+ .locator('.table tbody td:last-of-type')
+ .map(header => header.innerText)
+ .wait();
+
+ expect(result).toEqual('3');
+ });
+});
+```
diff --git a/remote/test/puppeteer/packages/ng-schematics/package.json b/remote/test/puppeteer/packages/ng-schematics/package.json
new file mode 100644
index 0000000000..29db1dcdc9
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "@puppeteer/ng-schematics",
+ "version": "0.5.6",
+ "description": "Puppeteer Angular schematics",
+ "scripts": {
+ "build": "wireit",
+ "clean": "../../tools/clean.js",
+ "dev:test": "npm run test --watch",
+ "dev": "npm run build --watch",
+ "unit": "wireit"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b && node tools/copySchemaFiles.mjs",
+ "clean": "if-file-deleted",
+ "files": [
+ "tsconfig.json",
+ "tsconfig.test.json",
+ "src/**",
+ "test/src/**"
+ ],
+ "output": [
+ "lib/**",
+ "test/build/**",
+ "*.tsbuildinfo"
+ ]
+ },
+ "build:test": {
+ "command": "tsc -b test/tsconfig.json"
+ },
+ "unit": {
+ "command": "node --test --test-reporter spec test/build",
+ "dependencies": [
+ "build",
+ "build:test"
+ ]
+ }
+ },
+ "keywords": [
+ "angular",
+ "puppeteer",
+ "schematics"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/ng-schematics"
+ },
+ "author": "The Chromium Authors",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.13.2"
+ },
+ "dependencies": {
+ "@angular-devkit/architect": "^0.1701.1",
+ "@angular-devkit/core": "^17.0.7",
+ "@angular-devkit/schematics": "^17.0.7"
+ },
+ "devDependencies": {
+ "@schematics/angular": "^17.0.7",
+ "@angular/cli": "^17.0.7"
+ },
+ "files": [
+ "lib",
+ "!*.tsbuildinfo"
+ ],
+ "ng-add": {
+ "save": "devDependencies"
+ },
+ "schematics": "./lib/schematics/collection.json",
+ "builders": "./lib/builders/builders.json"
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json b/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json
new file mode 100644
index 0000000000..41079f7731
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "../../../../node_modules/@angular-devkit/architect/src/builders-schema.json",
+ "builders": {
+ "puppeteer": {
+ "implementation": "./puppeteer",
+ "schema": "./puppeteer/schema.json",
+ "description": "Run e2e test with Puppeteer"
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts
new file mode 100644
index 0000000000..82a1e8e7da
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts
@@ -0,0 +1,200 @@
+import {spawn} from 'child_process';
+import {normalize, join} from 'path';
+
+import {
+ createBuilder,
+ type BuilderContext,
+ type BuilderOutput,
+ targetFromTargetString,
+ type BuilderRun,
+} from '@angular-devkit/architect';
+import type {JsonObject} from '@angular-devkit/core';
+
+import {TestRunner} from '../../schematics/utils/types.js';
+
+import type {PuppeteerBuilderOptions} from './types.js';
+
+const terminalStyles = {
+ cyan: '\u001b[36;1m',
+ green: '\u001b[32m',
+ red: '\u001b[31m',
+ bold: '\u001b[1m',
+ reverse: '\u001b[7m',
+ clear: '\u001b[0m',
+};
+
+export function getCommandForRunner(runner: TestRunner): [string, ...string[]] {
+ switch (runner) {
+ case TestRunner.Jasmine:
+ return [`jasmine`, '--config=./e2e/jasmine.json'];
+ case TestRunner.Jest:
+ return [`jest`, '-c', 'e2e/jest.config.js'];
+ case TestRunner.Mocha:
+ return [`mocha`, '--config=./e2e/.mocharc.js'];
+ case TestRunner.Node:
+ return ['node', '--test', '--test-reporter', 'spec', 'e2e/build/'];
+ }
+
+ throw new Error(`Unknown test runner ${runner}!`);
+}
+
+function getExecutable(command: string[]) {
+ const executable = command.shift()!;
+ const debugError = `Error running '${executable}' with arguments '${command.join(
+ ' '
+ )}'.`;
+
+ return {
+ executable,
+ args: command,
+ debugError,
+ error: 'Please look at the output above to determine the issue!',
+ };
+}
+
+function updateExecutablePath(command: string, root?: string) {
+ if (command === TestRunner.Node) {
+ return command;
+ }
+
+ let path = 'node_modules/.bin/';
+ if (root && root !== '') {
+ const nested = root
+ .split('/')
+ .map(() => {
+ return '../';
+ })
+ .join('');
+ path = `${nested}${path}${command}`;
+ } else {
+ path = `./${path}${command}`;
+ }
+
+ return normalize(path);
+}
+
+async function executeCommand(
+ context: BuilderContext,
+ command: string[],
+ env: NodeJS.ProcessEnv = {}
+) {
+ let project: JsonObject;
+ if (context.target) {
+ project = await context.getProjectMetadata(context.target.project);
+ command[0] = updateExecutablePath(command[0]!, String(project['root']));
+ }
+
+ await new Promise(async (resolve, reject) => {
+ context.logger.debug(`Trying to execute command - ${command.join(' ')}.`);
+ const {executable, args, debugError, error} = getExecutable(command);
+ let path = context.workspaceRoot;
+ if (context.target) {
+ path = join(path, (project['root'] as string | undefined) ?? '');
+ }
+
+ const child = spawn(executable, args, {
+ cwd: path,
+ stdio: 'inherit',
+ shell: true,
+ env: {
+ ...process.env,
+ ...env,
+ },
+ });
+
+ child.on('error', message => {
+ context.logger.debug(debugError);
+ console.log(message);
+ reject(error);
+ });
+
+ child.on('exit', code => {
+ if (code === 0) {
+ resolve(true);
+ } else {
+ reject(error);
+ }
+ });
+ });
+}
+
+function message(
+ message: string,
+ context: BuilderContext,
+ type: 'info' | 'success' | 'error' = 'info'
+): void {
+ let style: string;
+ switch (type) {
+ case 'info':
+ style = terminalStyles.reverse + terminalStyles.cyan;
+ break;
+ case 'success':
+ style = terminalStyles.reverse + terminalStyles.green;
+ break;
+ case 'error':
+ style = terminalStyles.red;
+ break;
+ }
+ context.logger.info(
+ `${terminalStyles.bold}${style}${message}${terminalStyles.clear}`
+ );
+}
+
+async function startServer(
+ options: PuppeteerBuilderOptions,
+ context: BuilderContext
+): Promise<BuilderRun> {
+ context.logger.debug('Trying to start server.');
+ const target = targetFromTargetString(options.devServerTarget);
+ const defaultServerOptions = await context.getTargetOptions(target);
+
+ const overrides = {
+ watch: false,
+ host: defaultServerOptions['host'],
+ port: options.port ?? defaultServerOptions['port'],
+ } as JsonObject;
+
+ message(' Spawning test server ⚙️ ... \n', context);
+ const server = await context.scheduleTarget(target, overrides);
+ const result = await server.result;
+ if (!result.success) {
+ throw new Error('Failed to spawn server! Stopping tests...');
+ }
+
+ return server;
+}
+
+async function executeE2ETest(
+ options: PuppeteerBuilderOptions,
+ context: BuilderContext
+): Promise<BuilderOutput> {
+ let server: BuilderRun | null = null;
+ try {
+ message('\n Building tests 🛠️ ... \n', context);
+ await executeCommand(context, [`tsc`, '-p', 'e2e/tsconfig.json']);
+
+ server = await startServer(options, context);
+ const result = await server.result;
+
+ message('\n Running tests 🧪 ... \n', context);
+ const testRunnerCommand = getCommandForRunner(options.testRunner);
+ await executeCommand(context, testRunnerCommand, {
+ baseUrl: result['baseUrl'],
+ });
+
+ message('\n 🚀 Test ran successfully! 🚀 ', context, 'success');
+ return {success: true};
+ } catch (error) {
+ message('\n 🛑 Test failed! 🛑 ', context, 'error');
+ if (error instanceof Error) {
+ return {success: false, error: error.message};
+ }
+ return {success: false, error: error as string};
+ } finally {
+ if (server) {
+ await server.stop();
+ }
+ }
+}
+
+export default createBuilder<PuppeteerBuilderOptions>(executeE2ETest);
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json
new file mode 100644
index 0000000000..2693d19cce
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json
@@ -0,0 +1,26 @@
+{
+ "title": "Puppeteer",
+ "description": "Options for Puppeteer Angular Schematics",
+ "type": "object",
+ "properties": {
+ "commands": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "item": {
+ "type": "string"
+ }
+ },
+ "description": "Commands to execute in the repo. Commands prefixed with `./node_modules/bin` (Exception: 'node')."
+ },
+ "devServerTarget": {
+ "type": "string",
+ "description": "Angular target that spawns the server."
+ },
+ "port": {
+ "type": ["number", "null"],
+ "description": "Port to run the test server on."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts
new file mode 100644
index 0000000000..6258a955c0
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JsonObject} from '@angular-devkit/core';
+
+import type {TestRunner} from '../../schematics/utils/types.js';
+
+export interface PuppeteerBuilderOptions extends JsonObject {
+ testRunner: TestRunner;
+ devServerTarget: string;
+ port: number | null;
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json
new file mode 100644
index 0000000000..00bede45e5
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json",
+ "schematics": {
+ "ng-add": {
+ "description": "Add Puppeteer to an Angular project",
+ "factory": "./ng-add/index#ngAdd",
+ "schema": "./ng-add/schema.json"
+ },
+ "e2e": {
+ "description": "Create a single test file",
+ "factory": "./e2e/index#e2e",
+ "schema": "./e2e/schema.json"
+ },
+ "config": {
+ "description": "Eject Puppeteer config file",
+ "factory": "./config/index#config",
+ "schema": "./config/schema.json"
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/files/.puppeteerrc.mjs b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/files/.puppeteerrc.mjs
new file mode 100644
index 0000000000..0da14a80d8
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/files/.puppeteerrc.mjs
@@ -0,0 +1,4 @@
+/**
+ * @type {import("puppeteer").Configuration}
+ */
+export {};
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/index.ts
new file mode 100644
index 0000000000..b01d98e33e
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/index.ts
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ chain,
+ type Rule,
+ type SchematicContext,
+ type Tree,
+} from '@angular-devkit/schematics';
+
+import {addFilesSingle} from '../utils/files.js';
+import {TestRunner, type AngularProject} from '../utils/types.js';
+
+// You don't have to export the function as default. You can also have more than one rule
+// factory per file.
+export function config(): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ return chain([addPuppeteerConfig()])(tree, context);
+ };
+}
+
+function addPuppeteerConfig(): Rule {
+ return (_tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding Puppeteer config file.');
+
+ return addFilesSingle('', {root: ''} as AngularProject, {
+ // No-op here to fill types
+ options: {
+ testRunner: TestRunner.Jasmine,
+ port: 4200,
+ },
+ applyPath: './files',
+ relativeToWorkspacePath: `/`,
+ });
+ };
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/schema.json
new file mode 100644
index 0000000000..8d45751bb1
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/config/schema.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Puppeteer",
+ "title": "Puppeteer Config Schema",
+ "type": "object",
+ "properties": {},
+ "required": []
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/files/common/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/files/common/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template
new file mode 100644
index 0000000000..ca90f258b8
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/files/common/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template
@@ -0,0 +1,18 @@
+<% if(testRunner == 'node') { %>
+import * as assert from 'assert';
+import {describe, it} from 'node:test';
+<% } %><% if(testRunner == 'mocha') { %>
+import * as assert from 'assert';
+<% } %>
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('<%= classify(name) %>', function () {
+ <% if(route) { %>
+ setupBrowserHooks('<%= route %>');
+ <% } else { %>
+ setupBrowserHooks();
+ <% } %>
+ it('', async function () {
+ const {page} = getBrowserState();
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/index.ts
new file mode 100644
index 0000000000..cf1f634f94
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/index.ts
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ chain,
+ type Rule,
+ type SchematicContext,
+ SchematicsException,
+ type Tree,
+} from '@angular-devkit/schematics';
+
+import {addCommonFiles} from '../utils/files.js';
+import {getApplicationProjects} from '../utils/json.js';
+import {
+ TestRunner,
+ type SchematicsSpec,
+ type AngularProject,
+ type PuppeteerSchematicsConfig,
+} from '../utils/types.js';
+
+// You don't have to export the function as default. You can also have more than one rule
+// factory per file.
+export function e2e(userArgs: Record<string, string>): Rule {
+ const options = parseUserTestArgs(userArgs);
+
+ return (tree: Tree, context: SchematicContext) => {
+ return chain([addE2EFile(options)])(tree, context);
+ };
+}
+
+function parseUserTestArgs(userArgs: Record<string, string>): SchematicsSpec {
+ const options: Partial<SchematicsSpec> = {
+ ...userArgs,
+ };
+ if ('p' in userArgs) {
+ options['project'] = userArgs['p'];
+ }
+ if ('n' in userArgs) {
+ options['name'] = userArgs['n'];
+ }
+ if ('r' in userArgs) {
+ options['route'] = userArgs['r'];
+ }
+
+ if (options['route'] && options['route'].startsWith('/')) {
+ options['route'] = options['route'].substring(1);
+ }
+
+ return options as SchematicsSpec;
+}
+
+function findTestingOption<
+ Property extends keyof PuppeteerSchematicsConfig['options'],
+>(
+ [name, project]: [string, AngularProject | undefined],
+ property: Property
+): PuppeteerSchematicsConfig['options'][Property] {
+ if (!project) {
+ throw new Error(`Project "${name}" not found.`);
+ }
+
+ const e2e = project.architect?.e2e;
+ const puppeteer = project.architect?.puppeteer;
+ const builder = '@puppeteer/ng-schematics:puppeteer';
+
+ if (e2e?.builder === builder) {
+ return e2e.options[property];
+ } else if (puppeteer?.builder === builder) {
+ return puppeteer.options[property];
+ }
+
+ throw new Error(`Can't find property "${property}" for project "${name}".`);
+}
+
+function addE2EFile(options: SchematicsSpec): Rule {
+ return async (tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding Spec file.');
+
+ const projects = getApplicationProjects(tree);
+ const projectNames = Object.keys(projects) as [string, ...string[]];
+ const foundProject: [string, AngularProject | undefined] | undefined =
+ projectNames.length === 1
+ ? [projectNames[0], projects[projectNames[0]]]
+ : Object.entries(projects).find(([name, project]) => {
+ return options.project
+ ? options.project === name
+ : project.root === '';
+ });
+ if (!foundProject) {
+ throw new SchematicsException(
+ `Project not found! Please run "ng generate @puppeteer/ng-schematics:test <Test> <Project>"`
+ );
+ }
+
+ const testRunner = findTestingOption(foundProject, 'testRunner');
+ const port = findTestingOption(foundProject, 'port');
+
+ context.logger.debug('Creating Spec file.');
+
+ return addCommonFiles(
+ {[foundProject[0]]: foundProject[1]} as Record<string, AngularProject>,
+ {
+ options: {
+ name: options.name,
+ route: options.route,
+ testRunner,
+ // Node test runner does not support glob patterns
+ // It looks for files `*.test.js`
+ ext: testRunner === TestRunner.Node ? 'test' : 'e2e',
+ port,
+ },
+ }
+ );
+ };
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/schema.json
new file mode 100644
index 0000000000..7752c9ceef
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/e2e/schema.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Puppeteer",
+ "title": "Puppeteer E2E Schema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "alias": "n",
+ "$default": {
+ "$source": "argv",
+ "index": 0
+ },
+ "x-prompt": "Name for spec to be created:"
+ },
+ "project": {
+ "type": "string",
+ "$default": {
+ "$source": "argv",
+ "index": 1
+ },
+ "alias": "p"
+ },
+ "route": {
+ "type": "string",
+ "$default": {
+ "$source": "argv",
+ "index": 1
+ },
+ "alias": "r"
+ }
+ },
+ "required": []
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/.gitignore.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/.gitignore.template
new file mode 100644
index 0000000000..f038b2eb67
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/.gitignore.template
@@ -0,0 +1,2 @@
+# Compiled e2e tests output
+build/
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/app.__ext@dasherize__.ts.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/app.__ext@dasherize__.ts.template
new file mode 100644
index 0000000000..60637d0fa7
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/app.__ext@dasherize__.ts.template
@@ -0,0 +1,20 @@
+<% if(testRunner == 'node') { %>
+import * as assert from 'assert';
+import {describe, it} from 'node:test';
+<% } %><% if(testRunner == 'mocha') { %>
+import * as assert from 'assert';
+<% } %>
+import {setupBrowserHooks, getBrowserState} from './utils';
+
+describe('App test', function () {
+ setupBrowserHooks();
+ it('is running', async function () {
+ const {page} = getBrowserState();
+ const element = await page.locator('::-p-text(<%= project %>)').wait();
+<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
+ expect(element).not.toBeNull();
+<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
+ assert.ok(element);
+<% } %>
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/utils.ts.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/utils.ts.template
new file mode 100644
index 0000000000..2136f99a3a
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tests/utils.ts.template
@@ -0,0 +1,60 @@
+<% if(testRunner == 'node') { %>
+import {before, beforeEach, after, afterEach} from 'node:test';
+<% } %>
+import * as puppeteer from 'puppeteer';
+
+const baseUrl = process.env['baseUrl'] ?? '<%= baseUrl %>';
+let browser: puppeteer.Browser;
+let page: puppeteer.Page;
+
+export function setupBrowserHooks(path = ''): void {
+<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
+ beforeAll(async () => {
+ browser = await puppeteer.launch({
+ headless: 'new'
+ });
+ });
+<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
+ before(async () => {
+ browser = await puppeteer.launch({
+ headless: 'new'
+ });
+ });
+<% } %>
+
+ beforeEach(async () => {
+ page = await browser.newPage();
+ await page.goto(`${baseUrl}${path}`);
+ });
+
+ afterEach(async () => {
+ await page?.close();
+ });
+
+<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
+ afterAll(async () => {
+ await browser?.close();
+ });
+<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
+ after(async () => {
+ await browser?.close();
+ });
+<% } %>
+}
+
+export function getBrowserState(): {
+ browser: puppeteer.Browser;
+ page: puppeteer.Page;
+ baseUrl: string;
+} {
+ if (!browser) {
+ throw new Error(
+ 'No browser state found! Ensure `setupBrowserHooks()` is called.'
+ );
+ }
+ return {
+ browser,
+ page,
+ baseUrl,
+ };
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tsconfig.json.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tsconfig.json.template
new file mode 100644
index 0000000000..38501b89ef
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/common/e2e/tsconfig.json.template
@@ -0,0 +1,10 @@
+{
+ "extends": "<%= tsConfigPath %>",
+ "compilerOptions": {
+ "module": "CommonJS",
+ "rootDir": "tests/",
+ "outDir": "build/",
+ "types": ["<%= testRunner %>"]
+ },
+ "include": ["tests/**/*.ts"]
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/jasmine.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/jasmine.json
new file mode 100644
index 0000000000..ad5dc6fbce
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/jasmine.json
@@ -0,0 +1,10 @@
+{
+ "spec_dir": "e2e",
+ "spec_files": ["**/*[eE]2[eE].js"],
+ "helpers": ["helpers/**/*.?(m)js"],
+ "env": {
+ "failSpecWithNoExpectations": true,
+ "stopSpecOnExpectationFailure": false,
+ "random": true
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js
new file mode 100644
index 0000000000..ee21c6737e
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js
@@ -0,0 +1,10 @@
+/*
+ * For a detailed explanation regarding each configuration property and type check, visit:
+ * https://jestjs.io/docs/configuration
+ */
+
+/** @type {import('jest').Config} */
+module.exports = {
+ testMatch: ['<rootDir>/build/**/*.e2e.js'],
+ testEnvironment: 'node',
+};
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js
new file mode 100644
index 0000000000..28c1839674
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js
@@ -0,0 +1,4 @@
+module.exports = {
+ spec: './e2e/build/**/*.e2e.js',
+ timeout: 5000,
+};
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts
new file mode 100644
index 0000000000..1f962e0cfc
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts
@@ -0,0 +1,135 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ chain,
+ type Rule,
+ type SchematicContext,
+ type Tree,
+} from '@angular-devkit/schematics';
+import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
+import {of} from 'rxjs';
+import {concatMap, map, scan} from 'rxjs/operators';
+
+import {
+ addCommonFiles as addCommonFilesHelper,
+ addFrameworkFiles,
+ getNgCommandName,
+ hasE2ETester,
+} from '../utils/files.js';
+import {getApplicationProjects} from '../utils/json.js';
+import {
+ addPackageJsonDependencies,
+ addPackageJsonScripts,
+ getDependenciesFromOptions,
+ getPackageLatestNpmVersion,
+ DependencyType,
+ type NodePackage,
+ updateAngularJsonScripts,
+} from '../utils/packages.js';
+import {TestRunner, type SchematicsOptions} from '../utils/types.js';
+
+const DEFAULT_PORT = 4200;
+
+// You don't have to export the function as default. You can also have more than one rule
+// factory per file.
+export function ngAdd(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ return chain([
+ addDependencies(options),
+ addCommonFiles(options),
+ addOtherFiles(options),
+ updateScripts(),
+ updateAngularConfig(options),
+ ])(tree, context);
+ };
+}
+
+function addDependencies(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding dependencies to "package.json"');
+ const dependencies = getDependenciesFromOptions(options);
+
+ return of(...dependencies).pipe(
+ concatMap((packageName: string) => {
+ return getPackageLatestNpmVersion(packageName);
+ }),
+ scan((array, nodePackage) => {
+ array.push(nodePackage);
+ return array;
+ }, [] as NodePackage[]),
+ map(packages => {
+ context.logger.debug('Updating dependencies...');
+ addPackageJsonDependencies(tree, packages, DependencyType.Dev);
+ context.addTask(
+ new NodePackageInstallTask({
+ // Trigger Post-Install hooks to download the browser
+ allowScripts: true,
+ })
+ );
+
+ return tree;
+ })
+ );
+ };
+}
+
+function updateScripts(): Rule {
+ return (tree: Tree, context: SchematicContext): Tree => {
+ context.logger.debug('Updating "package.json" scripts');
+ const projects = getApplicationProjects(tree);
+ const projectsKeys = Object.keys(projects);
+
+ if (projectsKeys.length === 1) {
+ const name = getNgCommandName(projects);
+ const prefix = hasE2ETester(projects) ? `run ${projectsKeys[0]}:` : '';
+ return addPackageJsonScripts(tree, [
+ {
+ name,
+ script: `ng ${prefix}${name}`,
+ },
+ ]);
+ }
+ return tree;
+ };
+}
+
+function addCommonFiles(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding Puppeteer base files.');
+ const projects = getApplicationProjects(tree);
+
+ return addCommonFilesHelper(projects, {
+ options: {
+ ...options,
+ port: DEFAULT_PORT,
+ ext: options.testRunner === TestRunner.Node ? 'test' : 'e2e',
+ },
+ });
+ };
+}
+
+function addOtherFiles(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext) => {
+ context.logger.debug('Adding Puppeteer additional files.');
+ const projects = getApplicationProjects(tree);
+
+ return addFrameworkFiles(projects, {
+ options: {
+ ...options,
+ port: DEFAULT_PORT,
+ },
+ });
+ };
+}
+
+function updateAngularConfig(options: SchematicsOptions): Rule {
+ return (tree: Tree, context: SchematicContext): Tree => {
+ context.logger.debug('Updating "angular.json".');
+
+ return updateAngularJsonScripts(tree, options);
+ };
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json
new file mode 100644
index 0000000000..0fa581f1a7
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Puppeteer",
+ "title": "Puppeteer Install Schema",
+ "type": "object",
+ "properties": {
+ "testRunner": {
+ "type": "string",
+ "enum": ["jasmine", "jest", "mocha", "node"],
+ "default": "jasmine",
+ "alias": "t",
+ "x-prompt": {
+ "message": "Which test runners do you wish to use?",
+ "type": "list",
+ "items": [
+ {
+ "value": "jasmine",
+ "label": "Use Jasmine [https://jasmine.github.io/]"
+ },
+ {
+ "value": "jest",
+ "label": "Use Jest [https://jestjs.io/]"
+ },
+ {
+ "value": "mocha",
+ "label": "Use Mocha [https://mochajs.org/]"
+ },
+ {
+ "value": "node",
+ "label": "Use Node Test Runner [https://nodejs.org/api/test.html]"
+ }
+ ]
+ }
+ }
+ },
+ "required": []
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts
new file mode 100644
index 0000000000..4d255062b4
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts
@@ -0,0 +1,152 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {relative, resolve} from 'path';
+
+import {getSystemPath, normalize, strings} from '@angular-devkit/core';
+import type {Rule} from '@angular-devkit/schematics';
+import {
+ apply,
+ applyTemplates,
+ chain,
+ mergeWith,
+ move,
+ url,
+} from '@angular-devkit/schematics';
+
+import type {AngularProject, TestRunner} from './types.js';
+
+export interface FilesOptions {
+ options: {
+ testRunner: TestRunner;
+ port: number;
+ name?: string;
+ exportConfig?: boolean;
+ ext?: string;
+ route?: string;
+ };
+ applyPath: string;
+ relativeToWorkspacePath: string;
+ movePath?: string;
+}
+
+export function addFilesToProjects(
+ projects: Record<string, AngularProject>,
+ options: FilesOptions
+): Rule {
+ return chain(
+ Object.keys(projects).map(name => {
+ return addFilesSingle(name, projects[name] as AngularProject, options);
+ })
+ );
+}
+
+export function addFilesSingle(
+ name: string,
+ project: AngularProject,
+ {options, applyPath, movePath, relativeToWorkspacePath}: FilesOptions
+): Rule {
+ const projectPath = resolve(getSystemPath(normalize(project.root)));
+ const workspacePath = resolve(getSystemPath(normalize('')));
+
+ const relativeToWorkspace = relative(
+ `${projectPath}${relativeToWorkspacePath}`,
+ workspacePath
+ );
+
+ const baseUrl = getProjectBaseUrl(project, options.port);
+ const tsConfigPath = getTsConfigPath(project);
+
+ return mergeWith(
+ apply(url(applyPath), [
+ move(movePath ? `${project.root}${movePath}` : project.root),
+ applyTemplates({
+ ...options,
+ ...strings,
+ root: project.root ? `${project.root}/` : project.root,
+ baseUrl,
+ tsConfigPath,
+ project: name,
+ relativeToWorkspace,
+ }),
+ ])
+ );
+}
+
+function getProjectBaseUrl(project: AngularProject, port: number): string {
+ let options = {protocol: 'http', port, host: 'localhost'};
+
+ if (project.architect?.serve?.options) {
+ const projectOptions = project.architect?.serve?.options;
+ const projectPort = port !== 4200 ? port : projectOptions?.port ?? port;
+ options = {...options, ...projectOptions, port: projectPort};
+ options.protocol = projectOptions.ssl ? 'https' : 'http';
+ }
+
+ return `${options.protocol}://${options.host}:${options.port}/`;
+}
+
+function getTsConfigPath(project: AngularProject): string {
+ const filename = 'tsconfig.json';
+
+ if (!project.root) {
+ return `../${filename}`;
+ }
+
+ const nested = project.root
+ .split('/')
+ .map(() => {
+ return '../';
+ })
+ .join('');
+
+ // Prepend a single `../` as we put the test inside `e2e` folder
+ return `../${nested}${filename}`;
+}
+
+export function addCommonFiles(
+ projects: Record<string, AngularProject>,
+ filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
+): Rule {
+ const options: FilesOptions = {
+ ...filesOptions,
+ applyPath: './files/common',
+ relativeToWorkspacePath: `/`,
+ };
+
+ return addFilesToProjects(projects, options);
+}
+
+export function addFrameworkFiles(
+ projects: Record<string, AngularProject>,
+ filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
+): Rule {
+ const testRunner = filesOptions.options.testRunner;
+ const options: FilesOptions = {
+ ...filesOptions,
+ applyPath: `./files/${testRunner}`,
+ relativeToWorkspacePath: `/`,
+ };
+
+ return addFilesToProjects(projects, options);
+}
+
+export function hasE2ETester(
+ projects: Record<string, AngularProject>
+): boolean {
+ return Object.values(projects).some((project: AngularProject) => {
+ return Boolean(project.architect?.e2e);
+ });
+}
+
+export function getNgCommandName(
+ projects: Record<string, AngularProject>
+): string {
+ if (!hasE2ETester(projects)) {
+ return 'e2e';
+ }
+ return 'puppeteer';
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts
new file mode 100644
index 0000000000..1a38d638a7
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {SchematicsException, type Tree} from '@angular-devkit/schematics';
+
+import type {AngularJson, AngularProject} from './types.js';
+
+export function getJsonFileAsObject(
+ tree: Tree,
+ path: string
+): Record<string, unknown> {
+ try {
+ const buffer = tree.read(path) as Buffer;
+ const content = buffer.toString();
+ return JSON.parse(content);
+ } catch {
+ throw new SchematicsException(`Unable to retrieve file at ${path}.`);
+ }
+}
+
+export function getObjectAsJson(object: Record<string, unknown>): string {
+ return JSON.stringify(object, null, 2);
+}
+
+export function getAngularConfig(tree: Tree): AngularJson {
+ return getJsonFileAsObject(tree, './angular.json') as unknown as AngularJson;
+}
+
+export function getApplicationProjects(
+ tree: Tree
+): Record<string, AngularProject> {
+ const {projects} = getAngularConfig(tree);
+
+ const applications: Record<string, AngularProject> = {};
+ for (const key in projects) {
+ const project = projects[key]!;
+ if (project.projectType === 'application') {
+ applications[key] = project;
+ }
+ }
+ return applications;
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts
new file mode 100644
index 0000000000..6ef8ef6002
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts
@@ -0,0 +1,189 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {get} from 'https';
+
+import type {Tree} from '@angular-devkit/schematics';
+
+import {getNgCommandName} from './files.js';
+import {
+ getAngularConfig,
+ getApplicationProjects,
+ getJsonFileAsObject,
+ getObjectAsJson,
+} from './json.js';
+import {type SchematicsOptions, TestRunner} from './types.js';
+export interface NodePackage {
+ name: string;
+ version: string;
+}
+export interface NodeScripts {
+ name: string;
+ script: string;
+}
+
+export enum DependencyType {
+ Default = 'dependencies',
+ Dev = 'devDependencies',
+ Peer = 'peerDependencies',
+ Optional = 'optionalDependencies',
+}
+
+export function getPackageLatestNpmVersion(name: string): Promise<NodePackage> {
+ return new Promise(resolve => {
+ let version = 'latest';
+
+ return get(`https://registry.npmjs.org/${name}`, res => {
+ let data = '';
+
+ res.on('data', chunk => {
+ data += chunk;
+ });
+ res.on('end', () => {
+ try {
+ const response = JSON.parse(data);
+ version = response?.['dist-tags']?.latest ?? version;
+ } catch {
+ } finally {
+ resolve({
+ name,
+ version,
+ });
+ }
+ });
+ }).on('error', () => {
+ resolve({
+ name,
+ version,
+ });
+ });
+ });
+}
+
+function updateJsonValues(
+ json: Record<string, any>,
+ target: string,
+ updates: Array<{name: string; value: any}>,
+ overwrite = false
+) {
+ updates.forEach(({name, value}) => {
+ if (!json[target][name] || overwrite) {
+ json[target] = {
+ ...json[target],
+ [name]: value,
+ };
+ }
+ });
+}
+
+export function addPackageJsonDependencies(
+ tree: Tree,
+ packages: NodePackage[],
+ type: DependencyType,
+ overwrite?: boolean,
+ fileLocation = './package.json'
+): Tree {
+ const packageJson = getJsonFileAsObject(tree, fileLocation);
+
+ updateJsonValues(
+ packageJson,
+ type,
+ packages.map(({name, version}) => {
+ return {name, value: version};
+ }),
+ overwrite
+ );
+
+ tree.overwrite(fileLocation, getObjectAsJson(packageJson));
+
+ return tree;
+}
+
+export function getDependenciesFromOptions(
+ options: SchematicsOptions
+): string[] {
+ const dependencies = ['puppeteer'];
+
+ switch (options.testRunner) {
+ case TestRunner.Jasmine:
+ dependencies.push('jasmine');
+ break;
+ case TestRunner.Jest:
+ dependencies.push('jest', '@types/jest');
+ break;
+ case TestRunner.Mocha:
+ dependencies.push('mocha', '@types/mocha');
+ break;
+ case TestRunner.Node:
+ dependencies.push('@types/node');
+ break;
+ }
+
+ return dependencies;
+}
+
+export function addPackageJsonScripts(
+ tree: Tree,
+ scripts: NodeScripts[],
+ overwrite?: boolean,
+ fileLocation = './package.json'
+): Tree {
+ const packageJson = getJsonFileAsObject(tree, fileLocation);
+
+ updateJsonValues(
+ packageJson,
+ 'scripts',
+ scripts.map(({name, script}) => {
+ return {name, value: script};
+ }),
+ overwrite
+ );
+
+ tree.overwrite(fileLocation, getObjectAsJson(packageJson));
+
+ return tree;
+}
+
+export function updateAngularJsonScripts(
+ tree: Tree,
+ options: SchematicsOptions,
+ overwrite = true
+): Tree {
+ const angularJson = getAngularConfig(tree);
+ const projects = getApplicationProjects(tree);
+ const name = getNgCommandName(projects);
+
+ Object.keys(projects).forEach(project => {
+ const e2eScript = [
+ {
+ name,
+ value: {
+ builder: '@puppeteer/ng-schematics:puppeteer',
+ options: {
+ devServerTarget: `${project}:serve`,
+ testRunner: options.testRunner,
+ },
+ configurations: {
+ production: {
+ devServerTarget: `${project}:serve:production`,
+ },
+ },
+ },
+ },
+ ];
+
+ updateJsonValues(
+ angularJson['projects'][project]!,
+ 'architect',
+ e2eScript,
+ overwrite
+ );
+ });
+
+ tree.overwrite('./angular.json', getObjectAsJson(angularJson as any));
+
+ return tree;
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts
new file mode 100644
index 0000000000..7d66e0f0fa
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export enum TestRunner {
+ Jasmine = 'jasmine',
+ Jest = 'jest',
+ Mocha = 'mocha',
+ Node = 'node',
+}
+
+export interface SchematicsOptions {
+ testRunner: TestRunner;
+}
+
+export interface PuppeteerSchematicsConfig {
+ builder: string;
+ options: {
+ port: number;
+ testRunner: TestRunner;
+ };
+}
+export interface AngularProject {
+ projectType: 'application' | 'library';
+ root: string;
+ architect: {
+ e2e?: PuppeteerSchematicsConfig;
+ puppeteer?: PuppeteerSchematicsConfig;
+ serve: {
+ options: {
+ ssl: string;
+ port: number;
+ };
+ };
+ };
+}
+export interface AngularJson {
+ projects: Record<string, AngularProject>;
+}
+
+export interface SchematicsSpec {
+ name: string;
+ project?: string;
+ route?: string;
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/config.test.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/config.test.ts
new file mode 100644
index 0000000000..e4ec03ed54
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/src/config.test.ts
@@ -0,0 +1,30 @@
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {
+ buildTestingTree,
+ getMultiApplicationFile,
+ setupHttpHooks,
+} from './utils.js';
+
+void describe('@puppeteer/ng-schematics: config', () => {
+ setupHttpHooks();
+
+ void describe('Single Project', () => {
+ void it('should create default file', async () => {
+ const tree = await buildTestingTree('config', 'single');
+ expect(tree.files).toContain('/.puppeteerrc.mjs');
+ });
+ });
+
+ void describe('Multi projects', () => {
+ void it('should create default file', async () => {
+ const tree = await buildTestingTree('config', 'multi');
+ expect(tree.files).toContain('/.puppeteerrc.mjs');
+ expect(tree.files).not.toContain(
+ getMultiApplicationFile('.puppeteerrc.mjs')
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/e2e.test.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/e2e.test.ts
new file mode 100644
index 0000000000..8ae211cd59
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/src/e2e.test.ts
@@ -0,0 +1,111 @@
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {
+ buildTestingTree,
+ getMultiApplicationFile,
+ setupHttpHooks,
+} from './utils.js';
+
+void describe('@puppeteer/ng-schematics: e2e', () => {
+ setupHttpHooks();
+
+ void describe('Single Project', () => {
+ void it('should create default file', async () => {
+ const tree = await buildTestingTree('e2e', 'single', {
+ name: 'myTest',
+ });
+ expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
+ expect(tree.files).not.toContain('/e2e/tests/my-test.test.ts');
+ });
+
+ void it('should create Node file', async () => {
+ const tree = await buildTestingTree('e2e', 'single', {
+ name: 'myTest',
+ testRunner: 'node',
+ });
+ expect(tree.files).not.toContain('/e2e/tests/my-test.e2e.ts');
+ expect(tree.files).toContain('/e2e/tests/my-test.test.ts');
+ });
+
+ void it('should create file with route', async () => {
+ const route = 'home';
+ const tree = await buildTestingTree('e2e', 'single', {
+ name: 'myTest',
+ route,
+ });
+ expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
+ expect(tree.readContent('/e2e/tests/my-test.e2e.ts')).toContain(
+ `setupBrowserHooks('${route}');`
+ );
+ });
+
+ void it('should create with route with starting slash', async () => {
+ const route = '/home';
+ const tree = await buildTestingTree('e2e', 'single', {
+ name: 'myTest',
+ route,
+ });
+ expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
+ expect(tree.readContent('/e2e/tests/my-test.e2e.ts')).toContain(
+ `setupBrowserHooks('home');`
+ );
+ });
+ });
+
+ void describe('Multi projects', () => {
+ void it('should create default file', async () => {
+ const tree = await buildTestingTree('e2e', 'multi', {
+ name: 'myTest',
+ });
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
+ );
+ expect(tree.files).not.toContain(
+ getMultiApplicationFile('e2e/tests/my-test.test.ts')
+ );
+ });
+
+ void it('should create Node file', async () => {
+ const tree = await buildTestingTree('e2e', 'multi', {
+ name: 'myTest',
+ testRunner: 'node',
+ });
+ expect(tree.files).not.toContain(
+ getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
+ );
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/my-test.test.ts')
+ );
+ });
+
+ void it('should create file with route', async () => {
+ const route = 'home';
+ const tree = await buildTestingTree('e2e', 'multi', {
+ name: 'myTest',
+ route,
+ });
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
+ );
+ expect(
+ tree.readContent(getMultiApplicationFile('e2e/tests/my-test.e2e.ts'))
+ ).toContain(`setupBrowserHooks('${route}');`);
+ });
+
+ void it('should create with route with starting slash', async () => {
+ const route = '/home';
+ const tree = await buildTestingTree('e2e', 'multi', {
+ name: 'myTest',
+ route,
+ });
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
+ );
+ expect(
+ tree.readContent(getMultiApplicationFile('e2e/tests/my-test.e2e.ts'))
+ ).toContain(`setupBrowserHooks('home');`);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/ng-add.test.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/ng-add.test.ts
new file mode 100644
index 0000000000..d912c5dc3d
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/src/ng-add.test.ts
@@ -0,0 +1,260 @@
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {
+ MULTI_LIBRARY_OPTIONS,
+ buildTestingTree,
+ getAngularJsonScripts,
+ getMultiApplicationFile,
+ getMultiLibraryFile,
+ getPackageJson,
+ runSchematic,
+ setupHttpHooks,
+} from './utils.js';
+
+void describe('@puppeteer/ng-schematics: ng-add', () => {
+ setupHttpHooks();
+
+ void describe('Single Project', () => {
+ void it('should create base files and update to "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add');
+ const {devDependencies, scripts} = getPackageJson(tree);
+ const {builder, configurations} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/tsconfig.json');
+ expect(tree.files).toContain('/e2e/tests/app.e2e.ts');
+ expect(tree.files).toContain('/e2e/tests/utils.ts');
+ expect(devDependencies).toContain('puppeteer');
+ expect(scripts['e2e']).toBe('ng e2e');
+ expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
+ expect(configurations).toEqual({
+ production: {
+ devServerTarget: 'sandbox:serve:production',
+ },
+ });
+ });
+ void it('should update create proper "ng" command for non default tester', async () => {
+ let tree = await buildTestingTree('ng-add', 'single');
+ // Re-run schematic to have e2e populated
+ tree = await runSchematic(tree, 'ng-add');
+ const {scripts} = getPackageJson(tree);
+ const {builder} = getAngularJsonScripts(tree, false);
+
+ expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
+ expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
+ });
+ void it('should not create Puppeteer config', async () => {
+ const {files} = await buildTestingTree('ng-add', 'single');
+
+ expect(files).not.toContain('/.puppeteerrc.cjs');
+ });
+ void it('should create Jasmine files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'single', {
+ testRunner: 'jasmine',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/jasmine.json');
+ expect(devDependencies).toContain('jasmine');
+ expect(options['testRunner']).toBe('jasmine');
+ });
+ void it('should create Jest files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'single', {
+ testRunner: 'jest',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/jest.config.js');
+ expect(devDependencies).toContain('jest');
+ expect(devDependencies).toContain('@types/jest');
+ expect(options['testRunner']).toBe('jest');
+ });
+ void it('should create Mocha files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'single', {
+ testRunner: 'mocha',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/.mocharc.js');
+ expect(devDependencies).toContain('mocha');
+ expect(devDependencies).toContain('@types/mocha');
+ expect(options['testRunner']).toBe('mocha');
+ });
+ void it('should create Node files', async () => {
+ const tree = await buildTestingTree('ng-add', 'single', {
+ testRunner: 'node',
+ });
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain('/e2e/.gitignore');
+ expect(tree.files).not.toContain('/e2e/tests/app.e2e.ts');
+ expect(tree.files).toContain('/e2e/tests/app.test.ts');
+ expect(options['testRunner']).toBe('node');
+ });
+ void it('should create TypeScript files', async () => {
+ const tree = await buildTestingTree('ng-add', 'single');
+ const tsConfigPath = '/e2e/tsconfig.json';
+ const tsConfig = tree.readJson(tsConfigPath);
+
+ expect(tree.files).toContain(tsConfigPath);
+ expect(tsConfig).toMatchObject({
+ extends: '../tsconfig.json',
+ compilerOptions: {
+ module: 'CommonJS',
+ },
+ });
+ });
+ void it('should not create port value', async () => {
+ const tree = await buildTestingTree('ng-add');
+
+ const {options} = getAngularJsonScripts(tree);
+ expect(options['port']).toBeUndefined();
+ });
+ });
+
+ void describe('Multi projects Application', () => {
+ void it('should create base files and update to "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi');
+ const {devDependencies, scripts} = getPackageJson(tree);
+ const {builder, configurations} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tsconfig.json')
+ );
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/app.e2e.ts')
+ );
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/utils.ts')
+ );
+ expect(devDependencies).toContain('puppeteer');
+ expect(scripts['e2e']).toBe('ng e2e');
+ expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
+ expect(configurations).toEqual({
+ production: {
+ devServerTarget: 'sandbox:serve:production',
+ },
+ });
+ });
+ void it('should update create proper "ng" command for non default tester', async () => {
+ let tree = await buildTestingTree('ng-add', 'multi');
+ // Re-run schematic to have e2e populated
+ tree = await runSchematic(tree, 'ng-add');
+ const {scripts} = getPackageJson(tree);
+ const {builder} = getAngularJsonScripts(tree, false);
+
+ expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
+ expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
+ });
+ void it('should not create Puppeteer config', async () => {
+ const {files} = await buildTestingTree('ng-add', 'multi');
+
+ expect(files).not.toContain(getMultiApplicationFile('.puppeteerrc.cjs'));
+ expect(files).not.toContain('/.puppeteerrc.cjs');
+ });
+ void it('should create Jasmine files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi', {
+ testRunner: 'jasmine',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(getMultiApplicationFile('e2e/jasmine.json'));
+ expect(devDependencies).toContain('jasmine');
+ expect(options['testRunner']).toBe('jasmine');
+ });
+ void it('should create Jest files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi', {
+ testRunner: 'jest',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/jest.config.js')
+ );
+ expect(devDependencies).toContain('jest');
+ expect(devDependencies).toContain('@types/jest');
+ expect(options['testRunner']).toBe('jest');
+ });
+ void it('should create Mocha files and update "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi', {
+ testRunner: 'mocha',
+ });
+ const {devDependencies} = getPackageJson(tree);
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(getMultiApplicationFile('e2e/.mocharc.js'));
+ expect(devDependencies).toContain('mocha');
+ expect(devDependencies).toContain('@types/mocha');
+ expect(options['testRunner']).toBe('mocha');
+ });
+ void it('should create Node files', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi', {
+ testRunner: 'node',
+ });
+ const {options} = getAngularJsonScripts(tree);
+
+ expect(tree.files).toContain(getMultiApplicationFile('e2e/.gitignore'));
+ expect(tree.files).not.toContain(
+ getMultiApplicationFile('e2e/tests/app.e2e.ts')
+ );
+ expect(tree.files).toContain(
+ getMultiApplicationFile('e2e/tests/app.test.ts')
+ );
+ expect(options['testRunner']).toBe('node');
+ });
+ void it('should create TypeScript files', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi');
+ const tsConfigPath = getMultiApplicationFile('e2e/tsconfig.json');
+ const tsConfig = tree.readJson(tsConfigPath);
+
+ expect(tree.files).toContain(tsConfigPath);
+ expect(tsConfig).toMatchObject({
+ extends: '../../../tsconfig.json',
+ compilerOptions: {
+ module: 'CommonJS',
+ },
+ });
+ });
+ void it('should not create port value', async () => {
+ const tree = await buildTestingTree('ng-add');
+
+ const {options} = getAngularJsonScripts(tree);
+ expect(options['port']).toBeUndefined();
+ });
+ });
+
+ void describe('Multi projects Library', () => {
+ void it('should create base files and update to "package.json"', async () => {
+ const tree = await buildTestingTree('ng-add', 'multi');
+ const config = getAngularJsonScripts(
+ tree,
+ true,
+ MULTI_LIBRARY_OPTIONS.name
+ );
+
+ expect(tree.files).not.toContain(
+ getMultiLibraryFile('e2e/tsconfig.json')
+ );
+ expect(tree.files).not.toContain(
+ getMultiLibraryFile('e2e/tests/app.e2e.ts')
+ );
+ expect(tree.files).not.toContain(
+ getMultiLibraryFile('e2e/tests/utils.ts')
+ );
+ expect(config).toBeUndefined();
+ });
+
+ void it('should not create Puppeteer config', async () => {
+ const {files} = await buildTestingTree('ng-add', 'multi');
+
+ expect(files).not.toContain(getMultiLibraryFile('.puppeteerrc.cjs'));
+ expect(files).not.toContain('/.puppeteerrc.cjs');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/utils.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/utils.ts
new file mode 100644
index 0000000000..503cbd5cec
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/src/utils.ts
@@ -0,0 +1,147 @@
+import https from 'https';
+import {before, after} from 'node:test';
+import {join} from 'path';
+
+import type {JsonObject} from '@angular-devkit/core';
+import {
+ SchematicTestRunner,
+ type UnitTestTree,
+} from '@angular-devkit/schematics/testing';
+import sinon from 'sinon';
+
+const WORKSPACE_OPTIONS = {
+ name: 'workspace',
+ newProjectRoot: 'projects',
+ version: '14.0.0',
+};
+
+const SINGLE_APPLICATION_OPTIONS = {
+ name: 'sandbox',
+ directory: '.',
+ createApplication: true,
+ version: '14.0.0',
+};
+
+const MULTI_APPLICATION_OPTIONS = {
+ name: SINGLE_APPLICATION_OPTIONS.name,
+};
+
+export const MULTI_LIBRARY_OPTIONS = {
+ name: 'components',
+};
+
+export function setupHttpHooks(): void {
+ // Stop outgoing Request for version fetching
+ before(() => {
+ const httpsGetStub = sinon.stub(https, 'get');
+ httpsGetStub.returns({
+ on: (_: string, callback: () => void) => {
+ callback();
+ },
+ } as any);
+ });
+
+ after(() => {
+ sinon.restore();
+ });
+}
+
+export function getAngularJsonScripts(
+ tree: UnitTestTree,
+ isDefault = true,
+ name = SINGLE_APPLICATION_OPTIONS.name
+): {
+ builder: string;
+ configurations: Record<string, any>;
+ options: Record<string, any>;
+} {
+ const angularJson = tree.readJson('angular.json') as any;
+ const e2eScript = isDefault ? 'e2e' : 'puppeteer';
+ return angularJson['projects']?.[name]?.['architect'][e2eScript];
+}
+
+export function getPackageJson(tree: UnitTestTree): {
+ scripts: Record<string, string>;
+ devDependencies: string[];
+} {
+ const packageJson = tree.readJson('package.json') as JsonObject;
+ return {
+ scripts: packageJson['scripts'] as any,
+ devDependencies: Object.keys(
+ packageJson['devDependencies'] as Record<string, string>
+ ),
+ };
+}
+
+export function getMultiApplicationFile(file: string): string {
+ return `/${WORKSPACE_OPTIONS.newProjectRoot}/${MULTI_APPLICATION_OPTIONS.name}/${file}`;
+}
+export function getMultiLibraryFile(file: string): string {
+ return `/${WORKSPACE_OPTIONS.newProjectRoot}/${MULTI_LIBRARY_OPTIONS.name}/${file}`;
+}
+
+export async function buildTestingTree(
+ command: 'ng-add' | 'e2e' | 'config',
+ type: 'single' | 'multi' = 'single',
+ userOptions?: Record<string, unknown>
+): Promise<UnitTestTree> {
+ const runner = new SchematicTestRunner(
+ 'schematics',
+ join(__dirname, '../../lib/schematics/collection.json')
+ );
+ const options = {
+ testRunner: 'jasmine',
+ ...userOptions,
+ };
+ let workingTree: UnitTestTree;
+
+ // Build workspace
+ if (type === 'single') {
+ workingTree = await runner.runExternalSchematic(
+ '@schematics/angular',
+ 'ng-new',
+ SINGLE_APPLICATION_OPTIONS
+ );
+ } else {
+ // Build workspace
+ workingTree = await runner.runExternalSchematic(
+ '@schematics/angular',
+ 'workspace',
+ WORKSPACE_OPTIONS
+ );
+ // Build dummy application
+ workingTree = await runner.runExternalSchematic(
+ '@schematics/angular',
+ 'application',
+ MULTI_APPLICATION_OPTIONS,
+ workingTree
+ );
+ // Build dummy library
+ workingTree = await runner.runExternalSchematic(
+ '@schematics/angular',
+ 'library',
+ MULTI_LIBRARY_OPTIONS,
+ workingTree
+ );
+ }
+
+ if (command !== 'ng-add') {
+ // We want to create update the proper files with `ng-add`
+ // First else the angular.json will have wrong data
+ workingTree = await runner.runSchematic('ng-add', options, workingTree);
+ }
+
+ return await runner.runSchematic(command, options, workingTree);
+}
+
+export async function runSchematic(
+ tree: UnitTestTree,
+ command: 'ng-add' | 'test',
+ options?: Record<string, any>
+): Promise<UnitTestTree> {
+ const runner = new SchematicTestRunner(
+ 'schematics',
+ join(__dirname, '../../lib/schematics/collection.json')
+ );
+ return await runner.runSchematic(command, options, tree);
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/test/tsconfig.json b/remote/test/puppeteer/packages/ng-schematics/test/tsconfig.json
new file mode 100644
index 0000000000..3d45f9cc54
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/test/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src/",
+ "outDir": "build/",
+ "types": ["node"],
+ },
+ "include": ["src/**/*"],
+ "references": [{"path": "../tsconfig.json"}],
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.mjs b/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.mjs
new file mode 100644
index 0000000000..2bd88f229a
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.mjs
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs/promises';
+import path from 'path';
+import url from 'url';
+
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
+
+/**
+ *
+ * @param {String} directory
+ * @param {String[]} files
+ */
+async function findSchemaFiles(directory, files = []) {
+ const items = await fs.readdir(directory);
+ const promises = [];
+ // Match any listing that has no *.* format
+ // Ignore files folder
+ const regEx = /^.*\.[^\s]*$/;
+
+ items.forEach(item => {
+ if (!item.match(regEx)) {
+ promises.push(findSchemaFiles(`${directory}/${item}`, files));
+ } else if (item.endsWith('.json') || directory.includes('files')) {
+ files.push(`${directory}/${item}`);
+ }
+ });
+
+ await Promise.all(promises);
+
+ return files;
+}
+
+async function copySchemaFiles() {
+ const srcDir = path.join(__dirname, '..', 'src');
+ const outputDir = path.join(__dirname, '..', 'lib');
+ const files = await findSchemaFiles(srcDir);
+
+ const moves = files.map(file => {
+ const to = file.replace(srcDir, outputDir);
+
+ return {from: file, to};
+ });
+
+ // Because fs.cp is Experimental (recursive support)
+ // We need to create directories first and copy the files
+ await Promise.all(
+ moves.map(({to}) => {
+ const dir = path.dirname(to);
+ return fs.mkdir(dir, {recursive: true});
+ })
+ );
+ await Promise.all(
+ moves.map(({from, to}) => {
+ return fs.copyFile(from, to);
+ })
+ );
+}
+
+copySchemaFiles();
diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/projects.mjs b/remote/test/puppeteer/packages/ng-schematics/tools/projects.mjs
new file mode 100644
index 0000000000..985200881e
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tools/projects.mjs
@@ -0,0 +1,159 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {spawn} from 'child_process';
+import {randomUUID} from 'crypto';
+import {readFile, writeFile} from 'fs/promises';
+import {join} from 'path';
+import {cwd} from 'process';
+
+class AngularProject {
+ static ports = new Set();
+ static randomPort() {
+ const min = 4000;
+ const max = 9876;
+ return Math.floor(Math.random() * (max - min + 1) + min);
+ }
+ static port() {
+ const port = AngularProject.randomPort();
+ if (AngularProject.ports.has(port)) {
+ return AngularProject.port();
+ }
+ return port;
+ }
+
+ static #scripts = testRunner => {
+ return {
+ // Builds the ng-schematics before running them
+ 'build:schematics': 'npm run --prefix ../../ build',
+ // Deletes all files created by Puppeteer Ng-Schematics to avoid errors
+ 'delete:file':
+ 'rm -f .puppeteerrc.cjs && rm -f tsconfig.e2e.json && rm -R -f e2e/',
+ // Runs the Puppeteer Ng-Schematics against the sandbox
+ schematics: 'schematics ../../:ng-add --dry-run=false',
+ 'schematics:e2e': 'schematics ../../:e2e --dry-run=false',
+ 'schematics:config': 'schematics ../../:config --dry-run=false',
+ 'schematics:smoke': `schematics ../../:ng-add --dry-run=false --test-runner="${testRunner}" && ng e2e`,
+ };
+ };
+ /** Folder name */
+ #name;
+ /** E2E test runner to use */
+ #runner;
+
+ constructor(runner, name) {
+ this.#runner = runner ?? 'node';
+ this.#name = name ?? randomUUID();
+ }
+
+ get runner() {
+ return this.#runner;
+ }
+
+ get name() {
+ return this.#name;
+ }
+
+ async executeCommand(command, options) {
+ const [executable, ...args] = command.split(' ');
+ await new Promise((resolve, reject) => {
+ const createProcess = spawn(executable, args, {
+ shell: true,
+ ...options,
+ });
+
+ createProcess.stdout.on('data', data => {
+ data = data
+ .toString()
+ // Replace new lines with a prefix including the test runner
+ .replace(/(?:\r\n?|\n)(?=.*[\r\n])/g, `\n${this.#runner} - `);
+ console.log(`${this.#runner} - ${data}`);
+ });
+
+ createProcess.on('error', message => {
+ console.error(`Running ${command} exited with error:`, message);
+ reject(message);
+ });
+
+ createProcess.on('exit', code => {
+ if (code === 0) {
+ resolve(true);
+ } else {
+ reject();
+ }
+ });
+ });
+ }
+
+ async create() {
+ await this.createProject();
+ await this.updatePackageJson();
+ }
+
+ async updatePackageJson() {
+ const packageJsonFile = join(cwd(), `/sandbox/${this.#name}/package.json`);
+ const packageJson = JSON.parse(await readFile(packageJsonFile));
+ packageJson['scripts'] = {
+ ...packageJson['scripts'],
+ ...AngularProject.#scripts(this.#runner),
+ };
+ await writeFile(packageJsonFile, JSON.stringify(packageJson, null, 2));
+ }
+
+ get commandOptions() {
+ return {
+ ...process.env,
+ cwd: join(cwd(), `/sandbox/${this.#name}/`),
+ };
+ }
+
+ async runNpmScripts(command) {
+ await this.executeCommand(`npm run ${command}`, this.commandOptions);
+ }
+
+ async runSchematics() {
+ await this.runNpmScripts('schematics');
+ }
+
+ async runSchematicsE2E() {
+ await this.runNpmScripts('schematics:e2e');
+ }
+
+ async runSchematicsConfig() {
+ await this.runNpmScripts('schematics:config');
+ }
+
+ async runSmoke() {
+ await this.runNpmScripts(
+ `schematics:smoke -- --port=${AngularProject.port()}`
+ );
+ }
+}
+
+export class AngularProjectSingle extends AngularProject {
+ async createProject() {
+ await this.executeCommand(
+ `ng new ${this.name} --directory=sandbox/${this.name} --defaults --skip-git`
+ );
+ }
+}
+
+export class AngularProjectMulti extends AngularProject {
+ async createProject() {
+ await this.executeCommand(
+ `ng new ${this.name} --create-application=false --directory=sandbox/${this.name} --defaults --skip-git`
+ );
+
+ await this.executeCommand(
+ `ng generate application core --style=css --routing=true`,
+ this.commandOptions
+ );
+ await this.executeCommand(
+ `ng generate application admin --style=css --routing=false`,
+ this.commandOptions
+ );
+ }
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/smoke.mjs b/remote/test/puppeteer/packages/ng-schematics/tools/smoke.mjs
new file mode 100644
index 0000000000..8ae9907266
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tools/smoke.mjs
@@ -0,0 +1,72 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ok} from 'node:assert';
+import {execSync} from 'node:child_process';
+import {parseArgs} from 'node:util';
+
+import {AngularProjectMulti, AngularProjectSingle} from './projects.mjs';
+
+const {values: args} = parseArgs({
+ options: {
+ testRunner: {
+ type: 'string',
+ short: 't',
+ default: undefined,
+ },
+ name: {
+ type: 'string',
+ short: 'n',
+ default: undefined,
+ },
+ },
+});
+
+if (process.env.CI) {
+ // Need to install in CI
+ execSync('npm install -g @angular/cli@latest @angular-devkit/schematics-cli');
+ const runners = ['node', 'jest', 'jasmine', 'mocha'];
+ const groups = [];
+
+ for (const runner of runners) {
+ groups.push([
+ new AngularProjectSingle(runner),
+ new AngularProjectMulti(runner),
+ ]);
+ }
+
+ const angularProjects = await Promise.allSettled(
+ groups.flat().map(async project => {
+ return await project.create();
+ })
+ );
+ ok(
+ angularProjects.every(project => {
+ return project.status === 'fulfilled';
+ }),
+ 'Building of 1 or more projects failed!'
+ );
+
+ for await (const runnerGroup of groups) {
+ const smokeResults = await Promise.allSettled(
+ runnerGroup.map(async project => {
+ return await project.runSmoke();
+ })
+ );
+ ok(
+ smokeResults.every(project => {
+ return project.status === 'fulfilled';
+ }),
+ `Smoke test for ${runnerGroup[0].runner} failed!`
+ );
+ }
+} else {
+ const single = new AngularProjectSingle(args.testRunner, args.name);
+ const multi = new AngularProjectMulti(args.testRunner, args.name);
+
+ await Promise.all([single.create(), multi.create()]);
+ await Promise.all([single.runSmoke(), multi.runSmoke()]);
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/tsconfig.json b/remote/test/puppeteer/packages/ng-schematics/tsconfig.json
new file mode 100644
index 0000000000..40529c7d17
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": "tsconfig",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "noEmitOnError": true,
+ "rootDir": "src/",
+ "outDir": "lib/",
+ "skipDefaultLibCheck": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "types": ["node"],
+ },
+ "include": ["src/**/*"],
+ "exclude": ["src/**/files/**/*"],
+}
diff --git a/remote/test/puppeteer/packages/ng-schematics/tsdoc.json b/remote/test/puppeteer/packages/ng-schematics/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/packages/ng-schematics/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/puppeteer-core/.gitignore b/remote/test/puppeteer/packages/puppeteer-core/.gitignore
new file mode 100644
index 0000000000..42061c01a1
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/.gitignore
@@ -0,0 +1 @@
+README.md \ No newline at end of file
diff --git a/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md b/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md
new file mode 100644
index 0000000000..341d706fb4
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md
@@ -0,0 +1,1926 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.0.1 to 1.1.0
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.4.4 to 1.4.5
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.5.1 to 1.6.0
+
+## [21.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.9.0...puppeteer-core-v21.10.0) (2024-01-29)
+
+
+### Features
+
+* add experimental browser.debugInfo ([#11748](https://github.com/puppeteer/puppeteer/issues/11748)) ([f88e1da](https://github.com/puppeteer/puppeteer/commit/f88e1da6385bc72e9ffde8514c28e4a0ff9e396a))
+* download chrome-headless-shell by default and use it for the old headless mode ([#11754](https://github.com/puppeteer/puppeteer/issues/11754)) ([ce894a2](https://github.com/puppeteer/puppeteer/commit/ce894a2ffce4bc44bd11f12d1f0543e003a97e02))
+
+
+### Bug Fixes
+
+* set viewport for element screenshots ([#11772](https://github.com/puppeteer/puppeteer/issues/11772)) ([9cd6673](https://github.com/puppeteer/puppeteer/commit/9cd66731d148afff9c2f873c1383fbe367cc5fb2))
+
+## [21.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.8.0...puppeteer-core-v21.9.0) (2024-01-24)
+
+
+### Features
+
+* roll to Chrome 121.0.6167.85 (r1233107) ([#11743](https://github.com/puppeteer/puppeteer/issues/11743)) ([0eec94c](https://github.com/puppeteer/puppeteer/commit/0eec94cf57288528ecd0a084a71311b181864f7b))
+
+## [21.8.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.7.0...puppeteer-core-v21.8.0) (2024-01-24)
+
+
+### Features
+
+* roll to Chrome 120.0.6099.109 (r1217362) ([#11733](https://github.com/puppeteer/puppeteer/issues/11733)) ([415cfac](https://github.com/puppeteer/puppeteer/commit/415cfaca202126b64ff496e4318cae64c4f14e89))
+
+
+### Bug Fixes
+
+* expose function for Firefox BiDi ([#11660](https://github.com/puppeteer/puppeteer/issues/11660)) ([cf879b8](https://github.com/puppeteer/puppeteer/commit/cf879b82f6c10302fcafe186b315fe7807107c31))
+* wait for WebDriver BiDi browser to close gracefully ([#11636](https://github.com/puppeteer/puppeteer/issues/11636)) ([cc3aeeb](https://github.com/puppeteer/puppeteer/commit/cc3aeeb6eae4663198466755f23746ef821408ae))
+
+
+### Reverts
+
+* refactor: adopt `core/UserContext` on `BidiBrowserContext` ([#11721](https://github.com/puppeteer/puppeteer/issues/11721)) ([d17a9df](https://github.com/puppeteer/puppeteer/commit/d17a9df0278be34c206701d8dfc1fb62af3637b3))
+
+## [21.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.6.1...puppeteer-core-v21.7.0) (2024-01-04)
+
+
+### Features
+
+* allow converting other targets to pages ([#11604](https://github.com/puppeteer/puppeteer/issues/11604)) ([66aa770](https://github.com/puppeteer/puppeteer/commit/66aa77003880a1458e14b47a3ed87856fd3a1933))
+* support fetching request POST data ([#11598](https://github.com/puppeteer/puppeteer/issues/11598)) ([80143de](https://github.com/puppeteer/puppeteer/commit/80143def9606ec5f2018dde618c00784442c5c1d))
+* support timeouts per CDP command ([#11595](https://github.com/puppeteer/puppeteer/issues/11595)) ([c660d40](https://github.com/puppeteer/puppeteer/commit/c660d4001d610854399d7ecb551c4eb56a7f840a))
+
+
+### Bug Fixes
+
+* change viewportHeight in screencast ([#11583](https://github.com/puppeteer/puppeteer/issues/11583)) ([107b833](https://github.com/puppeteer/puppeteer/commit/107b8337e5eebc5e31a57663ba1345be81fb486e))
+* 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))
+* improve reliability of exposeFunction ([#11600](https://github.com/puppeteer/puppeteer/issues/11600)) ([b0c5392](https://github.com/puppeteer/puppeteer/commit/b0c5392cb36eed2ed4ae4864587885b6059f4cfb))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.9.0 to 1.9.1
+
+## [21.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.6.0...puppeteer-core-v21.6.1) (2023-12-13)
+
+
+### Bug Fixes
+
+* emulate if captureBeyondViewport is false ([#11525](https://github.com/puppeteer/puppeteer/issues/11525)) ([b6d1163](https://github.com/puppeteer/puppeteer/commit/b6d1163f7f33d80fd43fa4915789d3689ea2369f))
+* ensure fission.bfcacheInParent is disabled for cdp in Firefox ([#11522](https://github.com/puppeteer/puppeteer/issues/11522)) ([b4a6524](https://github.com/puppeteer/puppeteer/commit/b4a65245b0ad01b2b634473ebb4d8bb2d7e420f7))
+
+## [21.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.5.2...puppeteer-core-v21.6.0) (2023-12-05)
+
+
+### Features
+
+* BiDi implementation of `Puppeteer.connect` for Firefox ([#11451](https://github.com/puppeteer/puppeteer/issues/11451)) ([be081ba](https://github.com/puppeteer/puppeteer/commit/be081ba17a9bbac70c13cafa81f1038f0ecfda70))
+* experimental WebDriver BiDi support with Firefox ([#11412](https://github.com/puppeteer/puppeteer/issues/11412)) ([8aba033](https://github.com/puppeteer/puppeteer/commit/8aba033dde1a306e37f6033d6f6ff36387e1aac3))
+* implement the Puppeteer CLI ([#11344](https://github.com/puppeteer/puppeteer/issues/11344)) ([53fb69b](https://github.com/puppeteer/puppeteer/commit/53fb69bf7f2bf06fa4fd7bb6d3cf21382386f6e7))
+
+
+### Bug Fixes
+
+* end WebDriver BiDi session on disconnect ([#11470](https://github.com/puppeteer/puppeteer/issues/11470)) ([a66d029](https://github.com/puppeteer/puppeteer/commit/a66d0296077a82179a2182281a5040fd96d3843c))
+* remove CDP-specific preferences from defaults for Firefox ([#11477](https://github.com/puppeteer/puppeteer/issues/11477)) ([f8c9469](https://github.com/puppeteer/puppeteer/commit/f8c94699c7f5b15c7bb96f299c2c8217d74230cd))
+* warn about launch Chrome using Node x64 on arm64 Macs ([#11471](https://github.com/puppeteer/puppeteer/issues/11471)) ([957a829](https://github.com/puppeteer/puppeteer/commit/957a8293bb1444fd51fd5673002a7781e8127c9d))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.8.0 to 1.9.0
+
+## [21.5.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.5.1...puppeteer-core-v21.5.2) (2023-11-15)
+
+
+### Bug Fixes
+
+* add --disable-field-trial-config ([#11352](https://github.com/puppeteer/puppeteer/issues/11352)) ([cbc33be](https://github.com/puppeteer/puppeteer/commit/cbc33bea40b8801b8eeb3277fc15d04900715795))
+* add --disable-infobars ([#11377](https://github.com/puppeteer/puppeteer/issues/11377)) ([0a41f8d](https://github.com/puppeteer/puppeteer/commit/0a41f8d01e85ff732fdd2e50468bc746d7bc6475))
+* mitt types should not be exported ([#11371](https://github.com/puppeteer/puppeteer/issues/11371)) ([4bf2a09](https://github.com/puppeteer/puppeteer/commit/4bf2a09a13450c530b24288d65791fd5c4d4dce7))
+
+## [21.5.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.5.0...puppeteer-core-v21.5.1) (2023-11-09)
+
+
+### Bug Fixes
+
+* better debugging for WaitTask ([#11330](https://github.com/puppeteer/puppeteer/issues/11330)) ([d2480b0](https://github.com/puppeteer/puppeteer/commit/d2480b022d74b7071b515408a31c6e82448e3c9e))
+
+## [21.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.4.1...puppeteer-core-v21.5.0) (2023-11-02)
+
+
+### Features
+
+* roll to Chrome 119.0.6045.105 (r1204232) ([#11287](https://github.com/puppeteer/puppeteer/issues/11287)) ([325fa8b](https://github.com/puppeteer/puppeteer/commit/325fa8b1b16a9dafd5bb320e49984d24044fa3d7))
+
+
+### Bug Fixes
+
+* ignore unordered frames ([#11283](https://github.com/puppeteer/puppeteer/issues/11283)) ([ce4e485](https://github.com/puppeteer/puppeteer/commit/ce4e485d1b1e9d4e223890ee0fc2475a1ad71bc3))
+* Type for ElementHandle.screenshot ([#11274](https://github.com/puppeteer/puppeteer/issues/11274)) ([22aeff1](https://github.com/puppeteer/puppeteer/commit/22aeff1eac9d22048330a16aa3c41293133911e4))
+
+## [21.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.4.0...puppeteer-core-v21.4.1) (2023-10-23)
+
+
+### Bug Fixes
+
+* do not pass --{enable,disable}-features twice when user-provided ([#11230](https://github.com/puppeteer/puppeteer/issues/11230)) ([edec7d5](https://github.com/puppeteer/puppeteer/commit/edec7d53f8190381ade7db145ad7e7d6dba2ee13))
+* remove circular import in IsolatedWorld ([#11228](https://github.com/puppeteer/puppeteer/issues/11228)) ([3edce3a](https://github.com/puppeteer/puppeteer/commit/3edce3aee9521654d7a285f4068a5e60bfb52245))
+* remove import cycle ([#11227](https://github.com/puppeteer/puppeteer/issues/11227)) ([525f13c](https://github.com/puppeteer/puppeteer/commit/525f13cd18b39cc951a84aa51b2d852758e6f0d2))
+* remove import cycle in connection ([#11225](https://github.com/puppeteer/puppeteer/issues/11225)) ([60f1b78](https://github.com/puppeteer/puppeteer/commit/60f1b788a6304504f504b0be9f02cb768e2803f8))
+* remove import cycle in query handlers ([#11234](https://github.com/puppeteer/puppeteer/issues/11234)) ([954c75f](https://github.com/puppeteer/puppeteer/commit/954c75f9a9879e2e68935c17d7eb777b1f9f808a))
+* remove more import cycles ([#11231](https://github.com/puppeteer/puppeteer/issues/11231)) ([b9ce89e](https://github.com/puppeteer/puppeteer/commit/b9ce89e460702ad85314685c600a4e5267f4db9b))
+* typo in screencast error message ([#11213](https://github.com/puppeteer/puppeteer/issues/11213)) ([25b90b2](https://github.com/puppeteer/puppeteer/commit/25b90b2b542c4693150b67dc0c690b99f4ccfc95))
+
+## [21.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.8...puppeteer-core-v21.4.0) (2023-10-20)
+
+
+### Features
+
+* added tagged (accessible) PDFs option ([#11182](https://github.com/puppeteer/puppeteer/issues/11182)) ([0316863](https://github.com/puppeteer/puppeteer/commit/031686339136873c555a19ffb871f7140a2c39d9))
+* enable tab targets ([#11099](https://github.com/puppeteer/puppeteer/issues/11099)) ([8324c16](https://github.com/puppeteer/puppeteer/commit/8324c1634883d97ed83f32a1e62acc9b5e64e0bd))
+* implement screencasting ([#11084](https://github.com/puppeteer/puppeteer/issues/11084)) ([f060d46](https://github.com/puppeteer/puppeteer/commit/f060d467c00457e6be6878e0789d0df2ac4aae50))
+* merge user-provided --{disable,enable}-features in args ([#11152](https://github.com/puppeteer/puppeteer/issues/11152)) ([2b578e4](https://github.com/puppeteer/puppeteer/commit/2b578e4a096aa94d792cc2da2da41fee061a77b8)), closes [#11072](https://github.com/puppeteer/puppeteer/issues/11072)
+* roll to Chrome 118.0.5993.70 (r1192594) ([#11123](https://github.com/puppeteer/puppeteer/issues/11123)) ([91d14c8](https://github.com/puppeteer/puppeteer/commit/91d14c8c86f5be48c8e0937fd209bea643d60b45))
+
+
+### Bug Fixes
+
+* `Page.waitForDevicePrompt` crash ([#11153](https://github.com/puppeteer/puppeteer/issues/11153)) ([257be15](https://github.com/puppeteer/puppeteer/commit/257be15d83a46038a65d47977d4d847c54506517))
+* add InlineTextBox as a non-element a11y role ([#11142](https://github.com/puppeteer/puppeteer/issues/11142)) ([8aa6cb3](https://github.com/puppeteer/puppeteer/commit/8aa6cb37d2443ff7fe2a1fd5d5adafdde4e9d165))
+* disable ProcessPerSiteUpToMainFrameThreshold in Chrome ([#11139](https://github.com/puppeteer/puppeteer/issues/11139)) ([9347aae](https://github.com/puppeteer/puppeteer/commit/9347aae12e996604cea871acc9d007cbf338542e))
+* make sure discovery happens before auto-attach ([#11100](https://github.com/puppeteer/puppeteer/issues/11100)) ([9ce204e](https://github.com/puppeteer/puppeteer/commit/9ce204e27ed091bde5aa5bc9f82da41c80534bde))
+* synchronize frame tree with the events processing ([#11112](https://github.com/puppeteer/puppeteer/issues/11112)) ([d63f0cf](https://github.com/puppeteer/puppeteer/commit/d63f0cfc61e8ba2233eee8b2f3b99d8619a0acaf))
+* update TextQuerySelector cache on subtree update ([#11200](https://github.com/puppeteer/puppeteer/issues/11200)) ([4206e76](https://github.com/puppeteer/puppeteer/commit/4206e76c3e4647ea6290f16127764d1a2f337dcf))
+* xpath queries should be atomic ([#11101](https://github.com/puppeteer/puppeteer/issues/11101)) ([6098bab](https://github.com/puppeteer/puppeteer/commit/6098bab2ba68276c85a974e17c9fe3bdac8c4c58))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.7.1 to 1.8.0
+
+## [21.3.8](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.7...puppeteer-core-v21.3.8) (2023-10-06)
+
+
+### Bug Fixes
+
+* avoid double subscription to frame manager in Page ([#11091](https://github.com/puppeteer/puppeteer/issues/11091)) ([5887649](https://github.com/puppeteer/puppeteer/commit/5887649891ea9cf1d7b3afbcf7196620ceb20ab2))
+* update file chooser events ([#11057](https://github.com/puppeteer/puppeteer/issues/11057)) ([317f820](https://github.com/puppeteer/puppeteer/commit/317f82055b2f4dd68db136a3d52c5712425fa339))
+
+## [21.3.7](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.6...puppeteer-core-v21.3.7) (2023-10-05)
+
+
+### Bug Fixes
+
+* roll to Chrome 117.0.5938.149 (r1181205) ([#11077](https://github.com/puppeteer/puppeteer/issues/11077)) ([0c0e516](https://github.com/puppeteer/puppeteer/commit/0c0e516d736665a27f7773f66a0f9c362daa73aa))
+
+## [21.3.6](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.5...puppeteer-core-v21.3.6) (2023-09-28)
+
+
+### Bug Fixes
+
+* remove the flag disabling bfcache ([#11047](https://github.com/puppeteer/puppeteer/issues/11047)) ([b0d7375](https://github.com/puppeteer/puppeteer/commit/b0d73755193e7c60deb70df120859b5db87e7817))
+
+## [21.3.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.4...puppeteer-core-v21.3.5) (2023-09-26)
+
+
+### Bug Fixes
+
+* set defaults in screenshot ([#11021](https://github.com/puppeteer/puppeteer/issues/11021)) ([ace1230](https://github.com/puppeteer/puppeteer/commit/ace1230e41aad6168dc85b9bc1f7c04d9dce5527))
+
+## [21.3.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.3...puppeteer-core-v21.3.4) (2023-09-22)
+
+
+### Bug Fixes
+
+* avoid structuredClone for Node 16 ([#11006](https://github.com/puppeteer/puppeteer/issues/11006)) ([25eca9a](https://github.com/puppeteer/puppeteer/commit/25eca9a747c122b3096b0f2d01b3323339d57dd9))
+
+## [21.3.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.2...puppeteer-core-v21.3.3) (2023-09-22)
+
+
+### Bug Fixes
+
+* do not export bidi and fix import from the entrypoint ([#10998](https://github.com/puppeteer/puppeteer/issues/10998)) ([88c78de](https://github.com/puppeteer/puppeteer/commit/88c78dea41eb7690d67343298c150194fe145763))
+
+## [21.3.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.1...puppeteer-core-v21.3.2) (2023-09-22)
+
+
+### Bug Fixes
+
+* handle missing detach events for restored bfcache targets ([#10967](https://github.com/puppeteer/puppeteer/issues/10967)) ([7bcdfcb](https://github.com/puppeteer/puppeteer/commit/7bcdfcb7e9e75feca0a8de692926ea25ca8fbed0))
+* roll to Chrome 117.0.5938.92 (r1181205) ([#10989](https://github.com/puppeteer/puppeteer/issues/10989)) ([d048cd9](https://github.com/puppeteer/puppeteer/commit/d048cd965f0707dd9b2a3276f02c563b69f6fac4))
+
+## [21.3.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.0...puppeteer-core-v21.3.1) (2023-09-19)
+
+
+### Bug Fixes
+
+* make `CDPSessionEvent.SessionAttached` public ([#10941](https://github.com/puppeteer/puppeteer/issues/10941)) ([cfed7b9](https://github.com/puppeteer/puppeteer/commit/cfed7b93ec23e92ec11632f1cd90f00dac754739))
+
+## [21.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.2.1...puppeteer-core-v21.3.0) (2023-09-19)
+
+
+### Features
+
+* implement `Browser.connected` ([#10927](https://github.com/puppeteer/puppeteer/issues/10927)) ([a4345a4](https://github.com/puppeteer/puppeteer/commit/a4345a477f58541f5d95da11ffee74abe24c12bf))
+* implement `BrowserContext.closed` ([#10928](https://github.com/puppeteer/puppeteer/issues/10928)) ([2292078](https://github.com/puppeteer/puppeteer/commit/2292078969fa46a27d5759989cd44a4d48beb310))
+* implement improved Drag n' Drop APIs ([#10651](https://github.com/puppeteer/puppeteer/issues/10651)) ([9342bac](https://github.com/puppeteer/puppeteer/commit/9342bac2639702090f39fc1e3a97d43a934f3f0b))
+* implement typed events ([#10889](https://github.com/puppeteer/puppeteer/issues/10889)) ([9b6f1de](https://github.com/puppeteer/puppeteer/commit/9b6f1de8b99445c661c5aebcf041fe90daf469b9))
+* roll to Chrome 117.0.5938.62 (r1181205) ([#10893](https://github.com/puppeteer/puppeteer/issues/10893)) ([4b8d20d](https://github.com/puppeteer/puppeteer/commit/4b8d20d0edeccaa3028e0c1c0b63c022cfabcee2))
+
+
+### Bug Fixes
+
+* fix line/column number in errors ([#10926](https://github.com/puppeteer/puppeteer/issues/10926)) ([a0e57f7](https://github.com/puppeteer/puppeteer/commit/a0e57f7eb230ba6a659c2d418da8d3f67add2d00))
+* handle frame manager init without unhandled rejection ([#10902](https://github.com/puppeteer/puppeteer/issues/10902)) ([ea14834](https://github.com/puppeteer/puppeteer/commit/ea14834fdf1c7c1afa45bdd1fb5339380f4631a2))
+* remove explicit resource management from types ([#10918](https://github.com/puppeteer/puppeteer/issues/10918)) ([a1b1bff](https://github.com/puppeteer/puppeteer/commit/a1b1bffb7258f1dec3b0a2e9ce068baf2cc3db19))
+* roll to Chrome 117.0.5938.88 (r1181205) ([#10920](https://github.com/puppeteer/puppeteer/issues/10920)) ([b7bcc9a](https://github.com/puppeteer/puppeteer/commit/b7bcc9a733a3ac376397a32c3f62eb68101bedf9))
+
+## [21.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.2.0...puppeteer-core-v21.2.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))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.7.0 to 1.7.1
+
+## [21.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.1.1...puppeteer-core-v21.2.0) (2023-09-12)
+
+
+### Features
+
+* expose DevTools as a target ([#10812](https://github.com/puppeteer/puppeteer/issues/10812)) ([a540085](https://github.com/puppeteer/puppeteer/commit/a540085176d92bd160a12ebc54606dbacd064979))
+
+
+### Bug Fixes
+
+* add --disable-search-engine-choice-screen to default arguments ([#10880](https://github.com/puppeteer/puppeteer/issues/10880)) ([d08ad5f](https://github.com/puppeteer/puppeteer/commit/d08ad5fbbe3be4349dd6132c209895f8436ae9e6))
+* apply viewport emulation to prerender targets ([#10804](https://github.com/puppeteer/puppeteer/issues/10804)) ([14f0ab7](https://github.com/puppeteer/puppeteer/commit/14f0ab7397053db5591823c716e142c684f25b44))
+* implement `throwIfDetached` ([#10826](https://github.com/puppeteer/puppeteer/issues/10826)) ([538bb73](https://github.com/puppeteer/puppeteer/commit/538bb73ea7e280cacf15fc1d2100251d8e17f906))
+* LifecycleWatcher sub frames handling ([#10841](https://github.com/puppeteer/puppeteer/issues/10841)) ([06c1588](https://github.com/puppeteer/puppeteer/commit/06c1588016e1ebef5ed8f079dc34507f6d781e07))
+* make network manager multi session ([#10793](https://github.com/puppeteer/puppeteer/issues/10793)) ([085936b](https://github.com/puppeteer/puppeteer/commit/085936bd7e17ed5a8085311f5b212c7b9ca96a0d))
+* make page.goBack work with bfcache in tab mode ([#10818](https://github.com/puppeteer/puppeteer/issues/10818)) ([22daf18](https://github.com/puppeteer/puppeteer/commit/22daf1861fc358acf4d84c360049736c22249f92))
+* only a single disable features flag is allowed ([#10887](https://github.com/puppeteer/puppeteer/issues/10887)) ([4852e22](https://github.com/puppeteer/puppeteer/commit/4852e222b771ed9b95596657f70e45c1d5b9790d))
+* trimCache should remove Firefox too ([#10872](https://github.com/puppeteer/puppeteer/issues/10872)) ([acdd7d3](https://github.com/puppeteer/puppeteer/commit/acdd7d3cd5529bc934edbb8479bdb950cc7d8a6a))
+
+## [21.1.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.1.0...puppeteer-core-v21.1.1) (2023-08-28)
+
+
+### Bug Fixes
+
+* **locators:** do not retry via catchError ([#10762](https://github.com/puppeteer/puppeteer/issues/10762)) ([8f9388f](https://github.com/puppeteer/puppeteer/commit/8f9388f2ce5220ad9b3c05fb3f3d9a86fac894dc))
+
+## [21.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.0.3...puppeteer-core-v21.1.0) (2023-08-18)
+
+
+### Features
+
+* roll to Chrome 116.0.5845.96 (r1160321) ([#10735](https://github.com/puppeteer/puppeteer/issues/10735)) ([e12b558](https://github.com/puppeteer/puppeteer/commit/e12b558f505aab13f38030a7b748261bdeadc48b))
+
+
+### Bug Fixes
+
+* locator.fill should work for textareas ([#10737](https://github.com/puppeteer/puppeteer/issues/10737)) ([fc08a7d](https://github.com/puppeteer/puppeteer/commit/fc08a7dd54226878300f3a4b52fb16aeb5cc93e8))
+* relative ordering of events and command responses should be ensured ([#10725](https://github.com/puppeteer/puppeteer/issues/10725)) ([81ecb60](https://github.com/puppeteer/puppeteer/commit/81ecb60190f89389abb6d8834158f38ff7317ec8))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.6.0 to 1.7.0
+
+## [21.0.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.0.1...puppeteer-core-v21.0.2) (2023-08-08)
+
+
+### Bug Fixes
+
+* destroy puppeteer utility on context destruction ([#10672](https://github.com/puppeteer/puppeteer/issues/10672)) ([8b8770c](https://github.com/puppeteer/puppeteer/commit/8b8770c004ba842496e0ca4845642fe82a211051))
+* roll to Chrome 115.0.5790.170 (r1148114) ([#10677](https://github.com/puppeteer/puppeteer/issues/10677)) ([e5af57e](https://github.com/puppeteer/puppeteer/commit/e5af57ebd0187c296bc44426c1b931f57442732e))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.5.0 to 1.5.1
+
+## [21.0.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.0.0...puppeteer-core-v21.0.1) (2023-08-03)
+
+
+### Bug Fixes
+
+* use handle frame instead of page ([#10676](https://github.com/puppeteer/puppeteer/issues/10676)) ([1b44b91](https://github.com/puppeteer/puppeteer/commit/1b44b911d3633df89bd6106aaf7accb49230934d))
+
+## [21.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.9.0...puppeteer-core-v21.0.0) (2023-08-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* use Target for filters ([#10601](https://github.com/puppeteer/puppeteer/issues/10601))
+
+### Features
+
+* add page.createCDPSession method ([#10515](https://github.com/puppeteer/puppeteer/issues/10515)) ([d0c5b8e](https://github.com/puppeteer/puppeteer/commit/d0c5b8e08905f3802705a1a90d7cc8fa04bc82db))
+* implement `Locator.prototype.filter` ([#10631](https://github.com/puppeteer/puppeteer/issues/10631)) ([e73d35d](https://github.com/puppeteer/puppeteer/commit/e73d35def0718468fe854ac2ef5f4a8beafb2fb3))
+* implement `Locator.prototype.map` ([#10630](https://github.com/puppeteer/puppeteer/issues/10630)) ([47eecf5](https://github.com/puppeteer/puppeteer/commit/47eecf5bb11daba0114ad04282beb01c85eb9405))
+* implement `Locator.prototype.wait` ([#10629](https://github.com/puppeteer/puppeteer/issues/10629)) ([5d34d42](https://github.com/puppeteer/puppeteer/commit/5d34d42d1536cbe7cf2ba1aa8670d909c4e6a6fc))
+* implement `Locator.prototype.waitHandle` ([#10650](https://github.com/puppeteer/puppeteer/issues/10650)) ([fdada74](https://github.com/puppeteer/puppeteer/commit/fdada74ba7265b3571ebdf60ae301b64d13a8226))
+* implement function locators ([#10632](https://github.com/puppeteer/puppeteer/issues/10632)) ([6ad92f7](https://github.com/puppeteer/puppeteer/commit/6ad92f7f84f477b22674f52f0a145a500c3aa152))
+* implement immutable locator operations ([#10638](https://github.com/puppeteer/puppeteer/issues/10638)) ([34be28d](https://github.com/puppeteer/puppeteer/commit/34be28db5d9971cf16d9741b0141357df3cbf74c))
+
+
+### Bug Fixes
+
+* remove typescript from peer dependencies ([#10593](https://github.com/puppeteer/puppeteer/issues/10593)) ([c60572a](https://github.com/puppeteer/puppeteer/commit/c60572a1ca36ea5946d287bd629ac31798d84cb0))
+* roll to Chrome 115.0.5790.102 (r1148114) ([#10608](https://github.com/puppeteer/puppeteer/issues/10608)) ([8649c53](https://github.com/puppeteer/puppeteer/commit/8649c53a706e5a09ae5e16849eb29a793cec5bec))
+
+
+### Code Refactoring
+
+* use Target for filters ([#10601](https://github.com/puppeteer/puppeteer/issues/10601)) ([44712d1](https://github.com/puppeteer/puppeteer/commit/44712d1e6efcb3fa49c27b1195d17c0c1c92a0ca))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.4.6 to 1.5.0
+
+## [20.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.8.3...puppeteer-core-v20.9.0) (2023-07-20)
+
+
+### Features
+
+* add autofill support ([#10565](https://github.com/puppeteer/puppeteer/issues/10565)) ([6c9306a](https://github.com/puppeteer/puppeteer/commit/6c9306a72e0f7195a4a6c300645f6089845c9abc))
+* roll to Chrome 115.0.5790.98 (r1148114) ([#10584](https://github.com/puppeteer/puppeteer/issues/10584)) ([830f926](https://github.com/puppeteer/puppeteer/commit/830f926d486675701720b5c147f597364f3e8f7b))
+
+
+### Bug Fixes
+
+* update the target to ES2022 ([#10574](https://github.com/puppeteer/puppeteer/issues/10574)) ([88439f9](https://github.com/puppeteer/puppeteer/commit/88439f913ed4159cdc8be573f2dbda0b1f615301))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.4.5 to 1.4.6
+
+## [20.8.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.8.2...puppeteer-core-v20.8.3) (2023-07-18)
+
+
+### Bug Fixes
+
+* **locators:** reject the race if there are only failures ([#10567](https://github.com/puppeteer/puppeteer/issues/10567)) ([e3dd596](https://github.com/puppeteer/puppeteer/commit/e3dd5968cae196b64d958c161fed3d1b39aed3f6))
+* prevent erroneous new main frame ([#10549](https://github.com/puppeteer/puppeteer/issues/10549)) ([cb46413](https://github.com/puppeteer/puppeteer/commit/cb46413d87f10970f4088b7d58e02a65c5ccd27e))
+
+## [20.8.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.8.0...puppeteer-core-v20.8.1) (2023-07-11)
+
+
+### Bug Fixes
+
+* remove test metadata files ([#10520](https://github.com/puppeteer/puppeteer/issues/10520)) ([cbf4f2a](https://github.com/puppeteer/puppeteer/commit/cbf4f2a66912f24849ae8c88fc1423851dcc4aa7))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.4.3 to 1.4.4
+
+## [20.8.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.7.4...puppeteer-core-v20.8.0) (2023-07-06)
+
+
+### Features
+
+* **screenshot:** enable optimizeForSpeed ([#10492](https://github.com/puppeteer/puppeteer/issues/10492)) ([87aaed4](https://github.com/puppeteer/puppeteer/commit/87aaed4807e5240dec7b25273e44c1ce5e884336))
+
+
+### Bug Fixes
+
+* add an internal page.locatorRace ([#10512](https://github.com/puppeteer/puppeteer/issues/10512)) ([56a97dd](https://github.com/puppeteer/puppeteer/commit/56a97dd2fb1cbf36e4f3344f7d22afd6e7ef2380))
+
+## [20.7.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.7.3...puppeteer-core-v20.7.4) (2023-06-29)
+
+
+### Bug Fixes
+
+* fix escaping algo for P selectors ([#10474](https://github.com/puppeteer/puppeteer/issues/10474)) ([84a956f](https://github.com/puppeteer/puppeteer/commit/84a956f56ba9ce74e9dd0f95ff40fdd14be87b1d))
+* fix the util import in Connection.ts ([#10450](https://github.com/puppeteer/puppeteer/issues/10450)) ([61f4525](https://github.com/puppeteer/puppeteer/commit/61f4525ae306810404af9083d2e7440403c02722))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.4.2 to 1.4.3
+
+## [20.7.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.7.2...puppeteer-core-v20.7.3) (2023-06-20)
+
+
+### Bug Fixes
+
+* add parenthesis to JS values in interpolateFunction ([#10426](https://github.com/puppeteer/puppeteer/issues/10426)) ([fbdcc0d](https://github.com/puppeteer/puppeteer/commit/fbdcc0d6469abe7115723347a9f161628074d41e))
+* added clipboard permission that was not exposed ([#10119](https://github.com/puppeteer/puppeteer/issues/10119)) ([c06e15f](https://github.com/puppeteer/puppeteer/commit/c06e15fb5bd7ec21db2d883ccf63ef8fe98c7f4d))
+* include src into published package ([#10415](https://github.com/puppeteer/puppeteer/issues/10415)) ([d1ffad0](https://github.com/puppeteer/puppeteer/commit/d1ffad059ae66104842b92dc814d362c123b9646))
+* WaitForNetworkIdle and Deferred.race ([#10411](https://github.com/puppeteer/puppeteer/issues/10411)) ([138cc5c](https://github.com/puppeteer/puppeteer/commit/138cc5c961da698bf7ca635c9947058df4b2ec72))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.4.1 to 1.4.2
+
+## [20.7.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.7.1...puppeteer-core-v20.7.2) (2023-06-16)
+
+
+### Bug Fixes
+
+* roll to Chrome 114.0.5735.133 (r1135570) ([#10384](https://github.com/puppeteer/puppeteer/issues/10384)) ([9311558](https://github.com/puppeteer/puppeteer/commit/93115587c94278e0a5309429d3f23a52ed24e22d))
+
+## [20.7.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.7.0...puppeteer-core-v20.7.1) (2023-06-13)
+
+
+### Bug Fixes
+
+* avoid importing puppeteer-core.js ([#10376](https://github.com/puppeteer/puppeteer/issues/10376)) ([3171c12](https://github.com/puppeteer/puppeteer/commit/3171c12a0c16b283e6b65b1ed3d801b089a6e28b))
+
+## [20.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.6.0...puppeteer-core-v20.7.0) (2023-06-13)
+
+
+### Features
+
+* add `reset` to mouse ([#10340](https://github.com/puppeteer/puppeteer/issues/10340)) ([35aedc0](https://github.com/puppeteer/puppeteer/commit/35aedc0dbbd80818e6f83ff9f0777dc3ea2588f0))
+
+
+### Bug Fixes
+
+* Locator.scroll in race ([#10363](https://github.com/puppeteer/puppeteer/issues/10363)) ([ba28724](https://github.com/puppeteer/puppeteer/commit/ba28724952b41ea653830a75efc4c73b234ea354))
+* mark CDPSessionOnMessageObject as internal ([#10373](https://github.com/puppeteer/puppeteer/issues/10373)) ([7cb6059](https://github.com/puppeteer/puppeteer/commit/7cb6059bcc36f8dc3739a8df9119c658146ac100))
+* specify the context id when adding bindings ([#10366](https://github.com/puppeteer/puppeteer/issues/10366)) ([c2d3488](https://github.com/puppeteer/puppeteer/commit/c2d3488ad8c0453312557ba28e6ade9c32464f17))
+
+## [20.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.5.0...puppeteer-core-v20.6.0) (2023-06-09)
+
+
+### Features
+
+* add `page.removeExposedFunction` ([#10297](https://github.com/puppeteer/puppeteer/issues/10297)) ([4d0dbbc](https://github.com/puppeteer/puppeteer/commit/4d0dbbc517f388a3fe984ec569bc1bad28d91494))
+* **chrome:** roll to Chrome 114.0.5735.45 (r1135570) ([#10302](https://github.com/puppeteer/puppeteer/issues/10302)) ([021402d](https://github.com/puppeteer/puppeteer/commit/021402d1363accabc05f75ea1004451a90e1dfca))
+* implement Locator.race ([#10337](https://github.com/puppeteer/puppeteer/issues/10337)) ([9c35e9a](https://github.com/puppeteer/puppeteer/commit/9c35e9ab1f92e99aab8dabcd17f687befd6aad81))
+* implement Locators ([#10305](https://github.com/puppeteer/puppeteer/issues/10305)) ([1f978f5](https://github.com/puppeteer/puppeteer/commit/1f978f5fc5f0580859ad423e952595979f50d5a9))
+
+
+### Bug Fixes
+
+* content() not showing comments outside html tag ([#10293](https://github.com/puppeteer/puppeteer/issues/10293)) ([9abd48a](https://github.com/puppeteer/puppeteer/commit/9abd48a062a4a30fb93d0b555f2fa03d3dc410f3))
+* ensure stack trace contains one line ([#10317](https://github.com/puppeteer/puppeteer/issues/10317)) ([bc0b04b](https://github.com/puppeteer/puppeteer/commit/bc0b04beef3244280e6569a233173d512adaa9d8))
+* roll to Chrome 114.0.5735.90 (r1135570) ([#10329](https://github.com/puppeteer/puppeteer/issues/10329)) ([60acefc](https://github.com/puppeteer/puppeteer/commit/60acefc1d6d719ed6c5053d6b9ad734306d08c4a))
+* send capabilities property in session.new command ([#10311](https://github.com/puppeteer/puppeteer/issues/10311)) ([e8d044c](https://github.com/puppeteer/puppeteer/commit/e8d044cb8dcb689cc066ffa18a1e3c9366f57902))
+
+## [20.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.4.0...puppeteer-core-v20.5.0) (2023-05-31)
+
+
+### Features
+
+* Page.removeScriptToEvaluateOnNewDocument ([#10250](https://github.com/puppeteer/puppeteer/issues/10250)) ([b5a124f](https://github.com/puppeteer/puppeteer/commit/b5a124ff738a03fa7eb5755b441af5b773447449))
+
+
+### Bug Fixes
+
+* bind trimCache to the instance ([#10270](https://github.com/puppeteer/puppeteer/issues/10270)) ([50e72a4](https://github.com/puppeteer/puppeteer/commit/50e72a4d1164af7d53e31b8b83117f695ede7ae4))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.4.0 to 1.4.1
+
+## [20.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.3.0...puppeteer-core-v20.4.0) (2023-05-24)
+
+
+### Features
+
+* Page.setBypassServiceWorker ([#10229](https://github.com/puppeteer/puppeteer/issues/10229)) ([81f73a5](https://github.com/puppeteer/puppeteer/commit/81f73a55f31892e55219ef9d37e235e988731fc1))
+
+
+### Bug Fixes
+
+* stacktraces should not throw errors ([#10231](https://github.com/puppeteer/puppeteer/issues/10231)) ([557ec24](https://github.com/puppeteer/puppeteer/commit/557ec24cfc084440197da67581bf9782f10eb346))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.3.0 to 1.4.0
+
+## [20.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.2.1...puppeteer-core-v20.3.0) (2023-05-22)
+
+
+### Features
+
+* add an ability to trim cache for Puppeteer ([#10199](https://github.com/puppeteer/puppeteer/issues/10199)) ([1ad32ec](https://github.com/puppeteer/puppeteer/commit/1ad32ec9948ca3e07e15548a562c8f3c633b3dc3))
+
+
+### Bug Fixes
+
+* ElementHandle dragAndDrop should fail when interception is disabled ([#10209](https://github.com/puppeteer/puppeteer/issues/10209)) ([bcf5fd8](https://github.com/puppeteer/puppeteer/commit/bcf5fd87aeeb822203c3388e8aa6dadaa0107690))
+
+## [20.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.2.0...puppeteer-core-v20.2.1) (2023-05-15)
+
+
+### Bug Fixes
+
+* use encode/decodeURIComponent ([#10183](https://github.com/puppeteer/puppeteer/issues/10183)) ([d0c68ff](https://github.com/puppeteer/puppeteer/commit/d0c68ff002df37907968d3b999a8273590ac7c97))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.2.0 to 1.3.0
+
+## [20.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.1.2...puppeteer-core-v20.2.0) (2023-05-11)
+
+
+### Features
+
+* implement detailed errors for evaluation ([#10114](https://github.com/puppeteer/puppeteer/issues/10114)) ([317fa73](https://github.com/puppeteer/puppeteer/commit/317fa732f920382f9b3f6dea4e31ed31b04e25da))
+
+
+### Bug Fixes
+
+* downloadPath should be used by the install script ([#10163](https://github.com/puppeteer/puppeteer/issues/10163)) ([4398f66](https://github.com/puppeteer/puppeteer/commit/4398f66f281f1ffe5be81b529fc4751edfaf761d))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.1.0 to 1.2.0
+
+## [20.1.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.1.0...puppeteer-core-v20.1.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))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 1.0.0 to 1.0.1
+
+## [20.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.0.0...puppeteer-core-v20.1.0) (2023-05-03)
+
+
+### Features
+
+* **chrome:** roll to Chrome 113.0.5672.63 (r1121455) ([#10116](https://github.com/puppeteer/puppeteer/issues/10116)) ([19f4334](https://github.com/puppeteer/puppeteer/commit/19f43348a884edfc3e73ab60e41a9757239df013))
+
+## [20.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.11.1...puppeteer-core-v20.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
+
+* add AbortSignal to waitForFunction ([#10078](https://github.com/puppeteer/puppeteer/issues/10078)) ([4dd4cb9](https://github.com/puppeteer/puppeteer/commit/4dd4cb929242a6b1a621fd461edd3167d40e1c4c))
+* 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
+
+* use AbortSignal.throwIfAborted ([#10105](https://github.com/puppeteer/puppeteer/issues/10105)) ([575f00a](https://github.com/puppeteer/puppeteer/commit/575f00a31d0278f7ff27096e770ff84399cd9993))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 0.5.0 to 1.0.0
+
+## [19.11.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.11.0...puppeteer-core-v19.11.1) (2023-04-25)
+
+
+### Bug Fixes
+
+* implement click `count` ([#10069](https://github.com/puppeteer/puppeteer/issues/10069)) ([8124a7d](https://github.com/puppeteer/puppeteer/commit/8124a7d5bfc1cfa8cb579271f78ce586efc62b8e))
+* implement flag for disabling headless warning ([#10073](https://github.com/puppeteer/puppeteer/issues/10073)) ([cfe9bbc](https://github.com/puppeteer/puppeteer/commit/cfe9bbc852d014b31c754950590b6b6c96573eeb))
+
+## [19.11.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.10.1...puppeteer-core-v19.11.0) (2023-04-24)
+
+
+### Features
+
+* add warn for `headless: true` ([#10039](https://github.com/puppeteer/puppeteer/issues/10039)) ([23d6a95](https://github.com/puppeteer/puppeteer/commit/23d6a95cf10c90f8aba2b12d7b02a73072e20382))
+
+
+### Bug Fixes
+
+* infer last pressed button in mouse move ([#10067](https://github.com/puppeteer/puppeteer/issues/10067)) ([a6eaac4](https://github.com/puppeteer/puppeteer/commit/a6eaac4c39d4b0ab3ab1a3c2f319a70fde393edb))
+
+## [19.10.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.10.0...puppeteer-core-v19.10.1) (2023-04-21)
+
+
+### Bug Fixes
+
+* move fs.js to the node folder ([#10055](https://github.com/puppeteer/puppeteer/issues/10055)) ([704624e](https://github.com/puppeteer/puppeteer/commit/704624eb2045a7e38ed14044d6863a2871e9d7e2))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 0.4.1 to 0.5.0
+
+## [19.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.9.1...puppeteer-core-v19.10.0) (2023-04-20)
+
+
+### Features
+
+* support AbortController in waitForSelector ([#10018](https://github.com/puppeteer/puppeteer/issues/10018)) ([9109b76](https://github.com/puppeteer/puppeteer/commit/9109b76276c9d86a2c521c72fc5b7189979279ca))
+* **webworker:** expose WebWorker.client ([#10042](https://github.com/puppeteer/puppeteer/issues/10042)) ([c125128](https://github.com/puppeteer/puppeteer/commit/c12512822a546e7bfdefd2c68f020aab2a308f4f))
+
+
+### Bug Fixes
+
+* continue requests without network instrumentation ([#10046](https://github.com/puppeteer/puppeteer/issues/10046)) ([8283823](https://github.com/puppeteer/puppeteer/commit/8283823cb860528a938e84cb5ba2b5f4cf980e83))
+* install bindings once ([#10049](https://github.com/puppeteer/puppeteer/issues/10049)) ([690aec1](https://github.com/puppeteer/puppeteer/commit/690aec1b5cb4e7e574abde9c533c6c0954e6f1aa))
+
+## [19.9.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.9.0...puppeteer-core-v19.9.1) (2023-04-17)
+
+
+### Bug Fixes
+
+* improve mouse actions ([#10021](https://github.com/puppeteer/puppeteer/issues/10021)) ([34db39e](https://github.com/puppeteer/puppeteer/commit/34db39e4474efee9d4579743026c3d6b6c8e494b))
+
+## [19.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.5...puppeteer-core-v19.9.0) (2023-04-13)
+
+
+### Features
+
+* add ElementHandle.isVisible and ElementHandle.isHidden ([#10007](https://github.com/puppeteer/puppeteer/issues/10007)) ([26c81b7](https://github.com/puppeteer/puppeteer/commit/26c81b7408a98cb9ef1aac9b57a038b699e6d518))
+* add ElementHandle.scrollIntoView ([#10005](https://github.com/puppeteer/puppeteer/issues/10005)) ([0d556a7](https://github.com/puppeteer/puppeteer/commit/0d556a71d6bcd5da501724ccbb4ce0be433768df))
+
+
+### Bug Fixes
+
+* make isIntersectingViewport work with SVG elements ([#10004](https://github.com/puppeteer/puppeteer/issues/10004)) ([656b562](https://github.com/puppeteer/puppeteer/commit/656b562c7488d4976a7a53264feef508c6b629dd))
+
+
+### Performance Improvements
+
+* amortize handle iterator ([#10002](https://github.com/puppeteer/puppeteer/issues/10002)) ([ab27f73](https://github.com/puppeteer/puppeteer/commit/ab27f738c9abb56f6083d02f7f45d2b8da9fc3f3))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 0.4.0 to 0.4.1
+
+## [19.8.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.4...puppeteer-core-v19.8.5) (2023-04-06)
+
+
+### Bug Fixes
+
+* add filter to setDiscoverTargets for Firefox ([#9693](https://github.com/puppeteer/puppeteer/issues/9693)) ([c09764e](https://github.com/puppeteer/puppeteer/commit/c09764e4c43d7a62096f430b598d63f2b688e860))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 0.3.3 to 0.4.0
+
+## [19.8.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.3...puppeteer-core-v19.8.4) (2023-04-06)
+
+
+### Bug Fixes
+
+* ignore extraInfo events if the response is served from cache ([#9983](https://github.com/puppeteer/puppeteer/issues/9983)) ([e7265c9](https://github.com/puppeteer/puppeteer/commit/e7265c9aa94e749de5745e5e98d45d4659f19d30))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 0.3.2 to 0.3.3
+
+## [19.8.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.1...puppeteer-core-v19.8.3) (2023-04-03)
+
+
+### Bug Fixes
+
+* use shadowRoot for tree walker ([#9950](https://github.com/puppeteer/puppeteer/issues/9950)) ([728547d](https://github.com/puppeteer/puppeteer/commit/728547d4608e8c601e209ede860493b1986da174))
+
+## [19.8.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.0...puppeteer-core-v19.8.1) (2023-03-28)
+
+
+### Bug Fixes
+
+* increase the default protocol timeout ([#9928](https://github.com/puppeteer/puppeteer/issues/9928)) ([4465f4b](https://github.com/puppeteer/puppeteer/commit/4465f4bd1900afc0b049ac863f4e372453a0c234))
+
+## [19.8.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.5...puppeteer-core-v19.8.0) (2023-03-24)
+
+
+### Features
+
+* add Page.waitForDevicePrompt ([#9299](https://github.com/puppeteer/puppeteer/issues/9299)) ([a5149d5](https://github.com/puppeteer/puppeteer/commit/a5149d52f54036a27a411bc070902b1eb3a7a629))
+* **chromium:** roll to Chromium 112.0.5614.0 (r1108766) ([#9841](https://github.com/puppeteer/puppeteer/issues/9841)) ([eddb1f6](https://github.com/puppeteer/puppeteer/commit/eddb1f6ec3958b79fea297123f7621eb7beaff04))
+
+
+### Bug Fixes
+
+* fallback to CSS ([#9876](https://github.com/puppeteer/puppeteer/issues/9876)) ([e6ec9c2](https://github.com/puppeteer/puppeteer/commit/e6ec9c295847fa0f1ec240952f0f2523bb13b7c8))
+* implement protocol-level timeouts ([#9877](https://github.com/puppeteer/puppeteer/issues/9877)) ([510b36c](https://github.com/puppeteer/puppeteer/commit/510b36c50001c95783b00dc8af42b5801ec57358))
+* viewport.deviceScaleFactor can be set to system default ([#9911](https://github.com/puppeteer/puppeteer/issues/9911)) ([022c909](https://github.com/puppeteer/puppeteer/commit/022c90932658d13ff4ae4aa51d26716f5dbe54ac))
+* waitForNavigation issue with aborted events ([#9883](https://github.com/puppeteer/puppeteer/issues/9883)) ([36c029b](https://github.com/puppeteer/puppeteer/commit/36c029b38d64a10590bfc74ecea255a58914b0d2))
+
+## [19.7.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.4...puppeteer-core-v19.7.5) (2023-03-14)
+
+
+### Bug Fixes
+
+* sort elements based on selector matching algorithm ([#9836](https://github.com/puppeteer/puppeteer/issues/9836)) ([9044609](https://github.com/puppeteer/puppeteer/commit/9044609be3ea78c650420533e7f6f40b83cedd99))
+
+
+### Performance Improvements
+
+* use `querySelector*` for pure CSS selectors ([#9835](https://github.com/puppeteer/puppeteer/issues/9835)) ([8aea8e0](https://github.com/puppeteer/puppeteer/commit/8aea8e047103b72c0238dde8e4777acf7897ddaa))
+
+## [19.7.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.3...puppeteer-core-v19.7.4) (2023-03-10)
+
+
+### Bug Fixes
+
+* call _detach on disconnect ([#9807](https://github.com/puppeteer/puppeteer/issues/9807)) ([bc1a04d](https://github.com/puppeteer/puppeteer/commit/bc1a04def8f699ad245c12ec69ac176e3e7e888d))
+* restore rimraf for puppeteer-core code ([#9815](https://github.com/puppeteer/puppeteer/issues/9815)) ([cefc4ea](https://github.com/puppeteer/puppeteer/commit/cefc4eab4750d2c1209eb36ca44f6963a4a6bf4c))
+* update troubleshooting guide links in errors ([#9821](https://github.com/puppeteer/puppeteer/issues/9821)) ([0165f06](https://github.com/puppeteer/puppeteer/commit/0165f06deef9e45862fd127a205ade5ad30ddaa3))
+
+## [19.7.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.2...puppeteer-core-v19.7.3) (2023-03-06)
+
+
+### Bug Fixes
+
+* update dependencies ([#9781](https://github.com/puppeteer/puppeteer/issues/9781)) ([364b23f](https://github.com/puppeteer/puppeteer/commit/364b23f8b5c7b04974f233c58e5ded9a8f912ff2))
+
+## [19.7.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.1...puppeteer-core-v19.7.2) (2023-02-20)
+
+
+### Bug Fixes
+
+* bump chromium-bidi to a version that does not declare mitt as a peer dependency ([#9701](https://github.com/puppeteer/puppeteer/issues/9701)) ([82916c1](https://github.com/puppeteer/puppeteer/commit/82916c102b2c399093ba9019e272207b5ce81849))
+
+## [19.7.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.0...puppeteer-core-v19.7.1) (2023-02-15)
+
+
+### Bug Fixes
+
+* fix circularity on JSHandle interface ([#9661](https://github.com/puppeteer/puppeteer/issues/9661)) ([eb13863](https://github.com/puppeteer/puppeteer/commit/eb138635d661d3cdaf2940959fece5aca482178a))
+* make chromium-bidi an opt peer dep ([#9667](https://github.com/puppeteer/puppeteer/issues/9667)) ([c6054ac](https://github.com/puppeteer/puppeteer/commit/c6054ac1a56c08ee7bf01321878699b7b4ab4e0b))
+
+## [19.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.3...puppeteer-core-v19.7.0) (2023-02-13)
+
+
+### Features
+
+* add touchstart, touchmove and touchend methods ([#9622](https://github.com/puppeteer/puppeteer/issues/9622)) ([c8bb11a](https://github.com/puppeteer/puppeteer/commit/c8bb11adfcf1537032730a91baa3c36a6e324926))
+* **chromium:** roll to Chromium 111.0.5556.0 (r1095492) ([#9656](https://github.com/puppeteer/puppeteer/issues/9656)) ([df59d01](https://github.com/puppeteer/puppeteer/commit/df59d010c20644da06eb4c4e28a11c4eea164aba))
+
+
+### Bug Fixes
+
+* `page.goto` error throwing on 40x/50x responses with an empty body ([#9523](https://github.com/puppeteer/puppeteer/issues/9523)) ([#9577](https://github.com/puppeteer/puppeteer/issues/9577)) ([ddb0cc1](https://github.com/puppeteer/puppeteer/commit/ddb0cc174d2a14c0948dcdaf9bae78620937c667))
+
+## [19.6.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.2...puppeteer-core-v19.6.3) (2023-02-01)
+
+
+### Bug Fixes
+
+* ignore not found contexts for console messages ([#9595](https://github.com/puppeteer/puppeteer/issues/9595)) ([390685b](https://github.com/puppeteer/puppeteer/commit/390685bbe52c22b686fc0e3119b4ac7b1073c581))
+* restore WaitTask terminate condition ([#9612](https://github.com/puppeteer/puppeteer/issues/9612)) ([e16cbc6](https://github.com/puppeteer/puppeteer/commit/e16cbc6626cffd40d0caa30801620e7293455006))
+
+## [19.6.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.1...puppeteer-core-v19.6.2) (2023-01-27)
+
+
+### Bug Fixes
+
+* atomically get Puppeteer utilities ([#9597](https://github.com/puppeteer/puppeteer/issues/9597)) ([050a7b0](https://github.com/puppeteer/puppeteer/commit/050a7b062415ebaf10bcb71c405143eacc4e5d4b))
+
+## [19.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.0...puppeteer-core-v19.6.1) (2023-01-26)
+
+
+### Bug Fixes
+
+* don't clean up previous browser versions ([#9568](https://github.com/puppeteer/puppeteer/issues/9568)) ([344bc2a](https://github.com/puppeteer/puppeteer/commit/344bc2af62e4068fe2cb8162d4b6c8242aac843b)), closes [#9533](https://github.com/puppeteer/puppeteer/issues/9533)
+* mimic rejection for PuppeteerUtil on early call ([#9589](https://github.com/puppeteer/puppeteer/issues/9589)) ([1980de9](https://github.com/puppeteer/puppeteer/commit/1980de91a161523c7098a79919b20e6d8d2e5d81))
+* **revert:** use LazyArg for puppeteer utilities ([#9590](https://github.com/puppeteer/puppeteer/issues/9590)) ([6edd996](https://github.com/puppeteer/puppeteer/commit/6edd99676827de2c83f7a858e4f903b1c34e7d35))
+* use LazyArg for puppeteer utilities ([#9575](https://github.com/puppeteer/puppeteer/issues/9575)) ([496658f](https://github.com/puppeteer/puppeteer/commit/496658f02945b53096483f36cb3d64556cff045e))
+
+## [19.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.5.2...puppeteer-core-v19.6.0) (2023-01-23)
+
+
+### Features
+
+* **chromium:** roll to Chromium 110.0.5479.0 (r1083080) ([#9500](https://github.com/puppeteer/puppeteer/issues/9500)) ([06e816b](https://github.com/puppeteer/puppeteer/commit/06e816bbfa7b9ca84284929f654de7288c51169d)), closes [#9470](https://github.com/puppeteer/puppeteer/issues/9470)
+* **page:** Adding support for referrerPolicy in `page.goto` ([#9561](https://github.com/puppeteer/puppeteer/issues/9561)) ([e3d69ec](https://github.com/puppeteer/puppeteer/commit/e3d69ec554beeac37bd206a21921d2fed3cb968c))
+
+
+### Bug Fixes
+
+* firefox revision resolution should not update chrome revision ([#9507](https://github.com/puppeteer/puppeteer/issues/9507)) ([f59bbf4](https://github.com/puppeteer/puppeteer/commit/f59bbf4014644dec6f395713e8403939aebe06ea)), closes [#9461](https://github.com/puppeteer/puppeteer/issues/9461)
+* improve screenshot method types ([#9529](https://github.com/puppeteer/puppeteer/issues/9529)) ([6847f88](https://github.com/puppeteer/puppeteer/commit/6847f8835f28e97edba6fce76a4cbf85561482b9))
+
+## [19.5.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.5.1...puppeteer-core-v19.5.2) (2023-01-11)
+
+
+### Bug Fixes
+
+* make sure browser fetcher in launchers uses configuration ([#9493](https://github.com/puppeteer/puppeteer/issues/9493)) ([df55439](https://github.com/puppeteer/puppeteer/commit/df554397b51e97aea2765b325f9a887b50b9263a)), closes [#9470](https://github.com/puppeteer/puppeteer/issues/9470)
+
+## [19.5.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.5.0...puppeteer-core-v19.5.1) (2023-01-11)
+
+
+### Bug Fixes
+
+* use puppeteer node for installation script ([#9489](https://github.com/puppeteer/puppeteer/issues/9489)) ([9bf90d9](https://github.com/puppeteer/puppeteer/commit/9bf90d9f4b5aeab06f8b433714712cad3259d36e))
+
+## [19.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.4.1...puppeteer-core-v19.5.0) (2023-01-05)
+
+
+### Features
+
+* add element validation ([#9352](https://github.com/puppeteer/puppeteer/issues/9352)) ([c7a063a](https://github.com/puppeteer/puppeteer/commit/c7a063a15274856184356e15f2ae4be41191d309))
+
+
+### Bug Fixes
+
+* **puppeteer-core:** target interceptor is not async ([#9430](https://github.com/puppeteer/puppeteer/issues/9430)) ([e3e9cc6](https://github.com/puppeteer/puppeteer/commit/e3e9cc622ac32f2067b6e74b5e8706c63169a157))
+
+## [19.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.4.0...puppeteer-core-v19.4.1) (2022-12-16)
+
+
+### Bug Fixes
+
+* improve a11y snapshot handling if the tree is not correct ([#9405](https://github.com/puppeteer/puppeteer/issues/9405)) ([02fe501](https://github.com/puppeteer/puppeteer/commit/02fe50194e60bd14c3a82539473a0313ab88c766)), closes [#9404](https://github.com/puppeteer/puppeteer/issues/9404)
+* remove oopif expectations and fix oopif flakiness ([#9375](https://github.com/puppeteer/puppeteer/issues/9375)) ([810e0cd](https://github.com/puppeteer/puppeteer/commit/810e0cd74ecef353cfa43746c18bd5f580a3233d))
+
+## [19.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.3.0...puppeteer-core-v19.4.0) (2022-12-07)
+
+
+### Features
+
+* ability to send headers via ws connection to browser in node.js environment ([#9314](https://github.com/puppeteer/puppeteer/issues/9314)) ([937fffa](https://github.com/puppeteer/puppeteer/commit/937fffaedc340ea12d5f6636d3ba6598cb22e397)), closes [#7218](https://github.com/puppeteer/puppeteer/issues/7218)
+* **chromium:** roll to Chromium 109.0.5412.0 (r1069273) ([#9364](https://github.com/puppeteer/puppeteer/issues/9364)) ([1875da6](https://github.com/puppeteer/puppeteer/commit/1875da61916df1fbcf98047858c01075bd9af189)), closes [#9233](https://github.com/puppeteer/puppeteer/issues/9233)
+* **puppeteer-core:** keydown supports commands ([#9357](https://github.com/puppeteer/puppeteer/issues/9357)) ([b7ebc5d](https://github.com/puppeteer/puppeteer/commit/b7ebc5d9bb9b9940ffdf470e51d007f709587d40))
+
+
+### Bug Fixes
+
+* **puppeteer-core:** avoid type instantiation errors ([#9370](https://github.com/puppeteer/puppeteer/issues/9370)) ([17f31a9](https://github.com/puppeteer/puppeteer/commit/17f31a9ee408ca5a08fe6dbceb8915e710156bd3)), closes [#9369](https://github.com/puppeteer/puppeteer/issues/9369)
+
+## [19.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.2.2...puppeteer-core-v19.3.0) (2022-11-23)
+
+
+### Features
+
+* **puppeteer-core:** Infer element type from complex selector ([#9253](https://github.com/puppeteer/puppeteer/issues/9253)) ([bef1061](https://github.com/puppeteer/puppeteer/commit/bef1061c064e5135d86a48fffd7278f3e7f4a29e))
+* **puppeteer-core:** update Chrome launcher flags ([#9239](https://github.com/puppeteer/puppeteer/issues/9239)) ([ae87bfc](https://github.com/puppeteer/puppeteer/commit/ae87bfc2b4361556e3660a1de2c6db348ce663ae))
+
+
+### Bug Fixes
+
+* remove boundary conditions for visibility ([#9249](https://github.com/puppeteer/puppeteer/issues/9249)) ([e003513](https://github.com/puppeteer/puppeteer/commit/e003513c0c049aad38e374a16dc96c3e54ab0de5))
+
+## [19.2.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.2.1...puppeteer-core-v19.2.2) (2022-11-03)
+
+
+### Bug Fixes
+
+* update missing product message ([#9207](https://github.com/puppeteer/puppeteer/issues/9207)) ([29f47e2](https://github.com/puppeteer/puppeteer/commit/29f47e2e150ff7bfd89e38a4ce4ca34eac7f2fdf))
+
+## [19.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.2.0...puppeteer-core-v19.2.1) (2022-10-28)
+
+
+### Bug Fixes
+
+* resolve navigation requests when request fails ([#9178](https://github.com/puppeteer/puppeteer/issues/9178)) ([c11297b](https://github.com/puppeteer/puppeteer/commit/c11297baa5124eb89f7686c3eb446d2ba1b7123a)), closes [#9175](https://github.com/puppeteer/puppeteer/issues/9175)
+
+## [19.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.1.1...puppeteer-core-v19.2.0) (2022-10-26)
+
+
+### Features
+
+* **chromium:** roll to Chromium 108.0.5351.0 (r1056772) ([#9153](https://github.com/puppeteer/puppeteer/issues/9153)) ([e78a4e8](https://github.com/puppeteer/puppeteer/commit/e78a4e89c22bb1180e72d180c16b39673ff9125e))
+
+## [19.1.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.1.0...puppeteer-core-v19.1.1) (2022-10-24)
+
+
+### Bug Fixes
+
+* update documentation on configuring puppeteer ([#9150](https://github.com/puppeteer/puppeteer/issues/9150)) ([f07ad2c](https://github.com/puppeteer/puppeteer/commit/f07ad2c6616ecd2a959b0c1a65b167ba77611d61))
+
+## [19.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.0.0...puppeteer-core-v19.1.0) (2022-10-21)
+
+
+### Features
+
+* expose browser context id ([#9134](https://github.com/puppeteer/puppeteer/issues/9134)) ([122778a](https://github.com/puppeteer/puppeteer/commit/122778a1f8b60e0dcc6f0ffcb2097e95ae98f4a3)), closes [#9132](https://github.com/puppeteer/puppeteer/issues/9132)
+* use configuration files ([#9140](https://github.com/puppeteer/puppeteer/issues/9140)) ([ec20174](https://github.com/puppeteer/puppeteer/commit/ec201744f077987b288e3dff52c0906fe700f6fb)), closes [#9128](https://github.com/puppeteer/puppeteer/issues/9128)
+
+
+### Bug Fixes
+
+* update `BrowserFetcher` deprecation message ([#9141](https://github.com/puppeteer/puppeteer/issues/9141)) ([efcbc97](https://github.com/puppeteer/puppeteer/commit/efcbc97c60e4cfd49a9ed25a900f6133d06b290b))
+
+## [19.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v18.2.1...puppeteer-core-v19.0.0) (2022-10-14)
+
+
+### ⚠ BREAKING CHANGES
+
+* use `~/.cache/puppeteer` for browser downloads (#9095)
+* deprecate `createBrowserFetcher` in favor of `BrowserFetcher` (#9079)
+* refactor custom query handler API (#9078)
+* remove `puppeteer.devices` in favor of `KnownDevices` (#9075)
+* deprecate indirect network condition imports (#9074)
+* deprecate indirect error imports (#9072)
+
+### Features
+
+* add ability to collect JS code coverage at the function level ([#9027](https://github.com/puppeteer/puppeteer/issues/9027)) ([a032583](https://github.com/puppeteer/puppeteer/commit/a032583b6c9b469bda699bca200b180206d61247))
+* deprecate `createBrowserFetcher` in favor of `BrowserFetcher` ([#9079](https://github.com/puppeteer/puppeteer/issues/9079)) ([7294dfe](https://github.com/puppeteer/puppeteer/commit/7294dfe9c6c3b224f95ba6d59b5ef33d379fd09a)), closes [#8999](https://github.com/puppeteer/puppeteer/issues/8999)
+* use `~/.cache/puppeteer` for browser downloads ([#9095](https://github.com/puppeteer/puppeteer/issues/9095)) ([3df375b](https://github.com/puppeteer/puppeteer/commit/3df375baedad64b8773bb1e1e6f81b604ed18989))
+
+
+### Bug Fixes
+
+* deprecate indirect error imports ([#9072](https://github.com/puppeteer/puppeteer/issues/9072)) ([9f4f43a](https://github.com/puppeteer/puppeteer/commit/9f4f43a28b06787a1cf97efe904ccfe7237dffdd))
+* deprecate indirect network condition imports ([#9074](https://github.com/puppeteer/puppeteer/issues/9074)) ([41d0122](https://github.com/puppeteer/puppeteer/commit/41d0122b94f41b308536c48ced345dec8c272a49))
+* refactor custom query handler API ([#9078](https://github.com/puppeteer/puppeteer/issues/9078)) ([1847704](https://github.com/puppeteer/puppeteer/commit/1847704789e2888c755de8c739d567364b8ad645))
+* remove `puppeteer.devices` in favor of `KnownDevices` ([#9075](https://github.com/puppeteer/puppeteer/issues/9075)) ([87c08fd](https://github.com/puppeteer/puppeteer/commit/87c08fd86a79b63308ad8d46c5f7acd1927505f8))
+* remove viewport conditions in `waitForSelector` ([#9087](https://github.com/puppeteer/puppeteer/issues/9087)) ([acbc599](https://github.com/puppeteer/puppeteer/commit/acbc59999bf800eeac75c4045b75a32b4357c79e))
+
+## [18.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v18.2.0...puppeteer-core-v18.2.1) (2022-10-06)
+
+
+### Bug Fixes
+
+* add README to package during prepack ([#9057](https://github.com/puppeteer/puppeteer/issues/9057)) ([9374e23](https://github.com/puppeteer/puppeteer/commit/9374e23d3da5e40378461ed08db24649730a445a))
+* waitForRequest works with async predicate ([#9058](https://github.com/puppeteer/puppeteer/issues/9058)) ([8f6b2c9](https://github.com/puppeteer/puppeteer/commit/8f6b2c9b7c219d405c954bf7af082d3d29fd48ff))
+
+## [18.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v18.1.0...puppeteer-core-v18.2.0) (2022-10-05)
+
+
+### Features
+
+* separate puppeteer and puppeteer-core ([#9023](https://github.com/puppeteer/puppeteer/issues/9023)) ([f42336c](https://github.com/puppeteer/puppeteer/commit/f42336cf83982332829ca7e14ee48d8676e11545))
+
+
+## [18.1.0](https://github.com/puppeteer/puppeteer/compare/v18.0.5...v18.1.0) (2022-10-05)
+
+### Features
+
+* **chromium:** roll to Chromium 107.0.5296.0 (r1045629) ([#9039](https://github.com/puppeteer/puppeteer/issues/9039)) ([022fbde](https://github.com/puppeteer/puppeteer/commit/022fbde85e067e8c419cf42dd571f9a1187c343c))
+
+## [18.0.5](https://github.com/puppeteer/puppeteer/compare/v18.0.4...v18.0.5) (2022-09-22)
+
+
+### Bug Fixes
+
+* add missing npm config environment variable ([#8996](https://github.com/puppeteer/puppeteer/issues/8996)) ([7c1be20](https://github.com/puppeteer/puppeteer/commit/7c1be20aef46aaf5029732a580ec65aa8008aa9c))
+
+## [18.0.4](https://github.com/puppeteer/puppeteer/compare/v18.0.3...v18.0.4) (2022-09-21)
+
+
+### Bug Fixes
+
+* hardcode binding names ([#8993](https://github.com/puppeteer/puppeteer/issues/8993)) ([7e20554](https://github.com/puppeteer/puppeteer/commit/7e2055433e79ef20f6dcdf02f92e1d64564b7d33))
+
+## [18.0.3](https://github.com/puppeteer/puppeteer/compare/v18.0.2...v18.0.3) (2022-09-20)
+
+
+### Bug Fixes
+
+* change injected.ts imports ([#8987](https://github.com/puppeteer/puppeteer/issues/8987)) ([10a114d](https://github.com/puppeteer/puppeteer/commit/10a114d36f2add90860950f61b3f8b93258edb5c))
+
+## [18.0.2](https://github.com/puppeteer/puppeteer/compare/v18.0.1...v18.0.2) (2022-09-19)
+
+
+### Bug Fixes
+
+* mark internal objects ([#8984](https://github.com/puppeteer/puppeteer/issues/8984)) ([181a148](https://github.com/puppeteer/puppeteer/commit/181a148269fce1575f5e37056929ecdec0517586))
+
+## [18.0.1](https://github.com/puppeteer/puppeteer/compare/v18.0.0...v18.0.1) (2022-09-19)
+
+
+### Bug Fixes
+
+* internal lazy params ([#8982](https://github.com/puppeteer/puppeteer/issues/8982)) ([d504597](https://github.com/puppeteer/puppeteer/commit/d5045976a6dd321bbd265b84c2474ff1ad5d0b77))
+
+## [18.0.0](https://github.com/puppeteer/puppeteer/compare/v17.1.3...v18.0.0) (2022-09-19)
+
+
+### ⚠ BREAKING CHANGES
+
+* fix bounding box visibility conditions (#8954)
+
+### Features
+
+* add text query handler ([#8956](https://github.com/puppeteer/puppeteer/issues/8956)) ([633e7cf](https://github.com/puppeteer/puppeteer/commit/633e7cfdf99d42f420d0af381394bd1f6ac7bcd1))
+
+
+### Bug Fixes
+
+* fix bounding box visibility conditions ([#8954](https://github.com/puppeteer/puppeteer/issues/8954)) ([ac9929d](https://github.com/puppeteer/puppeteer/commit/ac9929d80f6f7d4905a39183ae235500e29b4f53))
+* suppress init errors if the target is closed ([#8947](https://github.com/puppeteer/puppeteer/issues/8947)) ([cfaaa5e](https://github.com/puppeteer/puppeteer/commit/cfaaa5e2c07e5f98baeb7de99e303aa840a351e8))
+* use win64 version of chromium when on arm64 windows ([#8927](https://github.com/puppeteer/puppeteer/issues/8927)) ([64843b8](https://github.com/puppeteer/puppeteer/commit/64843b88853210314677ab1b434729513ce615a7))
+
+## [17.1.3](https://github.com/puppeteer/puppeteer/compare/v17.1.2...v17.1.3) (2022-09-08)
+
+
+### Bug Fixes
+
+* FirefoxLauncher should not use BrowserFetcher in puppeteer-core ([#8920](https://github.com/puppeteer/puppeteer/issues/8920)) ([f2e8de7](https://github.com/puppeteer/puppeteer/commit/f2e8de777fc5d547778fdc6cac658add84ed4082)), closes [#8919](https://github.com/puppeteer/puppeteer/issues/8919)
+* linux arm64 check on windows arm ([#8917](https://github.com/puppeteer/puppeteer/issues/8917)) ([f02b926](https://github.com/puppeteer/puppeteer/commit/f02b926245e28b5671087c051dbdbb3165696f08)), closes [#8915](https://github.com/puppeteer/puppeteer/issues/8915)
+
+## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07)
+
+
+### Bug Fixes
+
+* add missing code coverage ranges that span only a single character ([#8911](https://github.com/puppeteer/puppeteer/issues/8911)) ([0c577b9](https://github.com/puppeteer/puppeteer/commit/0c577b9bf8855dc0ccb6098cd43a25c528f6d7f5))
+* add Page.getDefaultTimeout getter ([#8903](https://github.com/puppeteer/puppeteer/issues/8903)) ([3240095](https://github.com/puppeteer/puppeteer/commit/32400954c50cbddc48468ad118c3f8a47653b9d3)), closes [#8901](https://github.com/puppeteer/puppeteer/issues/8901)
+* don't detect project root for puppeteer-core ([#8907](https://github.com/puppeteer/puppeteer/issues/8907)) ([b4f5ea1](https://github.com/puppeteer/puppeteer/commit/b4f5ea1167a60c870194c70d22f5372ada5b7c4c)), closes [#8896](https://github.com/puppeteer/puppeteer/issues/8896)
+* support scale for screenshot clips ([#8908](https://github.com/puppeteer/puppeteer/issues/8908)) ([260e428](https://github.com/puppeteer/puppeteer/commit/260e4282275ab1d05c86e5643e2a02c01f269a9c)), closes [#5329](https://github.com/puppeteer/puppeteer/issues/5329)
+* work around a race in waitForFileChooser ([#8905](https://github.com/puppeteer/puppeteer/issues/8905)) ([053d960](https://github.com/puppeteer/puppeteer/commit/053d960fb593e514e7914d7da9af436afc39a12f)), closes [#6040](https://github.com/puppeteer/puppeteer/issues/6040)
+
+## [17.1.1](https://github.com/puppeteer/puppeteer/compare/v17.1.0...v17.1.1) (2022-09-05)
+
+
+### Bug Fixes
+
+* restore deferred promise debugging ([#8895](https://github.com/puppeteer/puppeteer/issues/8895)) ([7b42250](https://github.com/puppeteer/puppeteer/commit/7b42250c7bb91ac873307acda493726ffc4c54a8))
+
+## [17.1.0](https://github.com/puppeteer/puppeteer/compare/v17.0.0...v17.1.0) (2022-09-02)
+
+
+### Features
+
+* **chromium:** roll to Chromium 106.0.5249.0 (r1036745) ([#8869](https://github.com/puppeteer/puppeteer/issues/8869)) ([6e9a47a](https://github.com/puppeteer/puppeteer/commit/6e9a47a6faa06d241dec0bcf7bcdf49370517008))
+
+
+### Bug Fixes
+
+* allow getting a frame from an elementhandle ([#8875](https://github.com/puppeteer/puppeteer/issues/8875)) ([3732757](https://github.com/puppeteer/puppeteer/commit/3732757450b4363041ccbacc3b236289a156abb0))
+* typos in documentation ([#8858](https://github.com/puppeteer/puppeteer/issues/8858)) ([8d95a9b](https://github.com/puppeteer/puppeteer/commit/8d95a9bc920b98820aa655ad4eb2d8fd9b2b893a))
+* use the timeout setting in waitForFileChooser ([#8856](https://github.com/puppeteer/puppeteer/issues/8856)) ([f477b46](https://github.com/puppeteer/puppeteer/commit/f477b46f212da9206102da695697760eea539f05))
+
+## [17.0.0](https://github.com/puppeteer/puppeteer/compare/v16.2.0...v17.0.0) (2022-08-26)
+
+
+### ⚠ BREAKING CHANGES
+
+* remove `root` from `WaitForSelectorOptions` (#8848)
+* internalize execution context (#8844)
+
+### Bug Fixes
+
+* allow multiple navigations to happen in LifecycleWatcher ([#8826](https://github.com/puppeteer/puppeteer/issues/8826)) ([341b669](https://github.com/puppeteer/puppeteer/commit/341b669a5e45ecbb9ffb0f28c45b520660f27ad2)), closes [#8811](https://github.com/puppeteer/puppeteer/issues/8811)
+* internalize execution context ([#8844](https://github.com/puppeteer/puppeteer/issues/8844)) ([2f33237](https://github.com/puppeteer/puppeteer/commit/2f33237d0443de77d58dca4454b0c9a1d2b57d03))
+* remove `root` from `WaitForSelectorOptions` ([#8848](https://github.com/puppeteer/puppeteer/issues/8848)) ([1155c8e](https://github.com/puppeteer/puppeteer/commit/1155c8eac85b176c3334cc3d98adfe7d943dfbe6))
+* remove deferred promise timeouts ([#8835](https://github.com/puppeteer/puppeteer/issues/8835)) ([202ffce](https://github.com/puppeteer/puppeteer/commit/202ffce0aa4f34dba35fbb8e7d740af16efee35f)), closes [#8832](https://github.com/puppeteer/puppeteer/issues/8832)
+
+## [16.2.0](https://github.com/puppeteer/puppeteer/compare/v16.1.1...v16.2.0) (2022-08-18)
+
+
+### Features
+
+* add Khmer (Cambodian) language support ([#8809](https://github.com/puppeteer/puppeteer/issues/8809)) ([34f8737](https://github.com/puppeteer/puppeteer/commit/34f873721804d57a5faf3eab8ef50340c69ed180))
+
+
+### Bug Fixes
+
+* handle service workers in extensions ([#8807](https://github.com/puppeteer/puppeteer/issues/8807)) ([2a0eefb](https://github.com/puppeteer/puppeteer/commit/2a0eefb99f0ae00dacc9e768a253308c0d18a4c3)), closes [#8800](https://github.com/puppeteer/puppeteer/issues/8800)
+
+## [16.1.1](https://github.com/puppeteer/puppeteer/compare/v16.1.0...v16.1.1) (2022-08-16)
+
+
+### Bug Fixes
+
+* custom sessions should not emit targetcreated events ([#8788](https://github.com/puppeteer/puppeteer/issues/8788)) ([3fad05d](https://github.com/puppeteer/puppeteer/commit/3fad05d333b79f41a7b58582c4ca493200bb5a79)), closes [#8787](https://github.com/puppeteer/puppeteer/issues/8787)
+* deprecate `ExecutionContext` ([#8792](https://github.com/puppeteer/puppeteer/issues/8792)) ([b5da718](https://github.com/puppeteer/puppeteer/commit/b5da718e2e4a2004a36cf23cad555e1fc3b50333))
+* deprecate `root` in `WaitForSelectorOptions` ([#8795](https://github.com/puppeteer/puppeteer/issues/8795)) ([65a5ce8](https://github.com/puppeteer/puppeteer/commit/65a5ce8464c56fcc55e5ac3ed490f31311bbe32a))
+* deprecate `waitForTimeout` ([#8793](https://github.com/puppeteer/puppeteer/issues/8793)) ([8f612d5](https://github.com/puppeteer/puppeteer/commit/8f612d5ff855d48ae4b38bdaacf2a8fbda8e9ce8))
+* make sure there is a check for targets when timeout=0 ([#8765](https://github.com/puppeteer/puppeteer/issues/8765)) ([c23cdb7](https://github.com/puppeteer/puppeteer/commit/c23cdb73a7b113c1dd29f7e4a7a61326422c4080)), closes [#8763](https://github.com/puppeteer/puppeteer/issues/8763)
+* resolve navigation flakiness ([#8768](https://github.com/puppeteer/puppeteer/issues/8768)) ([2580347](https://github.com/puppeteer/puppeteer/commit/2580347b50091d172b2a5591138a2e41ede072fe)), closes [#8644](https://github.com/puppeteer/puppeteer/issues/8644)
+* specify Puppeteer version for Chromium 105.0.5173.0 ([#8766](https://github.com/puppeteer/puppeteer/issues/8766)) ([b5064b7](https://github.com/puppeteer/puppeteer/commit/b5064b7b8bd3bd9eb481b6807c65d9d06d23b9dd))
+* use targetFilter in puppeteer.launch ([#8774](https://github.com/puppeteer/puppeteer/issues/8774)) ([ee2540b](https://github.com/puppeteer/puppeteer/commit/ee2540baefeced44f6b336f2b979af5c3a4cb040)), closes [#8772](https://github.com/puppeteer/puppeteer/issues/8772)
+
+## [16.1.0](https://github.com/puppeteer/puppeteer/compare/v16.0.0...v16.1.0) (2022-08-06)
+
+
+### Features
+
+* use an `xpath` query handler ([#8730](https://github.com/puppeteer/puppeteer/issues/8730)) ([5cf9b4d](https://github.com/puppeteer/puppeteer/commit/5cf9b4de8d50bd056db82bcaa23279b72c9313c5))
+
+
+### Bug Fixes
+
+* resolve target manager init if no existing targets detected ([#8748](https://github.com/puppeteer/puppeteer/issues/8748)) ([8cb5043](https://github.com/puppeteer/puppeteer/commit/8cb5043868f69cdff7f34f1cfe0c003ff09e281b)), closes [#8747](https://github.com/puppeteer/puppeteer/issues/8747)
+* specify the target filter in setDiscoverTargets ([#8742](https://github.com/puppeteer/puppeteer/issues/8742)) ([49193cb](https://github.com/puppeteer/puppeteer/commit/49193cbf1c17f16f0ca59a9fd2ebf306f812f52b))
+
+## [16.0.0](https://github.com/puppeteer/puppeteer/compare/v15.5.0...v16.0.0) (2022-08-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* With Chromium, Puppeteer will now attach to page/iframe targets immediately to allow reliable configuration of targets.
+
+### Features
+
+* add Dockerfile ([#8315](https://github.com/puppeteer/puppeteer/issues/8315)) ([936ed86](https://github.com/puppeteer/puppeteer/commit/936ed8607ec0c3798d2b22b590d0be0ad361a888))
+* detect Firefox in connect() automatically ([#8718](https://github.com/puppeteer/puppeteer/issues/8718)) ([2abd772](https://github.com/puppeteer/puppeteer/commit/2abd772c9c3d2b86deb71541eaac41aceef94356))
+* use CDP's auto-attach mechanism ([#8520](https://github.com/puppeteer/puppeteer/issues/8520)) ([2cbfdeb](https://github.com/puppeteer/puppeteer/commit/2cbfdeb0ca388a45cedfae865266230e1291bd29))
+
+
+### Bug Fixes
+
+* address flakiness in frame handling ([#8688](https://github.com/puppeteer/puppeteer/issues/8688)) ([6f81b23](https://github.com/puppeteer/puppeteer/commit/6f81b23728a511f7b89eaa2b8f850b22d6c4ab24))
+* disable AcceptCHFrame ([#8706](https://github.com/puppeteer/puppeteer/issues/8706)) ([96d9608](https://github.com/puppeteer/puppeteer/commit/96d9608d1de17877414a649a0737661894dd96c8)), closes [#8479](https://github.com/puppeteer/puppeteer/issues/8479)
+* use loaderId to reduce test flakiness ([#8717](https://github.com/puppeteer/puppeteer/issues/8717)) ([d2f6db2](https://github.com/puppeteer/puppeteer/commit/d2f6db20735342bb3f419e85adbd51ed10470044))
+
+## [15.5.0](https://github.com/puppeteer/puppeteer/compare/v15.4.2...v15.5.0) (2022-07-21)
+
+
+### Features
+
+* **chromium:** roll to Chromium 105.0.5173.0 (r1022525) ([#8682](https://github.com/puppeteer/puppeteer/issues/8682)) ([f1b8ad3](https://github.com/puppeteer/puppeteer/commit/f1b8ad3269286800d31818ea4b6b3ee23f7437c3))
+
+## [15.4.2](https://github.com/puppeteer/puppeteer/compare/v15.4.1...v15.4.2) (2022-07-21)
+
+
+### Bug Fixes
+
+* taking a screenshot with null viewport should be possible ([#8680](https://github.com/puppeteer/puppeteer/issues/8680)) ([2abb9f0](https://github.com/puppeteer/puppeteer/commit/2abb9f0c144779d555ecbf337a759440d0282cba)), closes [#8673](https://github.com/puppeteer/puppeteer/issues/8673)
+
+## [15.4.1](https://github.com/puppeteer/puppeteer/compare/v15.4.0...v15.4.1) (2022-07-21)
+
+
+### Bug Fixes
+
+* import URL ([#8670](https://github.com/puppeteer/puppeteer/issues/8670)) ([34ab5ca](https://github.com/puppeteer/puppeteer/commit/34ab5ca50353ffb6a6345a8984b724a6f42fb726))
+
+## [15.4.0](https://github.com/puppeteer/puppeteer/compare/v15.3.2...v15.4.0) (2022-07-13)
+
+
+### Features
+
+* expose the page getter on Frame ([#8657](https://github.com/puppeteer/puppeteer/issues/8657)) ([af08c5c](https://github.com/puppeteer/puppeteer/commit/af08c5c90380c853e8257a51298bfed4b0635779))
+
+
+### Bug Fixes
+
+* ignore *.tsbuildinfo ([#8662](https://github.com/puppeteer/puppeteer/issues/8662)) ([edcdf21](https://github.com/puppeteer/puppeteer/commit/edcdf217cefbf31aee5a2f571abac429dd81f3a0))
+
+## [15.3.2](https://github.com/puppeteer/puppeteer/compare/v15.3.1...v15.3.2) (2022-07-08)
+
+
+### Bug Fixes
+
+* cache dynamic imports ([#8652](https://github.com/puppeteer/puppeteer/issues/8652)) ([1de0383](https://github.com/puppeteer/puppeteer/commit/1de0383abf6be31cf06faede3e59b087a2958227))
+* expose a RemoteObject getter ([#8642](https://github.com/puppeteer/puppeteer/issues/8642)) ([d0c4291](https://github.com/puppeteer/puppeteer/commit/d0c42919956bd36ad7993a0fc1de86e886e39f62)), closes [#8639](https://github.com/puppeteer/puppeteer/issues/8639)
+* **page:** fix page.#scrollIntoViewIfNeeded method ([#8631](https://github.com/puppeteer/puppeteer/issues/8631)) ([b47f066](https://github.com/puppeteer/puppeteer/commit/b47f066c2c068825e3b65cfe17b6923c77ad30b9))
+
+## [15.3.1](https://github.com/puppeteer/puppeteer/compare/v15.3.0...v15.3.1) (2022-07-06)
+
+
+### Bug Fixes
+
+* extends `ElementHandle` to `Node`s ([#8552](https://github.com/puppeteer/puppeteer/issues/8552)) ([5ff205d](https://github.com/puppeteer/puppeteer/commit/5ff205dc8b659eb8864b4b1862105d21dd334c8f))
+
+## [15.3.0](https://github.com/puppeteer/puppeteer/compare/v15.2.0...v15.3.0) (2022-07-01)
+
+
+### Features
+
+* add documentation ([#8593](https://github.com/puppeteer/puppeteer/issues/8593)) ([066f440](https://github.com/puppeteer/puppeteer/commit/066f440ba7bdc9aca9423d7205adf36f2858bd78))
+
+
+### Bug Fixes
+
+* remove unused imports ([#8613](https://github.com/puppeteer/puppeteer/issues/8613)) ([0cf4832](https://github.com/puppeteer/puppeteer/commit/0cf4832878731ffcfc84570315f326eb851d7629))
+
+## [15.2.0](https://github.com/puppeteer/puppeteer/compare/v15.1.1...v15.2.0) (2022-06-29)
+
+
+### Features
+
+* add fromSurface option to page.screenshot ([#8496](https://github.com/puppeteer/puppeteer/issues/8496)) ([79e1198](https://github.com/puppeteer/puppeteer/commit/79e11985ba44b72b1ad6b8cd861fe316f1945e64))
+* export public types only ([#8584](https://github.com/puppeteer/puppeteer/issues/8584)) ([7001322](https://github.com/puppeteer/puppeteer/commit/7001322cd1cf9f77ee2c370d50a6707e7aaad72d))
+
+
+### Bug Fixes
+
+* clean up tmp profile dirs when browser is closed ([#8580](https://github.com/puppeteer/puppeteer/issues/8580)) ([9787a1d](https://github.com/puppeteer/puppeteer/commit/9787a1d8df7768017b36d42327faab402695c4bb))
+
+## [15.1.1](https://github.com/puppeteer/puppeteer/compare/v15.1.0...v15.1.1) (2022-06-25)
+
+
+### Bug Fixes
+
+* export `ElementHandle` ([e0198a7](https://github.com/puppeteer/puppeteer/commit/e0198a79e06c8bb72dde554db0246a3db5fec4c2))
+
+## [15.1.0](https://github.com/puppeteer/puppeteer/compare/v15.0.2...v15.1.0) (2022-06-24)
+
+
+### Features
+
+* **chromium:** roll to Chromium 104.0.5109.0 (r1011831) ([#8569](https://github.com/puppeteer/puppeteer/issues/8569)) ([fb7d31e](https://github.com/puppeteer/puppeteer/commit/fb7d31e3698428560e1f654d33782d241192f48f))
+
+## [15.0.2](https://github.com/puppeteer/puppeteer/compare/v15.0.1...v15.0.2) (2022-06-24)
+
+
+### Bug Fixes
+
+* CSS coverage should work with empty stylesheets ([#8570](https://github.com/puppeteer/puppeteer/issues/8570)) ([383e855](https://github.com/puppeteer/puppeteer/commit/383e8558477fae7708734ab2160ef50f385e2983)), closes [#8535](https://github.com/puppeteer/puppeteer/issues/8535)
+
+## [15.0.1](https://github.com/puppeteer/puppeteer/compare/v15.0.0...v15.0.1) (2022-06-24)
+
+
+### Bug Fixes
+
+* infer unioned handles ([#8562](https://github.com/puppeteer/puppeteer/issues/8562)) ([8100cbb](https://github.com/puppeteer/puppeteer/commit/8100cbb29569541541f61001983efb9a80d89890))
+
+## [15.0.0](https://github.com/puppeteer/puppeteer/compare/v14.4.1...v15.0.0) (2022-06-23)
+
+
+### ⚠ BREAKING CHANGES
+
+* type inference for evaluation types (#8547)
+
+### Features
+
+* add experimental `client` to `HTTPRequest` ([#8556](https://github.com/puppeteer/puppeteer/issues/8556)) ([ec79f3a](https://github.com/puppeteer/puppeteer/commit/ec79f3a58a44c9ea60a82f9cd2df4c8f19e82ab8))
+* type inference for evaluation types ([#8547](https://github.com/puppeteer/puppeteer/issues/8547)) ([26c3acb](https://github.com/puppeteer/puppeteer/commit/26c3acbb0795eb66f29479f442e156832f794f01))
+
+## [14.4.1](https://github.com/puppeteer/puppeteer/compare/v14.4.0...v14.4.1) (2022-06-17)
+
+
+### Bug Fixes
+
+* avoid `instanceof Object` check in `isErrorLike` ([#8527](https://github.com/puppeteer/puppeteer/issues/8527)) ([6cd5cd0](https://github.com/puppeteer/puppeteer/commit/6cd5cd043997699edca6e3458f90adc1118cf4a5))
+* export `devices`, `errors`, and more ([cba58a1](https://github.com/puppeteer/puppeteer/commit/cba58a12c4e2043f6a5acf7d4754e4a7b7f6e198))
+
+## [14.4.0](https://github.com/puppeteer/puppeteer/compare/v14.3.0...v14.4.0) (2022-06-13)
+
+
+### Features
+
+* export puppeteer methods ([#8493](https://github.com/puppeteer/puppeteer/issues/8493)) ([465a7c4](https://github.com/puppeteer/puppeteer/commit/465a7c405f01fcef99380ffa69d86042a1f5618f))
+* support node-like environments ([#8490](https://github.com/puppeteer/puppeteer/issues/8490)) ([f64ec20](https://github.com/puppeteer/puppeteer/commit/f64ec2051b9b2d12225abba6ffe9551da9751bf7))
+
+
+### Bug Fixes
+
+* parse empty options in \<select\> ([#8489](https://github.com/puppeteer/puppeteer/issues/8489)) ([b30f3f4](https://github.com/puppeteer/puppeteer/commit/b30f3f44cdabd9545c4661cd755b9d49e5c144cd))
+* use error-like ([#8504](https://github.com/puppeteer/puppeteer/issues/8504)) ([4d35990](https://github.com/puppeteer/puppeteer/commit/4d359906a44e4ddd5ec54a523cfd9076048d3433))
+* use OS-independent abs. path check ([#8505](https://github.com/puppeteer/puppeteer/issues/8505)) ([bfd4e68](https://github.com/puppeteer/puppeteer/commit/bfd4e68f25bec6e00fd5cbf261813f8297d362ee))
+
+## [14.3.0](https://github.com/puppeteer/puppeteer/compare/v14.2.1...v14.3.0) (2022-06-07)
+
+
+### Features
+
+* use absolute URL for EVALUATION_SCRIPT_URL ([#8481](https://github.com/puppeteer/puppeteer/issues/8481)) ([e142560](https://github.com/puppeteer/puppeteer/commit/e14256010d2d84d613cd3c6e7999b0705115d4bf)), closes [#8424](https://github.com/puppeteer/puppeteer/issues/8424)
+
+
+### Bug Fixes
+
+* don't throw on bad access ([#8472](https://github.com/puppeteer/puppeteer/issues/8472)) ([e837866](https://github.com/puppeteer/puppeteer/commit/e8378666c671e5703aec4f52912de2aac94e1828))
+* Kill browser process when killing process group fails ([#8477](https://github.com/puppeteer/puppeteer/issues/8477)) ([7dc8e37](https://github.com/puppeteer/puppeteer/commit/7dc8e37a23d025bb2c31efb9c060c7f6e00179b4))
+* only lookup `localhost` for DNS lookups ([1b025b4](https://github.com/puppeteer/puppeteer/commit/1b025b4c8466fe64da0fa2050eaa02b7764770b1))
+* robustly check for launch executable ([#8468](https://github.com/puppeteer/puppeteer/issues/8468)) ([b54dc55](https://github.com/puppeteer/puppeteer/commit/b54dc55f7622ee2b75afd3bd9fe118dd2f144f40))
+
+## [14.2.1](https://github.com/puppeteer/puppeteer/compare/v14.2.0...v14.2.1) (2022-06-02)
+
+
+### Bug Fixes
+
+* use isPageTargetCallback in Browser::pages() ([#8460](https://github.com/puppeteer/puppeteer/issues/8460)) ([5c9050a](https://github.com/puppeteer/puppeteer/commit/5c9050aea0fe8d57114130fe38bd33ed2b4955d6))
+
+## [14.2.0](https://github.com/puppeteer/puppeteer/compare/v14.1.2...v14.2.0) (2022-06-01)
+
+
+### Features
+
+* **chromium:** roll to Chromium 103.0.5059.0 (r1002410) ([#8410](https://github.com/puppeteer/puppeteer/issues/8410)) ([54efc2c](https://github.com/puppeteer/puppeteer/commit/54efc2c949be1d6ef22f4d2630620e33d14d2597))
+* support node 18 ([#8447](https://github.com/puppeteer/puppeteer/issues/8447)) ([f2d8276](https://github.com/puppeteer/puppeteer/commit/f2d8276d6e745a7547b8ce54c3f50934bb70de0b))
+* use strict typescript ([#8401](https://github.com/puppeteer/puppeteer/issues/8401)) ([b4e751f](https://github.com/puppeteer/puppeteer/commit/b4e751f29cb6fd4c3cc41fe702de83721f0eb6dc))
+
+
+### Bug Fixes
+
+* multiple same request event listener ([#8404](https://github.com/puppeteer/puppeteer/issues/8404)) ([9211015](https://github.com/puppeteer/puppeteer/commit/92110151d9a33f26abc07bc805f4f2f3943697a0))
+* NodeNext incompatibility in package.json ([#8445](https://github.com/puppeteer/puppeteer/issues/8445)) ([c4898a7](https://github.com/puppeteer/puppeteer/commit/c4898a7a2e69681baac55366848da6688f0d8790))
+* process documentation during publishing ([#8433](https://github.com/puppeteer/puppeteer/issues/8433)) ([d111d19](https://github.com/puppeteer/puppeteer/commit/d111d19f788d88d984dcf4ad7542f59acd2f4c1e))
+
+## [14.1.2](https://github.com/puppeteer/puppeteer/compare/v14.1.1...v14.1.2) (2022-05-30)
+
+
+### Bug Fixes
+
+* do not use loaderId for lifecycle events ([#8395](https://github.com/puppeteer/puppeteer/issues/8395)) ([c96c915](https://github.com/puppeteer/puppeteer/commit/c96c915b535dcf414038677bd3d3ed6b980a4901))
+* fix release-please bot ([#8400](https://github.com/puppeteer/puppeteer/issues/8400)) ([5c235c7](https://github.com/puppeteer/puppeteer/commit/5c235c701fc55380f09d09ac2cf63f2c94b60e3d))
+* use strict TS in Input.ts ([#8392](https://github.com/puppeteer/puppeteer/issues/8392)) ([af92a24](https://github.com/puppeteer/puppeteer/commit/af92a24ba9fc8efea1ba41f96d87515cf760da65))
+
+### [14.1.1](https://github.com/puppeteer/puppeteer/compare/v14.1.0...v14.1.1) (2022-05-19)
+
+
+### Bug Fixes
+
+* kill browser process when 'taskkill' fails on Windows ([#8352](https://github.com/puppeteer/puppeteer/issues/8352)) ([dccfadb](https://github.com/puppeteer/puppeteer/commit/dccfadb90e8947cae3f33d7a209b6f5752f97b46))
+* only check loading iframe in lifecycling ([#8348](https://github.com/puppeteer/puppeteer/issues/8348)) ([7438030](https://github.com/puppeteer/puppeteer/commit/74380303ac6cc6e2d84948a10920d56e665ccebe))
+* recompile before funit and unit commands ([#8363](https://github.com/puppeteer/puppeteer/issues/8363)) ([8735b78](https://github.com/puppeteer/puppeteer/commit/8735b784ba7838c1002b521a7f9f23bb27263d03)), closes [#8362](https://github.com/puppeteer/puppeteer/issues/8362)
+
+## [14.1.0](https://github.com/puppeteer/puppeteer/compare/v14.0.0...v14.1.0) (2022-05-13)
+
+
+### Features
+
+* add waitForXPath to ElementHandle ([#8329](https://github.com/puppeteer/puppeteer/issues/8329)) ([7eaadaf](https://github.com/puppeteer/puppeteer/commit/7eaadafe197279a7d1753e7274d2e24dfc11abdf))
+* allow handling other targets as pages internally ([#8336](https://github.com/puppeteer/puppeteer/issues/8336)) ([3b66a2c](https://github.com/puppeteer/puppeteer/commit/3b66a2c47ee36785a6a72c9afedd768fab3d040a))
+
+
+### Bug Fixes
+
+* disable AvoidUnnecessaryBeforeUnloadCheckSync to fix navigations ([#8330](https://github.com/puppeteer/puppeteer/issues/8330)) ([4854ad5](https://github.com/puppeteer/puppeteer/commit/4854ad5b15c9bdf93c06dcb758393e7cbacd7469))
+* If currentNode and root are the same, do not include them in the result ([#8332](https://github.com/puppeteer/puppeteer/issues/8332)) ([a61144d](https://github.com/puppeteer/puppeteer/commit/a61144d43780b5c32197427d7682b9b6c433f2bb))
+
+## [14.0.0](https://github.com/puppeteer/puppeteer/compare/v13.7.0...v14.0.0) (2022-05-09)
+
+
+### ⚠ BREAKING CHANGES
+
+* strict mode fixes for HTTPRequest/Response classes (#8297)
+* Node 12 is no longer supported.
+
+### Features
+
+* add support for Apple Silicon chromium builds ([#7546](https://github.com/puppeteer/puppeteer/issues/7546)) ([baa017d](https://github.com/puppeteer/puppeteer/commit/baa017db92b1fecf2e3584d5b3161371ae60f55b)), closes [#6622](https://github.com/puppeteer/puppeteer/issues/6622)
+* **chromium:** roll to Chromium 102.0.5002.0 (r991974) ([#8319](https://github.com/puppeteer/puppeteer/issues/8319)) ([be4c930](https://github.com/puppeteer/puppeteer/commit/be4c930c60164f681a966d0f8cb745f6c263fe2b))
+* support ES modules ([#8306](https://github.com/puppeteer/puppeteer/issues/8306)) ([6841bd6](https://github.com/puppeteer/puppeteer/commit/6841bd68d85e3b3952c5e7ce454ac4d23f84262d))
+
+
+### Bug Fixes
+
+* apparent typo SUPPORTER_PLATFORMS ([#8294](https://github.com/puppeteer/puppeteer/issues/8294)) ([e09287f](https://github.com/puppeteer/puppeteer/commit/e09287f4e9a1ff3c637dd165d65f221394970e2c))
+* make sure inner OOPIFs can be attached to ([#8304](https://github.com/puppeteer/puppeteer/issues/8304)) ([5539598](https://github.com/puppeteer/puppeteer/commit/553959884f4edb4deab760fa8ca38fc1c85c05c5))
+* strict mode fixes for HTTPRequest/Response classes ([#8297](https://github.com/puppeteer/puppeteer/issues/8297)) ([2804ae8](https://github.com/puppeteer/puppeteer/commit/2804ae8cdbc4c90bf942510bce656275a2d409e1)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769)
+* tests failing in headful ([#8273](https://github.com/puppeteer/puppeteer/issues/8273)) ([e841d7f](https://github.com/puppeteer/puppeteer/commit/e841d7f9f3f407c02dbc48e107b545b91db104e6))
+
+
+* drop Node 12 support ([#8299](https://github.com/puppeteer/puppeteer/issues/8299)) ([274bd6b](https://github.com/puppeteer/puppeteer/commit/274bd6b3b98c305ed014909d8053e4c54187971b))
+
+## [13.7.0](https://github.com/puppeteer/puppeteer/compare/v13.6.0...v13.7.0) (2022-04-28)
+
+
+### Features
+
+* add `back` and `forward` mouse buttons ([#8284](https://github.com/puppeteer/puppeteer/issues/8284)) ([7a51bff](https://github.com/puppeteer/puppeteer/commit/7a51bff47f6436fc29d0df7eb74f12f69102ca5b))
+* support chrome headless mode ([#8260](https://github.com/puppeteer/puppeteer/issues/8260)) ([1308d9a](https://github.com/puppeteer/puppeteer/commit/1308d9aa6a5920b20da02dca8db03c63e43c8b84))
+
+
+### Bug Fixes
+
+* doc typo ([#8263](https://github.com/puppeteer/puppeteer/issues/8263)) ([952a2ae](https://github.com/puppeteer/puppeteer/commit/952a2ae0bc4f059f8e8b4d1de809d0a486a74551))
+* use different test names for browser specific tests in launcher.spec.ts ([#8250](https://github.com/puppeteer/puppeteer/issues/8250)) ([c6cf1a9](https://github.com/puppeteer/puppeteer/commit/c6cf1a9f27621c8a619cfbdc9d0821541768ac94))
+
+## [13.6.0](https://github.com/puppeteer/puppeteer/compare/v13.5.2...v13.6.0) (2022-04-19)
+
+
+### Features
+
+* **chromium:** roll to Chromium 101.0.4950.0 (r982053) ([#8213](https://github.com/puppeteer/puppeteer/issues/8213)) ([ec74bd8](https://github.com/puppeteer/puppeteer/commit/ec74bd811d9b7fbaf600068e86f13a63d7b0bc6f))
+* respond multiple headers with same key ([#8183](https://github.com/puppeteer/puppeteer/issues/8183)) ([c1dcd85](https://github.com/puppeteer/puppeteer/commit/c1dcd857e3bc17769f02474a41bbedee01f471dc))
+
+
+### Bug Fixes
+
+* also kill Firefox when temporary profile is used ([#8233](https://github.com/puppeteer/puppeteer/issues/8233)) ([b6504d7](https://github.com/puppeteer/puppeteer/commit/b6504d7186336a2fc0b41c3878c843b7409ba5fb))
+* consider existing frames when waiting for a frame ([#8200](https://github.com/puppeteer/puppeteer/issues/8200)) ([0955225](https://github.com/puppeteer/puppeteer/commit/0955225b51421663288523a3dfb63103b51775b4))
+* disable bfcache in the launcher ([#8196](https://github.com/puppeteer/puppeteer/issues/8196)) ([9ac7318](https://github.com/puppeteer/puppeteer/commit/9ac7318506ac858b3465e9b4ede8ad75fbbcee11)), closes [#8182](https://github.com/puppeteer/puppeteer/issues/8182)
+* enable page.spec event handler test for firefox ([#8214](https://github.com/puppeteer/puppeteer/issues/8214)) ([2b45027](https://github.com/puppeteer/puppeteer/commit/2b45027d256f85f21a0c824183696b237e00ad33))
+* forget queuedEventGroup when emitting response in responseReceivedExtraInfo ([#8234](https://github.com/puppeteer/puppeteer/issues/8234)) ([#8239](https://github.com/puppeteer/puppeteer/issues/8239)) ([91a8e73](https://github.com/puppeteer/puppeteer/commit/91a8e73b1196e4128b1e7c25e08080f2faaf3cf7))
+* forget request will be sent from the _requestWillBeSentMap list. ([#8226](https://github.com/puppeteer/puppeteer/issues/8226)) ([4b786c9](https://github.com/puppeteer/puppeteer/commit/4b786c904cbfe3f059322292f3b788b8a5ebd9bf))
+* ignore favicon requests in page.spec event handler tests ([#8208](https://github.com/puppeteer/puppeteer/issues/8208)) ([04e5c88](https://github.com/puppeteer/puppeteer/commit/04e5c889973432c6163a8539cdec23c0e8726bff))
+* **network.spec.ts:** typo in the word should ([#8223](https://github.com/puppeteer/puppeteer/issues/8223)) ([e93faad](https://github.com/puppeteer/puppeteer/commit/e93faadc21b7fcb1e03b69c451c28b769f9cde51))
+
+### [13.5.2](https://github.com/puppeteer/puppeteer/compare/v13.5.1...v13.5.2) (2022-03-31)
+
+
+### Bug Fixes
+
+* chromium downloading hung at 99% ([#8169](https://github.com/puppeteer/puppeteer/issues/8169)) ([8f13470](https://github.com/puppeteer/puppeteer/commit/8f13470af06045857f32496f03e77b14f3ecff98))
+* get extra headers from Fetch.requestPaused event ([#8162](https://github.com/puppeteer/puppeteer/issues/8162)) ([37ede68](https://github.com/puppeteer/puppeteer/commit/37ede6877017a8dc6c946a3dff4ec6d79c3ebc59))
+
+### [13.5.1](https://github.com/puppeteer/puppeteer/compare/v13.5.0...v13.5.1) (2022-03-09)
+
+
+### Bug Fixes
+
+* waitForNavigation in OOPIFs ([#8117](https://github.com/puppeteer/puppeteer/issues/8117)) ([34775e5](https://github.com/puppeteer/puppeteer/commit/34775e58316be49d8bc5a13209a1f570bc66b448))
+
+## [13.5.0](https://github.com/puppeteer/puppeteer/compare/v13.4.1...v13.5.0) (2022-03-07)
+
+
+### Features
+
+* **chromium:** roll to Chromium 100.0.4889.0 (r970485) ([#8108](https://github.com/puppeteer/puppeteer/issues/8108)) ([d12f427](https://github.com/puppeteer/puppeteer/commit/d12f42754f7013b5ec0a2198cf2d9cf945d3cb38))
+
+
+### Bug Fixes
+
+* Inherit browser-level proxy settings from incognito context ([#7770](https://github.com/puppeteer/puppeteer/issues/7770)) ([3feca32](https://github.com/puppeteer/puppeteer/commit/3feca325a9472ee36f7e866ebe375c7f083e0e36))
+* **page:** page.createIsolatedWorld error catching has been added ([#7848](https://github.com/puppeteer/puppeteer/issues/7848)) ([309e8b8](https://github.com/puppeteer/puppeteer/commit/309e8b80da0519327bc37b44a3ebb6f2e2d357a7))
+* **tests:** ensure all tests honour BINARY envvar ([#8092](https://github.com/puppeteer/puppeteer/issues/8092)) ([3b8b9ad](https://github.com/puppeteer/puppeteer/commit/3b8b9adde5d18892af96329b6f9303979f9c04f5))
+
+### [13.4.1](https://github.com/puppeteer/puppeteer/compare/v13.4.0...v13.4.1) (2022-03-01)
+
+
+### Bug Fixes
+
+* regression in --user-data-dir handling ([#8060](https://github.com/puppeteer/puppeteer/issues/8060)) ([85decdc](https://github.com/puppeteer/puppeteer/commit/85decdc28d7d2128e6d2946a72f4d99dd5dbb48a))
+
+## [13.4.0](https://github.com/puppeteer/puppeteer/compare/v13.3.2...v13.4.0) (2022-02-22)
+
+
+### Features
+
+* add support for async waitForTarget ([#7885](https://github.com/puppeteer/puppeteer/issues/7885)) ([dbf0639](https://github.com/puppeteer/puppeteer/commit/dbf0639822d0b2736993de52c0bfe1dbf4e58f25))
+* export `Frame._client` through getter ([#8041](https://github.com/puppeteer/puppeteer/issues/8041)) ([e9278fc](https://github.com/puppeteer/puppeteer/commit/e9278fcfcffe2558de63ce7542483445bcb6e74f))
+* **HTTPResponse:** expose timing information ([#8025](https://github.com/puppeteer/puppeteer/issues/8025)) ([30b3d49](https://github.com/puppeteer/puppeteer/commit/30b3d49b0de46d812b7485e708174a07c73dbdd0))
+
+
+### Bug Fixes
+
+* change kill to signal the whole process group to terminate ([#6859](https://github.com/puppeteer/puppeteer/issues/6859)) ([0eb9c78](https://github.com/puppeteer/puppeteer/commit/0eb9c7861717ebba7012c03e76b7a46063e4e5dd))
+* element screenshot issue in headful mode ([#8018](https://github.com/puppeteer/puppeteer/issues/8018)) ([5346e70](https://github.com/puppeteer/puppeteer/commit/5346e70ffc15b33c1949657cf1b465f1acc5d84d)), closes [#7999](https://github.com/puppeteer/puppeteer/issues/7999)
+* ensure dom binding is not called after detach ([#8024](https://github.com/puppeteer/puppeteer/issues/8024)) ([5c308b0](https://github.com/puppeteer/puppeteer/commit/5c308b0704123736ddb085f97596c201ea18cf4a)), closes [#7814](https://github.com/puppeteer/puppeteer/issues/7814)
+* use both __dirname and require.resolve to support different bundlers ([#8046](https://github.com/puppeteer/puppeteer/issues/8046)) ([e6a6295](https://github.com/puppeteer/puppeteer/commit/e6a6295d9a7480bb59ee58a2cc7785171fa0fa2c)), closes [#8044](https://github.com/puppeteer/puppeteer/issues/8044)
+
+### [13.3.2](https://github.com/puppeteer/puppeteer/compare/v13.3.1...v13.3.2) (2022-02-14)
+
+
+### Bug Fixes
+
+* always use ENV executable path when present ([#7985](https://github.com/puppeteer/puppeteer/issues/7985)) ([6d6ea9b](https://github.com/puppeteer/puppeteer/commit/6d6ea9bf59daa3fb851b3da8baa27887e0aa2c28))
+* use require.resolve instead of __dirname ([#8003](https://github.com/puppeteer/puppeteer/issues/8003)) ([bbb186d](https://github.com/puppeteer/puppeteer/commit/bbb186d88cb99e4914299c983c822fa41a80f356))
+
+### [13.3.1](https://github.com/puppeteer/puppeteer/compare/v13.3.0...v13.3.1) (2022-02-10)
+
+
+### Bug Fixes
+
+* **puppeteer:** revert: esm modules ([#7986](https://github.com/puppeteer/puppeteer/issues/7986)) ([179eded](https://github.com/puppeteer/puppeteer/commit/179ededa1400c35c1f2edc015548e0f2a1bcee14))
+
+## [13.3.0](https://github.com/puppeteer/puppeteer/compare/v13.2.0...v13.3.0) (2022-02-09)
+
+
+### Features
+
+* **puppeteer:** export esm modules in package.json ([#7964](https://github.com/puppeteer/puppeteer/issues/7964)) ([523b487](https://github.com/puppeteer/puppeteer/commit/523b487e8802824cecff86d256b4f7dbc4c47c8a))
+
+## [13.2.0](https://github.com/puppeteer/puppeteer/compare/v13.1.3...v13.2.0) (2022-02-07)
+
+
+### Features
+
+* add more models to DeviceDescriptors ([#7904](https://github.com/puppeteer/puppeteer/issues/7904)) ([6a655cb](https://github.com/puppeteer/puppeteer/commit/6a655cb647e12eaf1055be0b298908d83bebac25))
+* **chromium:** roll to Chromium 99.0.4844.16 (r961656) ([#7960](https://github.com/puppeteer/puppeteer/issues/7960)) ([96c3f94](https://github.com/puppeteer/puppeteer/commit/96c3f943b2f6e26bd871ecfcce71b6a33e214ebf))
+
+
+### Bug Fixes
+
+* make projectRoot optional in Puppeteer and launchers ([#7967](https://github.com/puppeteer/puppeteer/issues/7967)) ([9afdc63](https://github.com/puppeteer/puppeteer/commit/9afdc6300b80f01091dc4cb42d4ebe952c7d60f0))
+* migrate more files to strict-mode TypeScript ([#7950](https://github.com/puppeteer/puppeteer/issues/7950)) ([aaac8d9](https://github.com/puppeteer/puppeteer/commit/aaac8d9c44327a2c503ffd6c97b7f21e8010c3e4))
+* typos in documentation ([#7968](https://github.com/puppeteer/puppeteer/issues/7968)) ([41ab4e9](https://github.com/puppeteer/puppeteer/commit/41ab4e9127df64baa6c43ecde2f7ddd702ba7b0c))
+
+### [13.1.3](https://github.com/puppeteer/puppeteer/compare/v13.1.2...v13.1.3) (2022-01-31)
+
+
+### Bug Fixes
+
+* issue with reading versions.js in doclint ([#7940](https://github.com/puppeteer/puppeteer/issues/7940)) ([06ba963](https://github.com/puppeteer/puppeteer/commit/06ba9632a4c63859244068d32c312817d90daf63))
+* make more files work in strict-mode TypeScript ([#7936](https://github.com/puppeteer/puppeteer/issues/7936)) ([0636513](https://github.com/puppeteer/puppeteer/commit/0636513e34046f4d40b5e88beb2b18b16dab80aa))
+* page.pdf producing an invalid pdf ([#7868](https://github.com/puppeteer/puppeteer/issues/7868)) ([afea509](https://github.com/puppeteer/puppeteer/commit/afea509544fb99bfffe5b0bebe6f3575c53802f0)), closes [#7757](https://github.com/puppeteer/puppeteer/issues/7757)
+
+### [13.1.2](https://github.com/puppeteer/puppeteer/compare/v13.1.1...v13.1.2) (2022-01-25)
+
+
+### Bug Fixes
+
+* **package.json:** update node-fetch package ([#7924](https://github.com/puppeteer/puppeteer/issues/7924)) ([e4c48d3](https://github.com/puppeteer/puppeteer/commit/e4c48d3b8c2a812752094ed8163e4f2f32c4b6cb))
+* types in Browser.ts to be compatible with strict mode Typescript ([#7918](https://github.com/puppeteer/puppeteer/issues/7918)) ([a8ec0aa](https://github.com/puppeteer/puppeteer/commit/a8ec0aadc9c90d224d568d9e418d14261e6e85b1)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769)
+* types in Connection.ts to be compatible with strict mode Typescript ([#7919](https://github.com/puppeteer/puppeteer/issues/7919)) ([d80d602](https://github.com/puppeteer/puppeteer/commit/d80d6027ea8e1b7fcdaf045398629cf8e6512658)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769)
+
+### [13.1.1](https://github.com/puppeteer/puppeteer/compare/v13.1.0...v13.1.1) (2022-01-18)
+
+
+### Bug Fixes
+
+* use content box for OOPIF offset calculations ([#7911](https://github.com/puppeteer/puppeteer/issues/7911)) ([344feb5](https://github.com/puppeteer/puppeteer/commit/344feb53c28ce018a4c600d408468f6d9d741eee))
+
+## [13.1.0](https://github.com/puppeteer/puppeteer/compare/v13.0.1...v13.1.0) (2022-01-17)
+
+
+### Features
+
+* **chromium:** roll to Chromium 98.0.4758.0 (r950341) ([#7907](https://github.com/puppeteer/puppeteer/issues/7907)) ([a55c86f](https://github.com/puppeteer/puppeteer/commit/a55c86fac504b5e89ba23735fb3a1b1d54a4e1e5))
+
+
+### Bug Fixes
+
+* apply OOPIF offsets to bounding box and box model calls ([#7906](https://github.com/puppeteer/puppeteer/issues/7906)) ([a566263](https://github.com/puppeteer/puppeteer/commit/a566263ba28e58ff648bffbdb628606f75d5876f))
+* correctly compute clickable points for elements inside OOPIFs ([#7900](https://github.com/puppeteer/puppeteer/issues/7900)) ([486bbe0](https://github.com/puppeteer/puppeteer/commit/486bbe010d5ee5c446d9e8daf61a080232379c3f)), closes [#7849](https://github.com/puppeteer/puppeteer/issues/7849)
+* error for pre-existing OOPIFs ([#7899](https://github.com/puppeteer/puppeteer/issues/7899)) ([d7937b8](https://github.com/puppeteer/puppeteer/commit/d7937b806d331bf16c2016aaf16e932b1334eac8)), closes [#7844](https://github.com/puppeteer/puppeteer/issues/7844) [#7896](https://github.com/puppeteer/puppeteer/issues/7896)
+
+### [13.0.1](https://github.com/puppeteer/puppeteer/compare/v13.0.0...v13.0.1) (2021-12-22)
+
+
+### Bug Fixes
+
+* disable a test failing on Firefox ([#7846](https://github.com/puppeteer/puppeteer/issues/7846)) ([36207c5](https://github.com/puppeteer/puppeteer/commit/36207c5efe8ca21f4b3fc5b00212700326a701d2))
+* make sure ElementHandle.waitForSelector is evaluated in the right context ([#7843](https://github.com/puppeteer/puppeteer/issues/7843)) ([8d8e874](https://github.com/puppeteer/puppeteer/commit/8d8e874b072b17fc763f33d08e51c046b7435244))
+* predicate arguments for waitForFunction ([#7845](https://github.com/puppeteer/puppeteer/issues/7845)) ([1c44551](https://github.com/puppeteer/puppeteer/commit/1c44551f1b5bb19455b4a1eb7061715717ec880e)), closes [#7836](https://github.com/puppeteer/puppeteer/issues/7836)
+
+## [13.0.0](https://github.com/puppeteer/puppeteer/compare/v12.0.1...v13.0.0) (2021-12-10)
+
+
+### ⚠ BREAKING CHANGES
+
+* typo in 'already-handled' constant of the request interception API (#7813)
+
+### Features
+
+* expose HTTPRequest intercept resolution state and clarify docs ([#7796](https://github.com/puppeteer/puppeteer/issues/7796)) ([dc23b75](https://github.com/puppeteer/puppeteer/commit/dc23b7535cb958c00d1eecfe85b4ee26e52e2e39))
+* implement Element.waitForSelector ([#7825](https://github.com/puppeteer/puppeteer/issues/7825)) ([c034294](https://github.com/puppeteer/puppeteer/commit/c03429444d05b39549489ad3da67d93b2be59f51))
+
+
+### Bug Fixes
+
+* handle multiple/duplicate Fetch.requestPaused events ([#7802](https://github.com/puppeteer/puppeteer/issues/7802)) ([636b086](https://github.com/puppeteer/puppeteer/commit/636b0863a169da132e333eb53b17eb2601daabe6)), closes [#7475](https://github.com/puppeteer/puppeteer/issues/7475) [#6696](https://github.com/puppeteer/puppeteer/issues/6696) [#7225](https://github.com/puppeteer/puppeteer/issues/7225)
+* revert "feat(typescript): allow using puppeteer without dom lib" ([02c9af6](https://github.com/puppeteer/puppeteer/commit/02c9af62d64060a83f53368640f343ae2e30e38a)), closes [#6998](https://github.com/puppeteer/puppeteer/issues/6998)
+* typo in 'already-handled' constant of the request interception API ([#7813](https://github.com/puppeteer/puppeteer/issues/7813)) ([8242422](https://github.com/puppeteer/puppeteer/commit/824242246de9e158aacb85f71350a79cb386ed92)), closes [#7745](https://github.com/puppeteer/puppeteer/issues/7745) [#7747](https://github.com/puppeteer/puppeteer/issues/7747) [#7780](https://github.com/puppeteer/puppeteer/issues/7780)
+
+### [12.0.1](https://github.com/puppeteer/puppeteer/compare/v12.0.0...v12.0.1) (2021-11-29)
+
+
+### Bug Fixes
+
+* handle extraInfo events even if event.hasExtraInfo === false ([#7808](https://github.com/puppeteer/puppeteer/issues/7808)) ([6ee2feb](https://github.com/puppeteer/puppeteer/commit/6ee2feb1eafdd399f0af50cdc4517f21bcb55121)), closes [#7805](https://github.com/puppeteer/puppeteer/issues/7805)
+
+## [12.0.0](https://github.com/puppeteer/puppeteer/compare/v11.0.0...v12.0.0) (2021-11-26)
+
+
+### ⚠ BREAKING CHANGES
+
+* **chromium:** roll to Chromium 97.0.4692.0 (r938248)
+
+### Features
+
+* **chromium:** roll to Chromium 97.0.4692.0 (r938248) ([ac162c5](https://github.com/puppeteer/puppeteer/commit/ac162c561ee43dd69eff38e1b354a41bb42c9eba)), closes [#7458](https://github.com/puppeteer/puppeteer/issues/7458)
+* support for custom user data (profile) directory for Firefox ([#7684](https://github.com/puppeteer/puppeteer/issues/7684)) ([790c7a0](https://github.com/puppeteer/puppeteer/commit/790c7a0eb92291efebaa37e80c72f5cb5f46bbdb))
+
+
+### Bug Fixes
+
+* **ariaqueryhandler:** allow single quotes in aria attribute selector ([#7750](https://github.com/puppeteer/puppeteer/issues/7750)) ([b0319ec](https://github.com/puppeteer/puppeteer/commit/b0319ecc89f8ea3d31ab9aee5e1cd33d2a4e62be)), closes [#7721](https://github.com/puppeteer/puppeteer/issues/7721)
+* clearer jsdoc for behavior of `headless` when `devtools` is true ([#7748](https://github.com/puppeteer/puppeteer/issues/7748)) ([9f9b4ed](https://github.com/puppeteer/puppeteer/commit/9f9b4ed72ab0bb43d002a0024122d6f5eab231aa))
+* null check for frame in FrameManager ([#7773](https://github.com/puppeteer/puppeteer/issues/7773)) ([23ee295](https://github.com/puppeteer/puppeteer/commit/23ee295f348d114617f2a86d0bb792936f413ac5)), closes [#7749](https://github.com/puppeteer/puppeteer/issues/7749)
+* only kill the process when there is no browser instance available ([#7762](https://github.com/puppeteer/puppeteer/issues/7762)) ([51e6169](https://github.com/puppeteer/puppeteer/commit/51e61696c1c20cc09bd4fc068ae1dfa259c41745)), closes [#7668](https://github.com/puppeteer/puppeteer/issues/7668)
+* parse statusText from the extraInfo event ([#7798](https://github.com/puppeteer/puppeteer/issues/7798)) ([a26b12b](https://github.com/puppeteer/puppeteer/commit/a26b12b7c775c36271cd4c98e39bbd59f4356320)), closes [#7458](https://github.com/puppeteer/puppeteer/issues/7458)
+* try to remove the temporary user data directory after the process has been killed ([#7761](https://github.com/puppeteer/puppeteer/issues/7761)) ([fc94a28](https://github.com/puppeteer/puppeteer/commit/fc94a28778cfdb3cb8bcd882af3ebcdacf85c94e))
+
+## [11.0.0](https://github.com/puppeteer/puppeteer/compare/v10.4.0...v11.0.0) (2021-11-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* **oop iframes:** integrate OOP iframes with the frame manager (#7556)
+
+### Features
+
+* improve error message for response.buffer() ([#7669](https://github.com/puppeteer/puppeteer/issues/7669)) ([03c9ecc](https://github.com/puppeteer/puppeteer/commit/03c9ecca400a02684cd60229550dbad1190a5b6e))
+* **oop iframes:** integrate OOP iframes with the frame manager ([#7556](https://github.com/puppeteer/puppeteer/issues/7556)) ([4d9dc8c](https://github.com/puppeteer/puppeteer/commit/4d9dc8c0e613f22d4cdf237e8bd0b0da3c588edb)), closes [#2548](https://github.com/puppeteer/puppeteer/issues/2548)
+* add custom debugging port option ([#4993](https://github.com/puppeteer/puppeteer/issues/4993)) ([26145e9](https://github.com/puppeteer/puppeteer/commit/26145e9a24af7caed6ece61031f2cafa6abd505f))
+* add initiator to HTTPRequest ([#7614](https://github.com/puppeteer/puppeteer/issues/7614)) ([a271145](https://github.com/puppeteer/puppeteer/commit/a271145b0663ef9de1903dd0eb9fd5366465bed7))
+* allow to customize tmpdir ([#7243](https://github.com/puppeteer/puppeteer/issues/7243)) ([b1f6e86](https://github.com/puppeteer/puppeteer/commit/b1f6e8692b0bc7e8551b2a78169c830cd80a7acb))
+* handle unhandled promise rejections in tests ([#7722](https://github.com/puppeteer/puppeteer/issues/7722)) ([07febca](https://github.com/puppeteer/puppeteer/commit/07febca04b391893cfc872250e4391da142d4fe2))
+
+
+### Bug Fixes
+
+* add support for relative install paths to BrowserFetcher ([#7613](https://github.com/puppeteer/puppeteer/issues/7613)) ([eebf452](https://github.com/puppeteer/puppeteer/commit/eebf452d38b79bb2ea1a1ba84c3d2ea6f2f9f899)), closes [#7592](https://github.com/puppeteer/puppeteer/issues/7592)
+* add webp to screenshot quality option allow list ([#7631](https://github.com/puppeteer/puppeteer/issues/7631)) ([b20c2bf](https://github.com/puppeteer/puppeteer/commit/b20c2bfa24cbdd4a1b9cefca2e0a9407e442baf5))
+* prevent Target closed errors on streams ([#7728](https://github.com/puppeteer/puppeteer/issues/7728)) ([5b792de](https://github.com/puppeteer/puppeteer/commit/5b792de7a97611441777d1ac99cb95516301d7dc))
+* request an animation frame to fix flaky clickablePoint test ([#7587](https://github.com/puppeteer/puppeteer/issues/7587)) ([7341d9f](https://github.com/puppeteer/puppeteer/commit/7341d9fadd1466a5b2f2bde8631f3b02cf9a7d8a))
+* setup husky properly ([#7727](https://github.com/puppeteer/puppeteer/issues/7727)) ([8b712e7](https://github.com/puppeteer/puppeteer/commit/8b712e7b642b58193437f26d4e104a9e412f388d)), closes [#7726](https://github.com/puppeteer/puppeteer/issues/7726)
+* updated troubleshooting.md to meet latest dependencies changes ([#7656](https://github.com/puppeteer/puppeteer/issues/7656)) ([edb0197](https://github.com/puppeteer/puppeteer/commit/edb01972b9606d8b05b979a588eda0d622315981))
+* **launcher:** launcher.launch() should pass 'timeout' option [#5180](https://github.com/puppeteer/puppeteer/issues/5180) ([#7596](https://github.com/puppeteer/puppeteer/issues/7596)) ([113489d](https://github.com/puppeteer/puppeteer/commit/113489d3b58e2907374a4e6e5133bf46630695d1))
+* **page:** fallback to default in exposeFunction when using imported module ([#6365](https://github.com/puppeteer/puppeteer/issues/6365)) ([44c9ec6](https://github.com/puppeteer/puppeteer/commit/44c9ec67c57dccf3e186c86f14f3a8da9a8eb971))
+* **page:** fix page.off method for request event ([#7624](https://github.com/puppeteer/puppeteer/issues/7624)) ([d0cb943](https://github.com/puppeteer/puppeteer/commit/d0cb9436a302418086f6763e0e58ae3732a20b62)), closes [#7572](https://github.com/puppeteer/puppeteer/issues/7572)
+
+## [10.4.0](https://github.com/puppeteer/puppeteer/compare/v10.2.0...v10.4.0) (2021-09-21)
+
+
+### Features
+
+* add webp to screenshot options ([#7565](https://github.com/puppeteer/puppeteer/issues/7565)) ([43a9268](https://github.com/puppeteer/puppeteer/commit/43a926832505a57922016907a264165676424557))
+* **page:** expose page.client() ([#7582](https://github.com/puppeteer/puppeteer/issues/7582)) ([99ca842](https://github.com/puppeteer/puppeteer/commit/99ca842124a1edef5e66426621885141a9feaca5))
+* **page:** mark page.client() as internal ([#7585](https://github.com/puppeteer/puppeteer/issues/7585)) ([8451951](https://github.com/puppeteer/puppeteer/commit/84519514831f304f9076ca235fe474f797616b2c))
+* add ability to specify offsets for JSHandle.click ([#7573](https://github.com/puppeteer/puppeteer/issues/7573)) ([2b5c001](https://github.com/puppeteer/puppeteer/commit/2b5c0019dc3744196c5858edeaa901dff9973ef5))
+* add durableStorage to allowed permissions ([#5295](https://github.com/puppeteer/puppeteer/issues/5295)) ([eda5171](https://github.com/puppeteer/puppeteer/commit/eda51712790b9260626dc53cfb58a72805c45582))
+* add id option to addScriptTag ([#5477](https://github.com/puppeteer/puppeteer/issues/5477)) ([300be5d](https://github.com/puppeteer/puppeteer/commit/300be5d167b6e7e532e725fdb86966081a5d0093))
+* add more Android models to DeviceDescriptors ([#7210](https://github.com/puppeteer/puppeteer/issues/7210)) ([b5020dc](https://github.com/puppeteer/puppeteer/commit/b5020dc04121b265c77662237dfb177d6de06053)), closes [/github.com/aerokube/moon-deploy/blob/master/moon-local.yaml#L199](https://github.com/puppeteer//github.com/aerokube/moon-deploy/blob/master/moon-local.yaml/issues/L199)
+* add proxy and bypass list parameters to createIncognitoBrowserContext ([#7516](https://github.com/puppeteer/puppeteer/issues/7516)) ([8e45a1c](https://github.com/puppeteer/puppeteer/commit/8e45a1c882207cc36e87be2a917b661eb841c4bf)), closes [#678](https://github.com/puppeteer/puppeteer/issues/678)
+* add threshold to Page.isIntersectingViewport ([#6497](https://github.com/puppeteer/puppeteer/issues/6497)) ([54c4318](https://github.com/puppeteer/puppeteer/commit/54c43180161c3c512e4698e7f2e85ce3c6f0ab50))
+* add unit test support for bisect ([#7553](https://github.com/puppeteer/puppeteer/issues/7553)) ([a0b1f6b](https://github.com/puppeteer/puppeteer/commit/a0b1f6b401abae2fbc5a8987061644adfaa7b482))
+* add User-Agent with Puppeteer version to WebSocket request ([#5614](https://github.com/puppeteer/puppeteer/issues/5614)) ([6a2bf0a](https://github.com/puppeteer/puppeteer/commit/6a2bf0aabaa4df72c7838f5a6cd742e8f9c72be6))
+* extend husky checks ([#7574](https://github.com/puppeteer/puppeteer/issues/7574)) ([7316086](https://github.com/puppeteer/puppeteer/commit/73160869417275200be19bd37372b6218dbc5f63))
+* **api:** implement `Page.waitForNetworkIdle()` ([#5140](https://github.com/puppeteer/puppeteer/issues/5140)) ([3c6029c](https://github.com/puppeteer/puppeteer/commit/3c6029c702291ca7ef637b66e78d72e03156fe58))
+* **coverage:** option for raw V8 script coverage ([#6454](https://github.com/puppeteer/puppeteer/issues/6454)) ([cb4470a](https://github.com/puppeteer/puppeteer/commit/cb4470a6d9b0a7f73836458bb3d5779eb85ac5f2))
+* support timeout for page.pdf() call ([#7508](https://github.com/puppeteer/puppeteer/issues/7508)) ([f90af66](https://github.com/puppeteer/puppeteer/commit/f90af6639d801e764bdb479b9543b7f8f2b926df))
+* **typescript:** allow using puppeteer without dom lib ([#6998](https://github.com/puppeteer/puppeteer/issues/6998)) ([723052d](https://github.com/puppeteer/puppeteer/commit/723052d5bb3c3d1d3908508467512bea4d8fdc80)), closes [#6989](https://github.com/puppeteer/puppeteer/issues/6989)
+
+
+### Bug Fixes
+
+* **docs:** deploy includes website documentation ([#7469](https://github.com/puppeteer/puppeteer/issues/7469)) ([6fde41c](https://github.com/puppeteer/puppeteer/commit/6fde41c6b6657986df1bbce3f2e0f7aa499f2be4))
+* **docs:** names in version 9.1.1 ([#7517](https://github.com/puppeteer/puppeteer/issues/7517)) ([44b22bb](https://github.com/puppeteer/puppeteer/commit/44b22bbc2629e3c75c1494b299a66790b371fb0a))
+* **frame:** fix Frame.waitFor's XPath pattern detection ([#5184](https://github.com/puppeteer/puppeteer/issues/5184)) ([caa2b73](https://github.com/puppeteer/puppeteer/commit/caa2b732fe58f32ec03f2a9fa8568f20188203c5))
+* **install:** respect environment proxy config when downloading Firef… ([#6577](https://github.com/puppeteer/puppeteer/issues/6577)) ([9399c97](https://github.com/puppeteer/puppeteer/commit/9399c9786fba4e45e1c5485ddbb197d2d4f1735f)), closes [#6573](https://github.com/puppeteer/puppeteer/issues/6573)
+* added names in V9.1.1 ([#7547](https://github.com/puppeteer/puppeteer/issues/7547)) ([d132b8b](https://github.com/puppeteer/puppeteer/commit/d132b8b041696e6d5b9a99d0be1acf1cf943efef))
+* **test:** tweak waitForNetworkIdle delay in test between downloads ([#7564](https://github.com/puppeteer/puppeteer/issues/7564)) ([a21b737](https://github.com/puppeteer/puppeteer/commit/a21b7376e7feaf23066d67948d52480516f42496))
+* **types:** allow evaluate functions to take a readonly array as an argument ([#7072](https://github.com/puppeteer/puppeteer/issues/7072)) ([491614c](https://github.com/puppeteer/puppeteer/commit/491614c7f8cfa50b902d0275064e611c2a48c3b2))
+* update firefox prefs documentation link ([#7539](https://github.com/puppeteer/puppeteer/issues/7539)) ([2aec355](https://github.com/puppeteer/puppeteer/commit/2aec35553bc6e0305f40837bb3665ddbd02aa889))
+* use non-deprecated tracing categories api ([#7413](https://github.com/puppeteer/puppeteer/issues/7413)) ([040a0e5](https://github.com/puppeteer/puppeteer/commit/040a0e561b4f623f7929130b90be129f94ebb642))
+
+## [10.2.0](https://github.com/puppeteer/puppeteer/compare/v10.1.0...v10.2.0) (2021-08-04)
+
+
+### Features
+
+* **api:** make `page.isDragInterceptionEnabled` a method ([#7419](https://github.com/puppeteer/puppeteer/issues/7419)) ([dd470c7](https://github.com/puppeteer/puppeteer/commit/dd470c7a226a8422a938a7b0fffa58ffc6b78512)), closes [#7150](https://github.com/puppeteer/puppeteer/issues/7150)
+* **chromium:** roll to Chromium 93.0.4577.0 (r901912) ([#7387](https://github.com/puppeteer/puppeteer/issues/7387)) ([e10faad](https://github.com/puppeteer/puppeteer/commit/e10faad4f239b1120491bb54fcba0216acd3a646))
+* add channel parameter for puppeteer.launch ([#7389](https://github.com/puppeteer/puppeteer/issues/7389)) ([d70f60e](https://github.com/puppeteer/puppeteer/commit/d70f60e0619b8659d191fa492e3db4bc221ae982))
+* add cooperative request intercepts ([#6735](https://github.com/puppeteer/puppeteer/issues/6735)) ([b5e6474](https://github.com/puppeteer/puppeteer/commit/b5e6474374ae6a88fc73cdb1a9906764c2ac5d70))
+* add support for useragentdata ([#7378](https://github.com/puppeteer/puppeteer/issues/7378)) ([7200b1a](https://github.com/puppeteer/puppeteer/commit/7200b1a6fb9dfdfb65d50f0000339333e71b1b2a))
+
+
+### Bug Fixes
+
+* **browser-runner:** reject promise on error ([#7338](https://github.com/puppeteer/puppeteer/issues/7338)) ([5eb20e2](https://github.com/puppeteer/puppeteer/commit/5eb20e29a21ea0e0368fa8937ef38f7c7693ab34))
+* add script to remove html comments from docs markdown ([#7394](https://github.com/puppeteer/puppeteer/issues/7394)) ([ea3df80](https://github.com/puppeteer/puppeteer/commit/ea3df80ed136a03d7698d2319106af5df8d48b58))
+
+## [10.1.0](https://github.com/puppeteer/puppeteer/compare/v10.0.0...v10.1.0) (2021-06-29)
+
+
+### Features
+
+* add a streaming version for page.pdf ([e3699e2](https://github.com/puppeteer/puppeteer/commit/e3699e248bc9c1f7a6ead9a07d68ae8b65905443))
+* add drag-and-drop support ([#7150](https://github.com/puppeteer/puppeteer/issues/7150)) ([a91b8ac](https://github.com/puppeteer/puppeteer/commit/a91b8aca3728b2c2e310e9446897d729bf983377))
+* add page.emulateCPUThrottling ([#7343](https://github.com/puppeteer/puppeteer/issues/7343)) ([4ce4110](https://github.com/puppeteer/puppeteer/commit/4ce41106288938b9d366c550e7a424812920683d))
+
+
+### Bug Fixes
+
+* remove redundant await while fetching target ([#7351](https://github.com/puppeteer/puppeteer/issues/7351)) ([083b297](https://github.com/puppeteer/puppeteer/commit/083b297a6741c6b1dd23867f441130655fac8f7d))
+
+## [10.0.0](https://github.com/puppeteer/puppeteer/compare/v9.1.1...v10.0.0) (2021-05-31)
+
+
+### ⚠ BREAKING CHANGES
+
+* Node.js 10 is no longer supported.
+
+### Features
+
+* **chromium:** roll to Chromium 92.0.4512.0 (r884014) ([#7288](https://github.com/puppeteer/puppeteer/issues/7288)) ([f863f4b](https://github.com/puppeteer/puppeteer/commit/f863f4bfe015e57ea1f9fbb322f1cedee468b857))
+* **requestinterception:** remove cacheSafe flag ([#7217](https://github.com/puppeteer/puppeteer/issues/7217)) ([d01aa6c](https://github.com/puppeteer/puppeteer/commit/d01aa6c84a1e41f15ffed3a8d36ad26a404a7187))
+* expose other sessions from connection ([#6863](https://github.com/puppeteer/puppeteer/issues/6863)) ([cb285a2](https://github.com/puppeteer/puppeteer/commit/cb285a237921259eac99ade1d8b5550e068a55eb))
+* **launcher:** add new launcher option `waitForInitialPage` ([#7105](https://github.com/puppeteer/puppeteer/issues/7105)) ([2605309](https://github.com/puppeteer/puppeteer/commit/2605309f74b43da160cda4d214016e4422bf7676)), closes [#3630](https://github.com/puppeteer/puppeteer/issues/3630)
+
+
+### Bug Fixes
+
+* added comments for browsercontext, startCSSCoverage, and startJSCoverage. ([#7264](https://github.com/puppeteer/puppeteer/issues/7264)) ([b750397](https://github.com/puppeteer/puppeteer/commit/b75039746ac6bddf1411538242b5e70b0f2e6e8a))
+* modified comment for method product, platform and newPage ([#7262](https://github.com/puppeteer/puppeteer/issues/7262)) ([159d283](https://github.com/puppeteer/puppeteer/commit/159d2835450697dabea6f9adf6e67d158b5b8ae3))
+* **requestinterception:** fix font loading issue ([#7060](https://github.com/puppeteer/puppeteer/issues/7060)) ([c9978d2](https://github.com/puppeteer/puppeteer/commit/c9978d20d5584c9fd2dc902e4b4ac86ed8ea5d6e)), closes [/github.com/puppeteer/puppeteer/pull/6996#issuecomment-811546501](https://github.com/puppeteer//github.com/puppeteer/puppeteer/pull/6996/issues/issuecomment-811546501) [/github.com/puppeteer/puppeteer/pull/6996#issuecomment-813797393](https://github.com/puppeteer//github.com/puppeteer/puppeteer/pull/6996/issues/issuecomment-813797393) [#7038](https://github.com/puppeteer/puppeteer/issues/7038)
+
+
+* drop support for Node.js 10 ([#7200](https://github.com/puppeteer/puppeteer/issues/7200)) ([97c9fe2](https://github.com/puppeteer/puppeteer/commit/97c9fe2520723d45a5a86da06b888ae888d400be)), closes [#6753](https://github.com/puppeteer/puppeteer/issues/6753)
+
+### [9.1.1](https://github.com/puppeteer/puppeteer/compare/v9.1.0...v9.1.1) (2021-05-05)
+
+
+### Bug Fixes
+
+* make targetFilter synchronous ([#7203](https://github.com/puppeteer/puppeteer/issues/7203)) ([bcc85a0](https://github.com/puppeteer/puppeteer/commit/bcc85a0969077d122e5d8d2fb5c1061999a8ae48))
+
+## [9.1.0](https://github.com/puppeteer/puppeteer/compare/v9.0.0...v9.1.0) (2021-05-03)
+
+
+### Features
+
+* add option to filter targets ([#7192](https://github.com/puppeteer/puppeteer/issues/7192)) ([ec3fc2e](https://github.com/puppeteer/puppeteer/commit/ec3fc2e035bb5ca14a576180fff612e1ecf6bad7))
+
+
+### Bug Fixes
+
+* change rm -rf to rimraf ([#7168](https://github.com/puppeteer/puppeteer/issues/7168)) ([ad6b736](https://github.com/puppeteer/puppeteer/commit/ad6b736039436fcc5c0a262e5b575aa041427be3))
+
+## [9.0.0](https://github.com/puppeteer/puppeteer/compare/v8.0.0...v9.0.0) (2021-04-21)
+
+
+### ⚠ BREAKING CHANGES
+
+* **filechooser:** FileChooser.cancel() is now synchronous.
+
+### Features
+
+* **chromium:** roll to Chromium 91.0.4469.0 (r869685) ([#7110](https://github.com/puppeteer/puppeteer/issues/7110)) ([715e7a8](https://github.com/puppeteer/puppeteer/commit/715e7a8d62901d1c7ec602425c2fce8d8148b742))
+* **launcher:** fix installation error on Apple M1 chips ([#7099](https://github.com/puppeteer/puppeteer/issues/7099)) ([c239d9e](https://github.com/puppeteer/puppeteer/commit/c239d9edc72d85697b4875c98fff3ec592848082)), closes [#6622](https://github.com/puppeteer/puppeteer/issues/6622)
+* **network:** request interception and caching compatibility ([#6996](https://github.com/puppeteer/puppeteer/issues/6996)) ([8695759](https://github.com/puppeteer/puppeteer/commit/8695759a223bc1bd31baecb00dc28721216e4c6f))
+* **page:** emit the event after removing the Worker ([#7080](https://github.com/puppeteer/puppeteer/issues/7080)) ([e34a6d5](https://github.com/puppeteer/puppeteer/commit/e34a6d53183c3e1f63a375ba6a26bee0dcfcf542))
+* **types:** improve type of predicate function ([#6997](https://github.com/puppeteer/puppeteer/issues/6997)) ([943477c](https://github.com/puppeteer/puppeteer/commit/943477cc1eb4b129870142873b3554737d5ef252)), closes [/github.com/DefinitelyTyped/DefinitelyTyped/blob/c43191a8f7a7d2a47bbff0bc3a7d95ecc64d2269/types/puppeteer/index.d.ts#L1883-L1885](https://github.com/puppeteer//github.com/DefinitelyTyped/DefinitelyTyped/blob/c43191a8f7a7d2a47bbff0bc3a7d95ecc64d2269/types/puppeteer/index.d.ts/issues/L1883-L1885)
+* accept captureBeyondViewport as optional screenshot param ([#7063](https://github.com/puppeteer/puppeteer/issues/7063)) ([0e092d2](https://github.com/puppeteer/puppeteer/commit/0e092d2ea0ec18ad7f07ad3507deb80f96086e7a))
+* **page:** add omitBackground option for page.pdf method ([#6981](https://github.com/puppeteer/puppeteer/issues/6981)) ([dc8ab6d](https://github.com/puppeteer/puppeteer/commit/dc8ab6d8ca1661f8e56d329e6d9c49c891e8b975))
+
+
+### Bug Fixes
+
+* **aria:** fix parsing of ARIA selectors ([#7037](https://github.com/puppeteer/puppeteer/issues/7037)) ([4426135](https://github.com/puppeteer/puppeteer/commit/4426135692ae3ee7ed2841569dd9375e7ca8286c))
+* **page:** fix mouse.click method ([#7097](https://github.com/puppeteer/puppeteer/issues/7097)) ([ba7c367](https://github.com/puppeteer/puppeteer/commit/ba7c367de33ace7753fd9d8b8cc894b2c14ab6c2)), closes [#6462](https://github.com/puppeteer/puppeteer/issues/6462) [#3347](https://github.com/puppeteer/puppeteer/issues/3347)
+* make `$` and `$$` selectors generic ([#6883](https://github.com/puppeteer/puppeteer/issues/6883)) ([b349c91](https://github.com/puppeteer/puppeteer/commit/b349c91e7df76630b7411d6645e649945c4609bd))
+* type page event listeners correctly ([#6891](https://github.com/puppeteer/puppeteer/issues/6891)) ([866d34e](https://github.com/puppeteer/puppeteer/commit/866d34ee1122e89eab00743246676845bb065968))
+* **typescript:** allow defaultViewport to be 'null' ([#6942](https://github.com/puppeteer/puppeteer/issues/6942)) ([e31e68d](https://github.com/puppeteer/puppeteer/commit/e31e68dfa12dd50482b700472bc98876b9031829)), closes [#6885](https://github.com/puppeteer/puppeteer/issues/6885)
+* make screenshots work in puppeteer-web ([#6936](https://github.com/puppeteer/puppeteer/issues/6936)) ([5f24f60](https://github.com/puppeteer/puppeteer/commit/5f24f608194fd4252da7b288461427cabc9dabb3))
+* **filechooser:** cancel is sync ([#6937](https://github.com/puppeteer/puppeteer/issues/6937)) ([2ba61e0](https://github.com/puppeteer/puppeteer/commit/2ba61e04e923edaac09c92315212552f2d4ce676))
+* **network:** don't disable cache for auth challenge ([#6962](https://github.com/puppeteer/puppeteer/issues/6962)) ([1c2479a](https://github.com/puppeteer/puppeteer/commit/1c2479a6cd4bd09a577175ffd31c40ca6f4279b8))
+
+## [8.0.0](https://github.com/puppeteer/puppeteer/compare/v7.1.0...v8.0.0) (2021-02-26)
+
+
+### ⚠ BREAKING CHANGES
+
+* renamed type `ChromeArgOptions` to `BrowserLaunchArgumentOptions`
+* renamed type `BrowserOptions` to `BrowserConnectOptions`
+
+### Features
+
+* **chromium:** roll Chromium to r856583 ([#6927](https://github.com/puppeteer/puppeteer/issues/6927)) ([0c688bd](https://github.com/puppeteer/puppeteer/commit/0c688bd75ef1d1fc3afd14cbe8966757ecda68fb))
+
+
+### Bug Fixes
+
+* explicit HTTPRequest.resourceType type defs ([#6882](https://github.com/puppeteer/puppeteer/issues/6882)) ([ff26c62](https://github.com/puppeteer/puppeteer/commit/ff26c62647b60cd0d8d7ea66ee998adaadc3fcc2)), closes [#6854](https://github.com/puppeteer/puppeteer/issues/6854)
+* expose `Viewport` type ([#6881](https://github.com/puppeteer/puppeteer/issues/6881)) ([be7c229](https://github.com/puppeteer/puppeteer/commit/be7c22933c1dcf5eee797d61463171bd0ef44582))
+* improve TS types for launching browsers ([#6888](https://github.com/puppeteer/puppeteer/issues/6888)) ([98c8145](https://github.com/puppeteer/puppeteer/commit/98c81458c27f378eb66c38e1620e79e2ffde418e))
+* move CI npm config out of .npmrc ([#6901](https://github.com/puppeteer/puppeteer/issues/6901)) ([f7de60b](https://github.com/puppeteer/puppeteer/commit/f7de60be22d9bc6433ada7bfefeaa7f6f6f62047))
+
+## [7.1.0](https://github.com/puppeteer/puppeteer/compare/v7.0.4...v7.1.0) (2021-02-12)
+
+
+### Features
+
+* **page:** add color-gamut support to Page.emulateMediaFeatures ([#6857](https://github.com/puppeteer/puppeteer/issues/6857)) ([ad59357](https://github.com/puppeteer/puppeteer/commit/ad5935738d869cfce386a0d28b4bc6131457f962)), closes [#6761](https://github.com/puppeteer/puppeteer/issues/6761)
+
+
+### Bug Fixes
+
+* add favicon test asset ([#6868](https://github.com/puppeteer/puppeteer/issues/6868)) ([a63f53c](https://github.com/puppeteer/puppeteer/commit/a63f53c9380545550503f5539494c72c607e19ac))
+* expose `ScreenshotOptions` type in type defs ([#6869](https://github.com/puppeteer/puppeteer/issues/6869)) ([63d48b2](https://github.com/puppeteer/puppeteer/commit/63d48b2ecba317b6c0a3acad87a7a3671c769dbc)), closes [#6866](https://github.com/puppeteer/puppeteer/issues/6866)
+* expose puppeteer.Permission type ([#6856](https://github.com/puppeteer/puppeteer/issues/6856)) ([a5e174f](https://github.com/puppeteer/puppeteer/commit/a5e174f696eb192c541db64a603ea5cdf385a643))
+* jsonValue() type is generic ([#6865](https://github.com/puppeteer/puppeteer/issues/6865)) ([bdaba78](https://github.com/puppeteer/puppeteer/commit/bdaba7829da366aabbc81885d84bb2401ab3eaff))
+* wider compat TS types and CI checks to ensure correct type defs ([#6855](https://github.com/puppeteer/puppeteer/issues/6855)) ([6a0eb78](https://github.com/puppeteer/puppeteer/commit/6a0eb7841fd82493903b0b9fa153d2de181350eb))
+
+### [7.0.4](https://github.com/puppeteer/puppeteer/compare/v7.0.3...v7.0.4) (2021-02-09)
+
+
+### Bug Fixes
+
+* make publish bot run full build, not just tsc ([#6848](https://github.com/puppeteer/puppeteer/issues/6848)) ([f718b14](https://github.com/puppeteer/puppeteer/commit/f718b14b64df8be492d344ddd35e40961ff750c5))
+
+### [7.0.3](https://github.com/puppeteer/puppeteer/compare/v7.0.2...v7.0.3) (2021-02-09)
+
+
+### Bug Fixes
+
+* include lib/types.d.ts in files list ([#6844](https://github.com/puppeteer/puppeteer/issues/6844)) ([e34f317](https://github.com/puppeteer/puppeteer/commit/e34f317b37533256a063c1238609b488d263b998))
+
+### [7.0.2](https://github.com/puppeteer/puppeteer/compare/v7.0.1...v7.0.2) (2021-02-09)
+
+
+### Bug Fixes
+
+* much better TypeScript definitions ([#6837](https://github.com/puppeteer/puppeteer/issues/6837)) ([f1b46ab](https://github.com/puppeteer/puppeteer/commit/f1b46ab5faa262f893c17923579d0cf52268a764))
+* **domworld:** reset bindings when context changes ([#6766](https://github.com/puppeteer/puppeteer/issues/6766)) ([#6836](https://github.com/puppeteer/puppeteer/issues/6836)) ([4e8d074](https://github.com/puppeteer/puppeteer/commit/4e8d074c2f8384a2f283f5edf9ef69c40bd8464f))
+* **launcher:** output correct error message for browser ([#6815](https://github.com/puppeteer/puppeteer/issues/6815)) ([6c61874](https://github.com/puppeteer/puppeteer/commit/6c618747979c3a08f2727e9e22fe45cade8c926a))
+
+### [7.0.1](https://github.com/puppeteer/puppeteer/compare/v7.0.0...v7.0.1) (2021-02-04)
+
+
+### Bug Fixes
+
+* **typescript:** ship .d.ts file in npm package ([#6811](https://github.com/puppeteer/puppeteer/issues/6811)) ([a7e3c2e](https://github.com/puppeteer/puppeteer/commit/a7e3c2e09e9163eee2f15221aafa4400e6a75f91))
+
+## [7.0.0](https://github.com/puppeteer/puppeteer/compare/v6.0.0...v7.0.0) (2021-02-03)
+
+
+### ⚠ BREAKING CHANGES
+
+* - `page.screenshot` makes a screenshot with the clip dimensions, not cutting it by the ViewPort size.
+* **chromium:** - `page.screenshot` cuts screenshot content by the ViewPort size, not ViewPort position.
+
+### Features
+
+* use `captureBeyondViewport` in `Page.captureScreenshot` ([#6805](https://github.com/puppeteer/puppeteer/issues/6805)) ([401d84e](https://github.com/puppeteer/puppeteer/commit/401d84e4a3508f9ca5c24dbfcad2a71571b1b8eb))
+* **chromium:** roll Chromium to r848005 ([#6801](https://github.com/puppeteer/puppeteer/issues/6801)) ([890d5c2](https://github.com/puppeteer/puppeteer/commit/890d5c2e57cdee7d73915a878bda86b72e26b608))
+
+## [6.0.0](https://github.com/puppeteer/puppeteer/compare/v5.5.0...v6.0.0) (2021-02-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* **chromium:** The built-in `aria/` selector query handler doesn’t return ignored elements anymore.
+
+### Features
+
+* **chromium:** roll Chromium to r843427 ([#6797](https://github.com/puppeteer/puppeteer/issues/6797)) ([8f9fbdb](https://github.com/puppeteer/puppeteer/commit/8f9fbdbae68254600a9c73ab05f36146c975dba6)), closes [#6758](https://github.com/puppeteer/puppeteer/issues/6758)
+* add page.emulateNetworkConditions ([#6759](https://github.com/puppeteer/puppeteer/issues/6759)) ([5ea76e9](https://github.com/puppeteer/puppeteer/commit/5ea76e9333c42ab5a751ca01aa5676a662f6c063))
+* **types:** expose typedefs to consumers ([#6745](https://github.com/puppeteer/puppeteer/issues/6745)) ([ebd087a](https://github.com/puppeteer/puppeteer/commit/ebd087a31661a1b701650d0be3e123cc5a813bd8))
+* add iPhone 11 models to DeviceDescriptors ([#6467](https://github.com/puppeteer/puppeteer/issues/6467)) ([50b810d](https://github.com/puppeteer/puppeteer/commit/50b810dab7fae5950ba086295462788f91ff1e6f))
+* support fetching and launching on Apple M1 ([9a8479a](https://github.com/puppeteer/puppeteer/commit/9a8479a52a7d8b51690b0732b2a10816cd1b8aef)), closes [#6495](https://github.com/puppeteer/puppeteer/issues/6495) [#6634](https://github.com/puppeteer/puppeteer/issues/6634) [#6641](https://github.com/puppeteer/puppeteer/issues/6641) [#6614](https://github.com/puppeteer/puppeteer/issues/6614)
+* support promise as return value for page.waitForResponse predicate ([#6624](https://github.com/puppeteer/puppeteer/issues/6624)) ([b57f3fc](https://github.com/puppeteer/puppeteer/commit/b57f3fcd5393c68f51d82e670b004f5b116dcbc3))
+
+
+### Bug Fixes
+
+* **domworld:** fix waitfor bindings ([#6766](https://github.com/puppeteer/puppeteer/issues/6766)) ([#6775](https://github.com/puppeteer/puppeteer/issues/6775)) ([cac540b](https://github.com/puppeteer/puppeteer/commit/cac540be3ab8799a1d77b0951b16bc22ea1c2adb))
+* **launcher:** rename TranslateUI to Translate to match Chrome ([#6692](https://github.com/puppeteer/puppeteer/issues/6692)) ([d901696](https://github.com/puppeteer/puppeteer/commit/d901696e0d8901bcb23cf676a5e5ac562f821a0d))
+* do not use old utility world ([#6528](https://github.com/puppeteer/puppeteer/issues/6528)) ([fb85911](https://github.com/puppeteer/puppeteer/commit/fb859115c0e2829bae1d1b32edbf642988e2ef76)), closes [#6527](https://github.com/puppeteer/puppeteer/issues/6527)
+* update to https-proxy-agent@^5.0.0 to fix `ERR_INVALID_PROTOCOL` ([#6555](https://github.com/puppeteer/puppeteer/issues/6555)) ([3bf5a55](https://github.com/puppeteer/puppeteer/commit/3bf5a552890ee80cc4326b1e430424b0fdad4363))
+
+## [5.5.0](https://github.com/puppeteer/puppeteer/compare/v5.4.1...v5.5.0) (2020-11-16)
+
+
+### Features
+
+* **chromium:** roll Chromium to r818858 ([#6526](https://github.com/puppeteer/puppeteer/issues/6526)) ([b549256](https://github.com/puppeteer/puppeteer/commit/b54925695200cad32f470f8eb407259606447a85))
+
+
+### Bug Fixes
+
+* **common:** fix generic type of `_isClosedPromise` ([#6579](https://github.com/puppeteer/puppeteer/issues/6579)) ([122f074](https://github.com/puppeteer/puppeteer/commit/122f074f92f47a7b9aa08091851e51a07632d23b))
+* **domworld:** fix missing binding for waittasks ([#6562](https://github.com/puppeteer/puppeteer/issues/6562)) ([67da1cf](https://github.com/puppeteer/puppeteer/commit/67da1cf866703f5f581c9cce4923697ac38129ef))
diff --git a/remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs b/remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs
new file mode 100644
index 0000000000..723fa2868a
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/Herebyfile.mjs
@@ -0,0 +1,112 @@
+import {mkdir, readFile, readdir, writeFile} from 'fs/promises';
+import {join} from 'path/posix';
+
+import esbuild from 'esbuild';
+import {execa} from 'execa';
+import {task} from 'hereby';
+
+export const generateVersionTask = task({
+ name: 'generate:version',
+ run: async () => {
+ const {version} = JSON.parse(await readFile('package.json', 'utf8'));
+ await mkdir('src/generated', {recursive: true});
+ await writeFile(
+ 'src/generated/version.ts',
+ (await readFile('src/templates/version.ts.tmpl', 'utf8')).replace(
+ 'PACKAGE_VERSION',
+ version
+ )
+ );
+ if (process.env['PUBLISH']) {
+ await writeFile(
+ '../../versions.js',
+ (
+ await readFile('../../versions.js', {
+ encoding: 'utf-8',
+ })
+ ).replace("'NEXT'", `'v${version}'`)
+ );
+ }
+ },
+});
+
+export const generateInjectedTask = task({
+ name: 'generate:injected',
+ run: async () => {
+ const {
+ outputFiles: [{text}],
+ } = await esbuild.build({
+ entryPoints: ['src/injected/injected.ts'],
+ bundle: true,
+ format: 'cjs',
+ target: ['chrome117', 'firefox118'],
+ minify: true,
+ write: false,
+ });
+ const template = await readFile('src/templates/injected.ts.tmpl', 'utf8');
+ await mkdir('src/generated', {recursive: true});
+ await writeFile(
+ 'src/generated/injected.ts',
+ template.replace('SOURCE_CODE', JSON.stringify(text))
+ );
+ },
+});
+
+export const generatePackageJsonTask = task({
+ name: 'generate:package-json',
+ run: async () => {
+ await mkdir('lib/esm', {recursive: true});
+ await writeFile('lib/esm/package.json', JSON.stringify({type: 'module'}));
+ },
+});
+
+export const generateTask = task({
+ name: 'generate',
+ dependencies: [
+ generateVersionTask,
+ generateInjectedTask,
+ generatePackageJsonTask,
+ ],
+});
+
+export const buildTscTask = task({
+ name: 'build:tsc',
+ dependencies: [generateTask],
+ run: async () => {
+ await execa('tsc', ['-b']);
+ },
+});
+
+export const buildTask = task({
+ name: 'build',
+ dependencies: [buildTscTask],
+ run: async () => {
+ const formats = ['esm', 'cjs'];
+ const packages = (await readdir('third_party', {withFileTypes: true}))
+ .filter(dirent => {
+ return dirent.isDirectory();
+ })
+ .map(({name}) => {
+ return name;
+ });
+ const builders = [];
+ for (const format of formats) {
+ const folder = join('lib', format, 'third_party');
+ for (const name of packages) {
+ const path = join(folder, name, `${name}.js`);
+ builders.push(
+ await esbuild.build({
+ entryPoints: [path],
+ outfile: path,
+ bundle: true,
+ allowOverwrite: true,
+ format,
+ target: 'node16',
+ minify: true,
+ })
+ );
+ }
+ }
+ await Promise.all(builders);
+ },
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/api-extractor.docs.json b/remote/test/puppeteer/packages/puppeteer-core/api-extractor.docs.json
new file mode 100644
index 0000000000..b0bcacbb34
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/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/puppeteer/puppeteer-core.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/puppeteer-core/api-extractor.json b/remote/test/puppeteer/packages/puppeteer-core/api-extractor.json
new file mode 100644
index 0000000000..7b9032de29
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/api-extractor.json
@@ -0,0 +1,46 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/puppeteer-core.d.ts",
+ "bundledPackages": [],
+
+ "apiReport": {
+ "enabled": false
+ },
+
+ "docModel": {
+ "enabled": false
+ },
+
+ "dtsRollup": {
+ "enabled": true,
+ "untrimmedFilePath": "",
+ "alphaTrimmedFilePath": "lib/types.d.ts"
+ },
+
+ "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/puppeteer-core/package.json b/remote/test/puppeteer/packages/puppeteer-core/package.json
new file mode 100644
index 0000000000..2f1943bd2f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/package.json
@@ -0,0 +1,136 @@
+{
+ "name": "puppeteer-core",
+ "version": "21.10.0",
+ "description": "A high-level API to control headless Chrome over the DevTools Protocol",
+ "keywords": [
+ "puppeteer",
+ "chrome",
+ "headless",
+ "automation"
+ ],
+ "type": "commonjs",
+ "main": "./lib/cjs/puppeteer/puppeteer-core.js",
+ "types": "./lib/types.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/types.d.ts",
+ "import": "./lib/esm/puppeteer/puppeteer-core.js",
+ "require": "./lib/cjs/puppeteer/puppeteer-core.js"
+ },
+ "./internal/*": {
+ "import": "./lib/esm/puppeteer/*",
+ "require": "./lib/cjs/puppeteer/*"
+ },
+ "./*": {
+ "import": "./*",
+ "require": "./*"
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core"
+ },
+ "engines": {
+ "node": ">=16.13.2"
+ },
+ "scripts": {
+ "build:docs": "wireit",
+ "build": "wireit",
+ "check": "tsx tools/ensure-correct-devtools-protocol-package",
+ "clean": "../../tools/clean.js",
+ "prepack": "wireit",
+ "unit": "wireit"
+ },
+ "wireit": {
+ "prepack": {
+ "command": "tsx ../../tools/cp.ts ../../README.md README.md",
+ "files": [
+ "../../README.md"
+ ],
+ "output": [
+ "README.md"
+ ]
+ },
+ "build": {
+ "dependencies": [
+ "build:tsc",
+ "build:types"
+ ]
+ },
+ "build:docs": {
+ "command": "api-extractor run --local --config \"./api-extractor.docs.json\"",
+ "files": [
+ "api-extractor.docs.json",
+ "lib/esm/puppeteer/puppeteer-core.d.ts",
+ "tsconfig.json"
+ ],
+ "dependencies": [
+ "build:tsc"
+ ]
+ },
+ "build:tsc": {
+ "command": "hereby build",
+ "clean": "if-file-deleted",
+ "dependencies": [
+ "../browsers:build"
+ ],
+ "files": [
+ "{src,third_party}/**",
+ "../../versions.js",
+ "!src/generated"
+ ],
+ "output": [
+ "lib/{cjs,esm}/**"
+ ]
+ },
+ "build:types": {
+ "command": "api-extractor run --local && eslint --cache-location .eslintcache --cache --ext=ts --no-ignore --no-eslintrc -c=../../.eslintrc.types.cjs --fix lib/types.d.ts",
+ "files": [
+ "../../.eslintrc.types.cjs",
+ "api-extractor.json",
+ "lib/esm/puppeteer/types.d.ts",
+ "tsconfig.json"
+ ],
+ "output": [
+ "lib/types.d.ts"
+ ],
+ "dependencies": [
+ "build:tsc"
+ ]
+ },
+ "unit": {
+ "command": "node --test --test-reporter spec lib/cjs",
+ "dependencies": [
+ "build"
+ ]
+ }
+ },
+ "files": [
+ "lib",
+ "src",
+ "!*.test.ts",
+ "!*.test.js",
+ "!*.test.d.ts",
+ "!*.test.js.map",
+ "!*.test.d.ts.map",
+ "!*.tsbuildinfo"
+ ],
+ "author": "The Chromium Authors",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "1.9.1",
+ "chromium-bidi": "0.5.6",
+ "cross-fetch": "4.0.0",
+ "debug": "4.3.4",
+ "devtools-protocol": "0.0.1232444",
+ "ws": "8.16.0"
+ },
+ "devDependencies": {
+ "@types/debug": "4.1.12",
+ "@types/node": "18.17.15",
+ "@types/ws": "8.5.10",
+ "mitt": "3.0.1",
+ "parsel-js": "1.1.2",
+ "rxjs": "7.8.1"
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts
new file mode 100644
index 0000000000..e3b465c80e
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts
@@ -0,0 +1,454 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ChildProcess} from 'child_process';
+
+import type {Protocol} from 'devtools-protocol';
+
+import {
+ filterAsync,
+ firstValueFrom,
+ from,
+ merge,
+ raceWith,
+} from '../../third_party/rxjs/rxjs.js';
+import type {ProtocolType} from '../common/ConnectOptions.js';
+import {EventEmitter, type EventType} from '../common/EventEmitter.js';
+import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
+import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
+
+import type {BrowserContext} from './BrowserContext.js';
+import type {Page} from './Page.js';
+import type {Target} from './Target.js';
+/**
+ * @public
+ */
+export interface BrowserContextOptions {
+ /**
+ * Proxy server with optional port to use for all requests.
+ * Username and password can be set in `Page.authenticate`.
+ */
+ proxyServer?: string;
+ /**
+ * Bypass the proxy for the given list of hosts.
+ */
+ proxyBypassList?: string[];
+}
+
+/**
+ * @internal
+ */
+export type BrowserCloseCallback = () => Promise<void> | void;
+
+/**
+ * @public
+ */
+export type TargetFilterCallback = (target: Target) => boolean;
+
+/**
+ * @internal
+ */
+export type IsPageTargetCallback = (target: Target) => boolean;
+
+/**
+ * @internal
+ */
+export const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map<
+ Permission,
+ Protocol.Browser.PermissionType
+>([
+ ['geolocation', 'geolocation'],
+ ['midi', 'midi'],
+ ['notifications', 'notifications'],
+ // TODO: push isn't a valid type?
+ // ['push', 'push'],
+ ['camera', 'videoCapture'],
+ ['microphone', 'audioCapture'],
+ ['background-sync', 'backgroundSync'],
+ ['ambient-light-sensor', 'sensors'],
+ ['accelerometer', 'sensors'],
+ ['gyroscope', 'sensors'],
+ ['magnetometer', 'sensors'],
+ ['accessibility-events', 'accessibilityEvents'],
+ ['clipboard-read', 'clipboardReadWrite'],
+ ['clipboard-write', 'clipboardReadWrite'],
+ ['clipboard-sanitized-write', 'clipboardSanitizedWrite'],
+ ['payment-handler', 'paymentHandler'],
+ ['persistent-storage', 'durableStorage'],
+ ['idle-detection', 'idleDetection'],
+ // chrome-specific permissions we have.
+ ['midi-sysex', 'midiSysex'],
+]);
+
+/**
+ * @public
+ */
+export type Permission =
+ | 'geolocation'
+ | 'midi'
+ | 'notifications'
+ | 'camera'
+ | 'microphone'
+ | 'background-sync'
+ | 'ambient-light-sensor'
+ | 'accelerometer'
+ | 'gyroscope'
+ | 'magnetometer'
+ | 'accessibility-events'
+ | 'clipboard-read'
+ | 'clipboard-write'
+ | 'clipboard-sanitized-write'
+ | 'payment-handler'
+ | 'persistent-storage'
+ | 'idle-detection'
+ | 'midi-sysex';
+
+/**
+ * @public
+ */
+export interface WaitForTargetOptions {
+ /**
+ * Maximum wait time in milliseconds. Pass `0` to disable the timeout.
+ *
+ * @defaultValue `30_000`
+ */
+ timeout?: number;
+}
+
+/**
+ * All the events a {@link Browser | browser instance} may emit.
+ *
+ * @public
+ */
+export const enum BrowserEvent {
+ /**
+ * Emitted when Puppeteer gets disconnected from the browser instance. This
+ * might happen because either:
+ *
+ * - The browser closes/crashes or
+ * - {@link Browser.disconnect} was called.
+ */
+ Disconnected = 'disconnected',
+ /**
+ * Emitted when the URL of a target changes. Contains a {@link Target}
+ * instance.
+ *
+ * @remarks Note that this includes target changes in incognito browser
+ * contexts.
+ */
+ TargetChanged = 'targetchanged',
+ /**
+ * Emitted when a target is created, for example when a new page is opened by
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
+ * or by {@link Browser.newPage | browser.newPage}
+ *
+ * Contains a {@link Target} instance.
+ *
+ * @remarks Note that this includes target creations in incognito browser
+ * contexts.
+ */
+ TargetCreated = 'targetcreated',
+ /**
+ * Emitted when a target is destroyed, for example when a page is closed.
+ * Contains a {@link Target} instance.
+ *
+ * @remarks Note that this includes target destructions in incognito browser
+ * contexts.
+ */
+ TargetDestroyed = 'targetdestroyed',
+ /**
+ * @internal
+ */
+ TargetDiscovered = 'targetdiscovered',
+}
+
+export {
+ /**
+ * @deprecated Use {@link BrowserEvent}.
+ */
+ BrowserEvent as BrowserEmittedEvents,
+};
+
+/**
+ * @public
+ */
+export interface BrowserEvents extends Record<EventType, unknown> {
+ [BrowserEvent.Disconnected]: undefined;
+ [BrowserEvent.TargetCreated]: Target;
+ [BrowserEvent.TargetDestroyed]: Target;
+ [BrowserEvent.TargetChanged]: Target;
+ /**
+ * @internal
+ */
+ [BrowserEvent.TargetDiscovered]: Protocol.Target.TargetInfo;
+}
+
+/**
+ * @public
+ * @experimental
+ */
+export interface DebugInfo {
+ pendingProtocolErrors: Error[];
+}
+
+/**
+ * {@link Browser} represents a browser instance that is either:
+ *
+ * - connected to via {@link Puppeteer.connect} or
+ * - launched by {@link PuppeteerNode.launch}.
+ *
+ * {@link Browser} {@link EventEmitter | emits} various events which are
+ * documented in the {@link BrowserEvent} enum.
+ *
+ * @example Using a {@link Browser} to create a {@link Page}:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.goto('https://example.com');
+ * await browser.close();
+ * ```
+ *
+ * @example Disconnecting from and reconnecting to a {@link Browser}:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * const browser = await puppeteer.launch();
+ * // Store the endpoint to be able to reconnect to the browser.
+ * const browserWSEndpoint = browser.wsEndpoint();
+ * // Disconnect puppeteer from the browser.
+ * await browser.disconnect();
+ *
+ * // Use the endpoint to reestablish a connection
+ * const browser2 = await puppeteer.connect({browserWSEndpoint});
+ * // Close the browser.
+ * await browser2.close();
+ * ```
+ *
+ * @public
+ */
+export abstract class Browser extends EventEmitter<BrowserEvents> {
+ /**
+ * @internal
+ */
+ constructor() {
+ super();
+ }
+
+ /**
+ * Gets the associated
+ * {@link https://nodejs.org/api/child_process.html#class-childprocess | ChildProcess}.
+ *
+ * @returns `null` if this instance was connected to via
+ * {@link Puppeteer.connect}.
+ */
+ abstract process(): ChildProcess | null;
+
+ /**
+ * Creates a new incognito {@link BrowserContext | browser context}.
+ *
+ * This won't share cookies/cache with other {@link BrowserContext | browser contexts}.
+ *
+ * @example
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * const browser = await puppeteer.launch();
+ * // Create a new incognito browser context.
+ * const context = await browser.createIncognitoBrowserContext();
+ * // Create a new page in a pristine context.
+ * const page = await context.newPage();
+ * // Do stuff
+ * await page.goto('https://example.com');
+ * ```
+ */
+ abstract createIncognitoBrowserContext(
+ options?: BrowserContextOptions
+ ): Promise<BrowserContext>;
+
+ /**
+ * Gets a list of open {@link BrowserContext | browser contexts}.
+ *
+ * In a newly-created {@link Browser | browser}, this will return a single
+ * instance of {@link BrowserContext}.
+ */
+ abstract browserContexts(): BrowserContext[];
+
+ /**
+ * Gets the default {@link BrowserContext | browser context}.
+ *
+ * @remarks The default {@link BrowserContext | browser context} cannot be
+ * closed.
+ */
+ abstract defaultBrowserContext(): BrowserContext;
+
+ /**
+ * Gets the WebSocket URL to connect to this {@link Browser | browser}.
+ *
+ * This is usually used with {@link Puppeteer.connect}.
+ *
+ * You can find the debugger URL (`webSocketDebuggerUrl`) from
+ * `http://HOST:PORT/json/version`.
+ *
+ * See {@link
+ * https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
+ * | browser endpoint} for more information.
+ *
+ * @remarks The format is always `ws://HOST:PORT/devtools/browser/<id>`.
+ */
+ abstract wsEndpoint(): string;
+
+ /**
+ * Creates a new {@link Page | page} in the
+ * {@link Browser.defaultBrowserContext | default browser context}.
+ */
+ abstract newPage(): Promise<Page>;
+
+ /**
+ * Gets all active {@link Target | targets}.
+ *
+ * In case of multiple {@link BrowserContext | browser contexts}, this returns
+ * all {@link Target | targets} in all
+ * {@link BrowserContext | browser contexts}.
+ */
+ abstract targets(): Target[];
+
+ /**
+ * Gets the {@link Target | target} associated with the
+ * {@link Browser.defaultBrowserContext | default browser context}).
+ */
+ abstract target(): Target;
+
+ /**
+ * Waits until a {@link Target | target} matching the given `predicate`
+ * appears and returns it.
+ *
+ * This will look all open {@link BrowserContext | browser contexts}.
+ *
+ * @example Finding a target for a page opened via `window.open`:
+ *
+ * ```ts
+ * await page.evaluate(() => window.open('https://www.example.com/'));
+ * const newWindowTarget = await browser.waitForTarget(
+ * target => target.url() === 'https://www.example.com/'
+ * );
+ * ```
+ */
+ async waitForTarget(
+ predicate: (x: Target) => boolean | Promise<boolean>,
+ options: WaitForTargetOptions = {}
+ ): Promise<Target> {
+ const {timeout: ms = 30000} = options;
+ return await firstValueFrom(
+ merge(
+ fromEmitterEvent(this, BrowserEvent.TargetCreated),
+ fromEmitterEvent(this, BrowserEvent.TargetChanged),
+ from(this.targets())
+ ).pipe(filterAsync(predicate), raceWith(timeout(ms)))
+ );
+ }
+
+ /**
+ * Gets a list of all open {@link Page | pages} inside this {@link Browser}.
+ *
+ * If there ar multiple {@link BrowserContext | browser contexts}, this
+ * returns all {@link Page | pages} in all
+ * {@link BrowserContext | browser contexts}.
+ *
+ * @remarks Non-visible {@link Page | pages}, such as `"background_page"`,
+ * will not be listed here. You can find them using {@link Target.page}.
+ */
+ async pages(): Promise<Page[]> {
+ const contextPages = await Promise.all(
+ this.browserContexts().map(context => {
+ return context.pages();
+ })
+ );
+ // Flatten array.
+ return contextPages.reduce((acc, x) => {
+ return acc.concat(x);
+ }, []);
+ }
+
+ /**
+ * Gets a string representing this {@link Browser | browser's} name and
+ * version.
+ *
+ * For headless browser, this is similar to `"HeadlessChrome/61.0.3153.0"`. For
+ * non-headless or new-headless, this is similar to `"Chrome/61.0.3153.0"`. For
+ * Firefox, it is similar to `"Firefox/116.0a1"`.
+ *
+ * The format of {@link Browser.version} might change with future releases of
+ * browsers.
+ */
+ abstract version(): Promise<string>;
+
+ /**
+ * Gets this {@link Browser | browser's} original user agent.
+ *
+ * {@link Page | Pages} can override the user agent with
+ * {@link Page.setUserAgent}.
+ *
+ */
+ abstract userAgent(): Promise<string>;
+
+ /**
+ * Closes this {@link Browser | browser} and all associated
+ * {@link Page | pages}.
+ */
+ abstract close(): Promise<void>;
+
+ /**
+ * Disconnects Puppeteer from this {@link Browser | browser}, but leaves the
+ * process running.
+ */
+ abstract disconnect(): Promise<void>;
+
+ /**
+ * Whether Puppeteer is connected to this {@link Browser | browser}.
+ *
+ * @deprecated Use {@link Browser | Browser.connected}.
+ */
+ isConnected(): boolean {
+ return this.connected;
+ }
+
+ /**
+ * Whether Puppeteer is connected to this {@link Browser | browser}.
+ */
+ abstract get connected(): boolean;
+
+ /** @internal */
+ [disposeSymbol](): void {
+ return void this.close().catch(debugError);
+ }
+
+ /** @internal */
+ [asyncDisposeSymbol](): Promise<void> {
+ return this.close();
+ }
+
+ /**
+ * @internal
+ */
+ abstract get protocol(): ProtocolType;
+
+ /**
+ * Get debug information from Puppeteer.
+ *
+ * @remarks
+ *
+ * Currently, includes pending protocol calls. In the future, we might add more info.
+ *
+ * @public
+ * @experimental
+ */
+ abstract get debugInfo(): DebugInfo;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts
new file mode 100644
index 0000000000..79335eb9ed
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts
@@ -0,0 +1,224 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {EventEmitter, type EventType} from '../common/EventEmitter.js';
+import {debugError} from '../common/util.js';
+import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
+
+import type {Browser, Permission, WaitForTargetOptions} from './Browser.js';
+import type {Page} from './Page.js';
+import type {Target} from './Target.js';
+
+/**
+ * @public
+ */
+export const enum BrowserContextEvent {
+ /**
+ * Emitted when the url of a target inside the browser context changes.
+ * Contains a {@link Target} instance.
+ */
+ TargetChanged = 'targetchanged',
+
+ /**
+ * Emitted when a target is created within the browser context, for example
+ * when a new page is opened by
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
+ * or by {@link BrowserContext.newPage | browserContext.newPage}
+ *
+ * Contains a {@link Target} instance.
+ */
+ TargetCreated = 'targetcreated',
+ /**
+ * Emitted when a target is destroyed within the browser context, for example
+ * when a page is closed. Contains a {@link Target} instance.
+ */
+ TargetDestroyed = 'targetdestroyed',
+}
+
+export {
+ /**
+ * @deprecated Use {@link BrowserContextEvent}
+ */
+ BrowserContextEvent as BrowserContextEmittedEvents,
+};
+
+/**
+ * @public
+ */
+export interface BrowserContextEvents extends Record<EventType, unknown> {
+ [BrowserContextEvent.TargetChanged]: Target;
+ [BrowserContextEvent.TargetCreated]: Target;
+ [BrowserContextEvent.TargetDestroyed]: Target;
+}
+
+/**
+ * {@link BrowserContext} represents individual sessions within a
+ * {@link Browser | browser}.
+ *
+ * When a {@link Browser | browser} is launched, it has a single
+ * {@link BrowserContext | browser context} by default. Others can be created
+ * using {@link Browser.createIncognitoBrowserContext}.
+ *
+ * {@link BrowserContext} {@link EventEmitter | emits} various events which are
+ * documented in the {@link BrowserContextEvent} enum.
+ *
+ * If a {@link Page | page} opens another {@link Page | page}, e.g. using
+ * `window.open`, the popup will belong to the parent {@link Page.browserContext
+ * | page's browser context}.
+ *
+ * @example Creating an incognito {@link BrowserContext | browser context}:
+ *
+ * ```ts
+ * // Create a new incognito browser context
+ * const context = await browser.createIncognitoBrowserContext();
+ * // Create a new page inside context.
+ * const page = await context.newPage();
+ * // ... do stuff with page ...
+ * await page.goto('https://example.com');
+ * // Dispose context once it's no longer needed.
+ * await context.close();
+ * ```
+ *
+ * @public
+ */
+
+export abstract class BrowserContext extends EventEmitter<BrowserContextEvents> {
+ /**
+ * @internal
+ */
+ constructor() {
+ super();
+ }
+
+ /**
+ * Gets all active {@link Target | targets} inside this
+ * {@link BrowserContext | browser context}.
+ */
+ abstract targets(): Target[];
+
+ /**
+ * Waits until a {@link Target | target} matching the given `predicate`
+ * appears and returns it.
+ *
+ * This will look all open {@link BrowserContext | browser contexts}.
+ *
+ * @example Finding a target for a page opened via `window.open`:
+ *
+ * ```ts
+ * await page.evaluate(() => window.open('https://www.example.com/'));
+ * const newWindowTarget = await browserContext.waitForTarget(
+ * target => target.url() === 'https://www.example.com/'
+ * );
+ * ```
+ */
+ abstract waitForTarget(
+ predicate: (x: Target) => boolean | Promise<boolean>,
+ options?: WaitForTargetOptions
+ ): Promise<Target>;
+
+ /**
+ * Gets a list of all open {@link Page | pages} inside this
+ * {@link BrowserContext | browser context}.
+ *
+ * @remarks Non-visible {@link Page | pages}, such as `"background_page"`,
+ * will not be listed here. You can find them using {@link Target.page}.
+ */
+ abstract pages(): Promise<Page[]>;
+
+ /**
+ * Whether this {@link BrowserContext | browser context} is incognito.
+ *
+ * The {@link Browser.defaultBrowserContext | default browser context} is the
+ * only non-incognito browser context.
+ */
+ abstract isIncognito(): boolean;
+
+ /**
+ * Grants this {@link BrowserContext | browser context} the given
+ * `permissions` within the given `origin`.
+ *
+ * @example Overriding permissions in the
+ * {@link Browser.defaultBrowserContext | default browser context}:
+ *
+ * ```ts
+ * const context = browser.defaultBrowserContext();
+ * await context.overridePermissions('https://html5demos.com', [
+ * 'geolocation',
+ * ]);
+ * ```
+ *
+ * @param origin - The origin to grant permissions to, e.g.
+ * "https://example.com".
+ * @param permissions - An array of permissions to grant. All permissions that
+ * are not listed here will be automatically denied.
+ */
+ abstract overridePermissions(
+ origin: string,
+ permissions: Permission[]
+ ): Promise<void>;
+
+ /**
+ * Clears all permission overrides for this
+ * {@link BrowserContext | browser context}.
+ *
+ * @example Clearing overridden permissions in the
+ * {@link Browser.defaultBrowserContext | default browser context}:
+ *
+ * ```ts
+ * const context = browser.defaultBrowserContext();
+ * context.overridePermissions('https://example.com', ['clipboard-read']);
+ * // do stuff ..
+ * context.clearPermissionOverrides();
+ * ```
+ */
+ abstract clearPermissionOverrides(): Promise<void>;
+
+ /**
+ * Creates a new {@link Page | page} in this
+ * {@link BrowserContext | browser context}.
+ */
+ abstract newPage(): Promise<Page>;
+
+ /**
+ * Gets the {@link Browser | browser} associated with this
+ * {@link BrowserContext | browser context}.
+ */
+ abstract browser(): Browser;
+
+ /**
+ * Closes this {@link BrowserContext | browser context} and all associated
+ * {@link Page | pages}.
+ *
+ * @remarks The
+ * {@link Browser.defaultBrowserContext | default browser context} cannot be
+ * closed.
+ */
+ abstract close(): Promise<void>;
+
+ /**
+ * Whether this {@link BrowserContext | browser context} is closed.
+ */
+ get closed(): boolean {
+ return !this.browser().browserContexts().includes(this);
+ }
+
+ /**
+ * Identifier for this {@link BrowserContext | browser context}.
+ */
+ get id(): string | undefined {
+ return undefined;
+ }
+
+ /** @internal */
+ [disposeSymbol](): void {
+ return void this.close().catch(debugError);
+ }
+
+ /** @internal */
+ [asyncDisposeSymbol](): Promise<void> {
+ return this.close();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts
new file mode 100644
index 0000000000..8bdf96f954
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/CDPSession.ts
@@ -0,0 +1,121 @@
+import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
+
+import type {Connection} from '../cdp/Connection.js';
+import {EventEmitter, type EventType} from '../common/EventEmitter.js';
+
+/**
+ * @public
+ */
+export type CDPEvents = {
+ [Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0];
+};
+
+/**
+ * Events that the CDPSession class emits.
+ *
+ * @public
+ */
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace CDPSessionEvent {
+ /** @internal */
+ export const Disconnected = Symbol('CDPSession.Disconnected');
+ /** @internal */
+ export const Swapped = Symbol('CDPSession.Swapped');
+ /**
+ * Emitted when the session is ready to be configured during the auto-attach
+ * process. Right after the event is handled, the session will be resumed.
+ *
+ * @internal
+ */
+ export const Ready = Symbol('CDPSession.Ready');
+ export const SessionAttached = 'sessionattached' as const;
+ export const SessionDetached = 'sessiondetached' as const;
+}
+
+/**
+ * @public
+ */
+export interface CDPSessionEvents
+ extends CDPEvents,
+ Record<EventType, unknown> {
+ /** @internal */
+ [CDPSessionEvent.Disconnected]: undefined;
+ /** @internal */
+ [CDPSessionEvent.Swapped]: CDPSession;
+ /** @internal */
+ [CDPSessionEvent.Ready]: CDPSession;
+ [CDPSessionEvent.SessionAttached]: CDPSession;
+ [CDPSessionEvent.SessionDetached]: CDPSession;
+}
+
+/**
+ * @public
+ */
+export interface CommandOptions {
+ timeout: number;
+}
+
+/**
+ * The `CDPSession` instances are used to talk raw Chrome Devtools Protocol.
+ *
+ * @remarks
+ *
+ * Protocol methods can be called with {@link CDPSession.send} method and protocol
+ * events can be subscribed to with `CDPSession.on` method.
+ *
+ * Useful links: {@link https://chromedevtools.github.io/devtools-protocol/ | DevTools Protocol Viewer}
+ * and {@link https://github.com/aslushnikov/getting-started-with-cdp/blob/HEAD/README.md | Getting Started with DevTools Protocol}.
+ *
+ * @example
+ *
+ * ```ts
+ * const client = await page.target().createCDPSession();
+ * await client.send('Animation.enable');
+ * client.on('Animation.animationCreated', () =>
+ * console.log('Animation created!')
+ * );
+ * const response = await client.send('Animation.getPlaybackRate');
+ * console.log('playback rate is ' + response.playbackRate);
+ * await client.send('Animation.setPlaybackRate', {
+ * playbackRate: response.playbackRate / 2,
+ * });
+ * ```
+ *
+ * @public
+ */
+export abstract class CDPSession extends EventEmitter<CDPSessionEvents> {
+ /**
+ * @internal
+ */
+ constructor() {
+ super();
+ }
+
+ abstract connection(): Connection | undefined;
+
+ /**
+ * Parent session in terms of CDP's auto-attach mechanism.
+ *
+ * @internal
+ */
+ parentSession(): CDPSession | undefined {
+ return undefined;
+ }
+
+ abstract send<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ params?: ProtocolMapping.Commands[T]['paramsType'][0],
+ options?: CommandOptions
+ ): Promise<ProtocolMapping.Commands[T]['returnType']>;
+
+ /**
+ * Detaches the cdpSession from the target. Once detached, the cdpSession object
+ * won't emit any events and can't be used to send messages.
+ */
+ abstract detach(): Promise<void>;
+
+ /**
+ * Returns the session's id.
+ */
+ abstract id(): string;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Dialog.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Dialog.ts
new file mode 100644
index 0000000000..352337f30f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Dialog.ts
@@ -0,0 +1,110 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import {assert} from '../util/assert.js';
+
+/**
+ * Dialog instances are dispatched by the {@link Page} via the `dialog` event.
+ *
+ * @remarks
+ *
+ * @example
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * page.on('dialog', async dialog => {
+ * console.log(dialog.message());
+ * await dialog.dismiss();
+ * await browser.close();
+ * });
+ * page.evaluate(() => alert('1'));
+ * })();
+ * ```
+ *
+ * @public
+ */
+export abstract class Dialog {
+ #type: Protocol.Page.DialogType;
+ #message: string;
+ #defaultValue: string;
+ #handled = false;
+
+ /**
+ * @internal
+ */
+ constructor(
+ type: Protocol.Page.DialogType,
+ message: string,
+ defaultValue = ''
+ ) {
+ this.#type = type;
+ this.#message = message;
+ this.#defaultValue = defaultValue;
+ }
+
+ /**
+ * The type of the dialog.
+ */
+ type(): Protocol.Page.DialogType {
+ return this.#type;
+ }
+
+ /**
+ * The message displayed in the dialog.
+ */
+ message(): string {
+ return this.#message;
+ }
+
+ /**
+ * The default value of the prompt, or an empty string if the dialog
+ * is not a `prompt`.
+ */
+ defaultValue(): string {
+ return this.#defaultValue;
+ }
+
+ /**
+ * @internal
+ */
+ protected abstract handle(options: {
+ accept: boolean;
+ text?: string;
+ }): Promise<void>;
+
+ /**
+ * A promise that resolves when the dialog has been accepted.
+ *
+ * @param promptText - optional text that will be entered in the dialog
+ * prompt. Has no effect if the dialog's type is not `prompt`.
+ *
+ */
+ async accept(promptText?: string): Promise<void> {
+ assert(!this.#handled, 'Cannot accept dialog which is already handled!');
+ this.#handled = true;
+ await this.handle({
+ accept: true,
+ text: promptText,
+ });
+ }
+
+ /**
+ * A promise which will resolve once the dialog has been dismissed
+ */
+ async dismiss(): Promise<void> {
+ assert(!this.#handled, 'Cannot dismiss dialog which is already handled!');
+ this.#handled = true;
+ await this.handle({
+ accept: false,
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts
new file mode 100644
index 0000000000..43fec58e37
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts
@@ -0,0 +1,1580 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {Frame} from '../api/Frame.js';
+import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
+import {LazyArg} from '../common/LazyArg.js';
+import type {
+ ElementFor,
+ EvaluateFuncWith,
+ HandleFor,
+ HandleOr,
+ NodeFor,
+} from '../common/types.js';
+import type {KeyInput} from '../common/USKeyboardLayout.js';
+import {
+ debugError,
+ isString,
+ withSourcePuppeteerURLIfNone,
+} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
+import {throwIfDisposed} from '../util/decorators.js';
+import {AsyncDisposableStack} from '../util/disposable.js';
+
+import {_isElementHandle} from './ElementHandleSymbol.js';
+import type {
+ KeyboardTypeOptions,
+ KeyPressOptions,
+ MouseClickOptions,
+} from './Input.js';
+import {JSHandle} from './JSHandle.js';
+import type {ScreenshotOptions, WaitForSelectorOptions} from './Page.js';
+
+/**
+ * @public
+ */
+export type Quad = [Point, Point, Point, Point];
+
+/**
+ * @public
+ */
+export interface BoxModel {
+ content: Quad;
+ padding: Quad;
+ border: Quad;
+ margin: Quad;
+ width: number;
+ height: number;
+}
+
+/**
+ * @public
+ */
+export interface BoundingBox extends Point {
+ /**
+ * the width of the element in pixels.
+ */
+ width: number;
+ /**
+ * the height of the element in pixels.
+ */
+ height: number;
+}
+
+/**
+ * @public
+ */
+export interface Offset {
+ /**
+ * x-offset for the clickable point relative to the top-left corner of the border box.
+ */
+ x: number;
+ /**
+ * y-offset for the clickable point relative to the top-left corner of the border box.
+ */
+ y: number;
+}
+
+/**
+ * @public
+ */
+export interface ClickOptions extends MouseClickOptions {
+ /**
+ * Offset for the clickable point relative to the top-left corner of the border box.
+ */
+ offset?: Offset;
+}
+
+/**
+ * @public
+ */
+export interface Point {
+ x: number;
+ y: number;
+}
+
+/**
+ * @public
+ */
+export interface ElementScreenshotOptions extends ScreenshotOptions {
+ /**
+ * @defaultValue `true`
+ */
+ scrollIntoView?: boolean;
+}
+
+/**
+ * ElementHandle represents an in-page DOM element.
+ *
+ * @remarks
+ * ElementHandles can be created with the {@link Page.$} method.
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.goto('https://example.com');
+ * const hrefElement = await page.$('a');
+ * await hrefElement.click();
+ * // ...
+ * })();
+ * ```
+ *
+ * ElementHandle prevents the DOM element from being garbage-collected unless the
+ * handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed
+ * when their origin frame gets navigated.
+ *
+ * ElementHandle instances can be used as arguments in {@link Page.$eval} and
+ * {@link Page.evaluate} methods.
+ *
+ * If you're using TypeScript, ElementHandle takes a generic argument that
+ * denotes the type of element the handle is holding within. For example, if you
+ * have a handle to a `<select>` element, you can type it as
+ * `ElementHandle<HTMLSelectElement>` and you get some nicer type checks.
+ *
+ * @public
+ */
+export abstract class ElementHandle<
+ ElementType extends Node = Element,
+> extends JSHandle<ElementType> {
+ /**
+ * @internal
+ */
+ declare [_isElementHandle]: boolean;
+
+ /**
+ * A given method will have it's `this` replaced with an isolated version of
+ * `this` when decorated with this decorator.
+ *
+ * All changes of isolated `this` are reflected on the actual `this`.
+ *
+ * @internal
+ */
+ static bindIsolatedHandle<This extends ElementHandle<Node>>(
+ target: (this: This, ...args: any[]) => Promise<any>,
+ _: unknown
+ ): typeof target {
+ return async function (...args) {
+ // If the handle is already isolated, then we don't need to adopt it
+ // again.
+ if (this.realm === this.frame.isolatedRealm()) {
+ return await target.call(this, ...args);
+ }
+ using adoptedThis = await this.frame.isolatedRealm().adoptHandle(this);
+ const result = await target.call(adoptedThis, ...args);
+ // If the function returns `adoptedThis`, then we return `this`.
+ if (result === adoptedThis) {
+ return this;
+ }
+ // If the function returns a handle, transfer it into the current realm.
+ if (result instanceof JSHandle) {
+ return await this.realm.transferHandle(result);
+ }
+ // If the function returns an array of handlers, transfer them into the
+ // current realm.
+ if (Array.isArray(result)) {
+ await Promise.all(
+ result.map(async (item, index, result) => {
+ if (item instanceof JSHandle) {
+ result[index] = await this.realm.transferHandle(item);
+ }
+ })
+ );
+ }
+ if (result instanceof Map) {
+ await Promise.all(
+ [...result.entries()].map(async ([key, value]) => {
+ if (value instanceof JSHandle) {
+ result.set(key, await this.realm.transferHandle(value));
+ }
+ })
+ );
+ }
+ return result;
+ };
+ }
+
+ /**
+ * @internal
+ */
+ protected readonly handle;
+
+ /**
+ * @internal
+ */
+ constructor(handle: JSHandle<ElementType>) {
+ super();
+ this.handle = handle;
+ this[_isElementHandle] = true;
+ }
+
+ /**
+ * @internal
+ */
+ override get id(): string | undefined {
+ return this.handle.id;
+ }
+
+ /**
+ * @internal
+ */
+ override get disposed(): boolean {
+ return this.handle.disposed;
+ }
+
+ /**
+ * @internal
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ override async getProperty<K extends keyof ElementType>(
+ propertyName: HandleOr<K>
+ ): Promise<HandleFor<ElementType[K]>> {
+ return await this.handle.getProperty(propertyName);
+ }
+
+ /**
+ * @internal
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ override async getProperties(): Promise<Map<string, JSHandle>> {
+ return await this.handle.getProperties();
+ }
+
+ /**
+ * @internal
+ */
+ override async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
+ ElementType,
+ Params
+ >,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluate.name,
+ pageFunction
+ );
+ return await this.handle.evaluate(pageFunction, ...args);
+ }
+
+ /**
+ * @internal
+ */
+ override async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
+ ElementType,
+ Params
+ >,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluateHandle.name,
+ pageFunction
+ );
+ return await this.handle.evaluateHandle(pageFunction, ...args);
+ }
+
+ /**
+ * @internal
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ override async jsonValue(): Promise<ElementType> {
+ return await this.handle.jsonValue();
+ }
+
+ /**
+ * @internal
+ */
+ override toString(): string {
+ return this.handle.toString();
+ }
+
+ /**
+ * @internal
+ */
+ override remoteObject(): Protocol.Runtime.RemoteObject {
+ return this.handle.remoteObject();
+ }
+
+ /**
+ * @internal
+ */
+ override dispose(): Promise<void> {
+ return this.handle.dispose();
+ }
+
+ /**
+ * @internal
+ */
+ override asElement(): ElementHandle<ElementType> {
+ return this;
+ }
+
+ /**
+ * Frame corresponding to the current handle.
+ */
+ abstract get frame(): Frame;
+
+ /**
+ * Queries the current element for an element matching the given selector.
+ *
+ * @param selector - The selector to query for.
+ * @returns A {@link ElementHandle | element handle} to the first element
+ * matching the given selector. Otherwise, `null`.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async $<Selector extends string>(
+ selector: Selector
+ ): Promise<ElementHandle<NodeFor<Selector>> | null> {
+ const {updatedSelector, QueryHandler} =
+ getQueryHandlerAndSelector(selector);
+ return (await QueryHandler.queryOne(
+ this,
+ updatedSelector
+ )) as ElementHandle<NodeFor<Selector>> | null;
+ }
+
+ /**
+ * Queries the current element for all elements matching the given selector.
+ *
+ * @param selector - The selector to query for.
+ * @returns An array of {@link ElementHandle | element handles} that point to
+ * elements matching the given selector.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async $$<Selector extends string>(
+ selector: Selector
+ ): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
+ const {updatedSelector, QueryHandler} =
+ getQueryHandlerAndSelector(selector);
+ return await (AsyncIterableUtil.collect(
+ QueryHandler.queryAll(this, updatedSelector)
+ ) as Promise<Array<ElementHandle<NodeFor<Selector>>>>);
+ }
+
+ /**
+ * Runs the given function on the first element matching the given selector in
+ * the current element.
+ *
+ * If the given function returns a promise, then this method will wait till
+ * the promise resolves.
+ *
+ * @example
+ *
+ * ```ts
+ * const tweetHandle = await page.$('.tweet');
+ * expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe(
+ * '100'
+ * );
+ * expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe(
+ * '10'
+ * );
+ * ```
+ *
+ * @param selector - The selector to query for.
+ * @param pageFunction - The function to be evaluated in this element's page's
+ * context. The first element matching the selector will be passed in as the
+ * first argument.
+ * @param args - Additional arguments to pass to `pageFunction`.
+ * @returns A promise to the result of the function.
+ */
+ async $eval<
+ Selector extends string,
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
+ NodeFor<Selector>,
+ Params
+ >,
+ >(
+ selector: Selector,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
+ using elementHandle = await this.$(selector);
+ if (!elementHandle) {
+ throw new Error(
+ `Error: failed to find element matching selector "${selector}"`
+ );
+ }
+ return await elementHandle.evaluate(pageFunction, ...args);
+ }
+
+ /**
+ * Runs the given function on an array of elements matching the given selector
+ * in the current element.
+ *
+ * If the given function returns a promise, then this method will wait till
+ * the promise resolves.
+ *
+ * @example
+ * HTML:
+ *
+ * ```html
+ * <div class="feed">
+ * <div class="tweet">Hello!</div>
+ * <div class="tweet">Hi!</div>
+ * </div>
+ * ```
+ *
+ * JavaScript:
+ *
+ * ```ts
+ * const feedHandle = await page.$('.feed');
+ * expect(
+ * await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))
+ * ).toEqual(['Hello!', 'Hi!']);
+ * ```
+ *
+ * @param selector - The selector to query for.
+ * @param pageFunction - The function to be evaluated in the element's page's
+ * context. An array of elements matching the given selector will be passed to
+ * the function as its first argument.
+ * @param args - Additional arguments to pass to `pageFunction`.
+ * @returns A promise to the result of the function.
+ */
+ async $$eval<
+ Selector extends string,
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<
+ Array<NodeFor<Selector>>,
+ Params
+ > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
+ >(
+ selector: Selector,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
+ const results = await this.$$(selector);
+ using elements = await this.evaluateHandle(
+ (_, ...elements) => {
+ return elements;
+ },
+ ...results
+ );
+ const [result] = await Promise.all([
+ elements.evaluate(pageFunction, ...args),
+ ...results.map(results => {
+ return results.dispose();
+ }),
+ ]);
+ return result;
+ }
+
+ /**
+ * @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix.
+ *
+ * Example: `await elementHandle.$$('xpath/' + xpathExpression)`
+ *
+ * The method evaluates the XPath expression relative to the elementHandle.
+ * If `xpath` starts with `//` instead of `.//`, the dot will be appended
+ * automatically.
+ *
+ * If there are no such elements, the method will resolve to an empty array.
+ * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
+ if (expression.startsWith('//')) {
+ expression = `.${expression}`;
+ }
+ return await this.$$(`xpath/${expression}`);
+ }
+
+ /**
+ * Wait for an element matching the given selector to appear in the current
+ * element.
+ *
+ * Unlike {@link Frame.waitForSelector}, this method does not work across
+ * navigations or if the element is detached from DOM.
+ *
+ * @example
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * let currentURL;
+ * page
+ * .mainFrame()
+ * .waitForSelector('img')
+ * .then(() => console.log('First URL with image: ' + currentURL));
+ *
+ * for (currentURL of [
+ * 'https://example.com',
+ * 'https://google.com',
+ * 'https://bbc.com',
+ * ]) {
+ * await page.goto(currentURL);
+ * }
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param selector - The selector to query and wait for.
+ * @param options - Options for customizing waiting behavior.
+ * @returns An element matching the given selector.
+ * @throws Throws if an element matching the given selector doesn't appear.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async waitForSelector<Selector extends string>(
+ selector: Selector,
+ options: WaitForSelectorOptions = {}
+ ): Promise<ElementHandle<NodeFor<Selector>> | null> {
+ const {updatedSelector, QueryHandler} =
+ getQueryHandlerAndSelector(selector);
+ return (await QueryHandler.waitFor(
+ this,
+ updatedSelector,
+ options
+ )) as ElementHandle<NodeFor<Selector>> | null;
+ }
+
+ async #checkVisibility(visibility: boolean): Promise<boolean> {
+ return await this.evaluate(
+ async (element, PuppeteerUtil, visibility) => {
+ return Boolean(PuppeteerUtil.checkVisibility(element, visibility));
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ visibility
+ );
+ }
+
+ /**
+ * Checks if an element is visible using the same mechanism as
+ * {@link ElementHandle.waitForSelector}.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async isVisible(): Promise<boolean> {
+ return await this.#checkVisibility(true);
+ }
+
+ /**
+ * Checks if an element is hidden using the same mechanism as
+ * {@link ElementHandle.waitForSelector}.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async isHidden(): Promise<boolean> {
+ return await this.#checkVisibility(false);
+ }
+
+ /**
+ * @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath`
+ * prefix.
+ *
+ * Example: `await elementHandle.waitForSelector('xpath/' + xpathExpression)`
+ *
+ * The method evaluates the XPath expression relative to the elementHandle.
+ *
+ * Wait for the `xpath` within the element. If at the moment of calling the
+ * method the `xpath` already exists, the method will return immediately. If
+ * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
+ * function will throw.
+ *
+ * If `xpath` starts with `//` instead of `.//`, the dot will be appended
+ * automatically.
+ *
+ * @example
+ * This method works across navigation.
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * let currentURL;
+ * page
+ * .waitForXPath('//img')
+ * .then(() => console.log('First URL with image: ' + currentURL));
+ * for (currentURL of [
+ * 'https://example.com',
+ * 'https://google.com',
+ * 'https://bbc.com',
+ * ]) {
+ * await page.goto(currentURL);
+ * }
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param xpath - A
+ * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an
+ * element to wait for
+ * @param options - Optional waiting parameters
+ * @returns Promise which resolves when element specified by xpath string is
+ * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
+ * not found in DOM, otherwise resolves to `ElementHandle`.
+ * @remarks
+ * The optional Argument `options` have properties:
+ *
+ * - `visible`: A boolean to wait for element to be present in DOM and to be
+ * visible, i.e. to not have `display: none` or `visibility: hidden` CSS
+ * properties. Defaults to `false`.
+ *
+ * - `hidden`: A boolean wait for element to not be found in the DOM or to be
+ * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
+ * Defaults to `false`.
+ *
+ * - `timeout`: A number which is maximum time to wait for in milliseconds.
+ * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The
+ * default value can be changed by using the {@link Page.setDefaultTimeout}
+ * method.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async waitForXPath(
+ xpath: string,
+ options: {
+ visible?: boolean;
+ hidden?: boolean;
+ timeout?: number;
+ } = {}
+ ): Promise<ElementHandle<Node> | null> {
+ if (xpath.startsWith('//')) {
+ xpath = `.${xpath}`;
+ }
+ return await this.waitForSelector(`xpath/${xpath}`, options);
+ }
+
+ /**
+ * Converts the current handle to the given element type.
+ *
+ * @example
+ *
+ * ```ts
+ * const element: ElementHandle<Element> = await page.$(
+ * '.class-name-of-anchor'
+ * );
+ * // DO NOT DISPOSE `element`, this will be always be the same handle.
+ * const anchor: ElementHandle<HTMLAnchorElement> =
+ * await element.toElement('a');
+ * ```
+ *
+ * @param tagName - The tag name of the desired element type.
+ * @throws An error if the handle does not match. **The handle will not be
+ * automatically disposed.**
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async toElement<
+ K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap,
+ >(tagName: K): Promise<HandleFor<ElementFor<K>>> {
+ const isMatchingTagName = await this.evaluate((node, tagName) => {
+ return node.nodeName === tagName.toUpperCase();
+ }, tagName);
+ if (!isMatchingTagName) {
+ throw new Error(`Element is not a(n) \`${tagName}\` element`);
+ }
+ return this as unknown as HandleFor<ElementFor<K>>;
+ }
+
+ /**
+ * Resolves the frame associated with the element, if any. Always exists for
+ * HTMLIFrameElements.
+ */
+ abstract contentFrame(this: ElementHandle<HTMLIFrameElement>): Promise<Frame>;
+ abstract contentFrame(): Promise<Frame | null>;
+
+ /**
+ * Returns the middle point within an element unless a specific offset is provided.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async clickablePoint(offset?: Offset): Promise<Point> {
+ const box = await this.#clickableBox();
+ if (!box) {
+ throw new Error('Node is either not clickable or not an Element');
+ }
+ if (offset !== undefined) {
+ return {
+ x: box.x + offset.x,
+ y: box.y + offset.y,
+ };
+ }
+ return {
+ x: box.x + box.width / 2,
+ y: box.y + box.height / 2,
+ };
+ }
+
+ /**
+ * This method scrolls element into view if needed, and then
+ * uses {@link Page} to hover over the center of the element.
+ * If the element is detached from DOM, the method throws an error.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async hover(this: ElementHandle<Element>): Promise<void> {
+ await this.scrollIntoViewIfNeeded();
+ const {x, y} = await this.clickablePoint();
+ await this.frame.page().mouse.move(x, y);
+ }
+
+ /**
+ * This method scrolls element into view if needed, and then
+ * uses {@link Page | Page.mouse} to click in the center of the element.
+ * If the element is detached from DOM, the method throws an error.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async click(
+ this: ElementHandle<Element>,
+ options: Readonly<ClickOptions> = {}
+ ): Promise<void> {
+ await this.scrollIntoViewIfNeeded();
+ const {x, y} = await this.clickablePoint(options.offset);
+ await this.frame.page().mouse.click(x, y, options);
+ }
+
+ /**
+ * Drags an element over the given element or point.
+ *
+ * @returns DEPRECATED. When drag interception is enabled, the drag payload is
+ * returned.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async drag(
+ this: ElementHandle<Element>,
+ target: Point | ElementHandle<Element>
+ ): Promise<Protocol.Input.DragData | void> {
+ await this.scrollIntoViewIfNeeded();
+ const page = this.frame.page();
+ if (page.isDragInterceptionEnabled()) {
+ const source = await this.clickablePoint();
+ if (target instanceof ElementHandle) {
+ target = await target.clickablePoint();
+ }
+ return await page.mouse.drag(source, target);
+ }
+ try {
+ if (!page._isDragging) {
+ page._isDragging = true;
+ await this.hover();
+ await page.mouse.down();
+ }
+ if (target instanceof ElementHandle) {
+ await target.hover();
+ } else {
+ await page.mouse.move(target.x, target.y);
+ }
+ } catch (error) {
+ page._isDragging = false;
+ throw error;
+ }
+ }
+
+ /**
+ * @deprecated Do not use. `dragenter` will automatically be performed during dragging.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async dragEnter(
+ this: ElementHandle<Element>,
+ data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
+ ): Promise<void> {
+ const page = this.frame.page();
+ await this.scrollIntoViewIfNeeded();
+ const target = await this.clickablePoint();
+ await page.mouse.dragEnter(target, data);
+ }
+
+ /**
+ * @deprecated Do not use. `dragover` will automatically be performed during dragging.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async dragOver(
+ this: ElementHandle<Element>,
+ data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
+ ): Promise<void> {
+ const page = this.frame.page();
+ await this.scrollIntoViewIfNeeded();
+ const target = await this.clickablePoint();
+ await page.mouse.dragOver(target, data);
+ }
+
+ /**
+ * Drops the given element onto the current one.
+ */
+ async drop(
+ this: ElementHandle<Element>,
+ element: ElementHandle<Element>
+ ): Promise<void>;
+
+ /**
+ * @deprecated No longer supported.
+ */
+ async drop(
+ this: ElementHandle<Element>,
+ data?: Protocol.Input.DragData
+ ): Promise<void>;
+
+ /**
+ * @internal
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async drop(
+ this: ElementHandle<Element>,
+ dataOrElement: ElementHandle<Element> | Protocol.Input.DragData = {
+ items: [],
+ dragOperationsMask: 1,
+ }
+ ): Promise<void> {
+ const page = this.frame.page();
+ if ('items' in dataOrElement) {
+ await this.scrollIntoViewIfNeeded();
+ const destination = await this.clickablePoint();
+ await page.mouse.drop(destination, dataOrElement);
+ } else {
+ // Note if the rest errors, we still want dragging off because the errors
+ // is most likely something implying the mouse is no longer dragging.
+ await dataOrElement.drag(this);
+ page._isDragging = false;
+ await page.mouse.up();
+ }
+ }
+
+ /**
+ * @deprecated Use `ElementHandle.drop` instead.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async dragAndDrop(
+ this: ElementHandle<Element>,
+ target: ElementHandle<Node>,
+ options?: {delay: number}
+ ): Promise<void> {
+ const page = this.frame.page();
+ assert(
+ page.isDragInterceptionEnabled(),
+ 'Drag Interception is not enabled!'
+ );
+ await this.scrollIntoViewIfNeeded();
+ const startPoint = await this.clickablePoint();
+ const targetPoint = await target.clickablePoint();
+ await page.mouse.dragAndDrop(startPoint, targetPoint, options);
+ }
+
+ /**
+ * Triggers a `change` and `input` event once all the provided options have been
+ * selected. If there's no `<select>` element matching `selector`, the method
+ * throws an error.
+ *
+ * @example
+ *
+ * ```ts
+ * handle.select('blue'); // single selection
+ * handle.select('red', 'green', 'blue'); // multiple selections
+ * ```
+ *
+ * @param values - Values of options to select. If the `<select>` has the
+ * `multiple` attribute, all values are considered, otherwise only the first
+ * one is taken into account.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async select(...values: string[]): Promise<string[]> {
+ for (const value of values) {
+ assert(
+ isString(value),
+ 'Values must be strings. Found value "' +
+ value +
+ '" of type "' +
+ typeof value +
+ '"'
+ );
+ }
+
+ return await this.evaluate((element, vals): string[] => {
+ const values = new Set(vals);
+ if (!(element instanceof HTMLSelectElement)) {
+ throw new Error('Element is not a <select> element.');
+ }
+
+ const selectedValues = new Set<string>();
+ if (!element.multiple) {
+ for (const option of element.options) {
+ option.selected = false;
+ }
+ for (const option of element.options) {
+ if (values.has(option.value)) {
+ option.selected = true;
+ selectedValues.add(option.value);
+ break;
+ }
+ }
+ } else {
+ for (const option of element.options) {
+ option.selected = values.has(option.value);
+ if (option.selected) {
+ selectedValues.add(option.value);
+ }
+ }
+ }
+ element.dispatchEvent(new Event('input', {bubbles: true}));
+ element.dispatchEvent(new Event('change', {bubbles: true}));
+ return [...selectedValues.values()];
+ }, values);
+ }
+
+ /**
+ * Sets the value of an
+ * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}
+ * to the given file paths.
+ *
+ * @remarks This will not validate whether the file paths exists. Also, if a
+ * path is relative, then it is resolved against the
+ * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}.
+ * For locals script connecting to remote chrome environments, paths must be
+ * absolute.
+ */
+ abstract uploadFile(
+ this: ElementHandle<HTMLInputElement>,
+ ...paths: string[]
+ ): Promise<void>;
+
+ /**
+ * This method scrolls element into view if needed, and then uses
+ * {@link Touchscreen.tap} to tap in the center of the element.
+ * If the element is detached from DOM, the method throws an error.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async tap(this: ElementHandle<Element>): Promise<void> {
+ await this.scrollIntoViewIfNeeded();
+ const {x, y} = await this.clickablePoint();
+ await this.frame.page().touchscreen.tap(x, y);
+ }
+
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async touchStart(this: ElementHandle<Element>): Promise<void> {
+ await this.scrollIntoViewIfNeeded();
+ const {x, y} = await this.clickablePoint();
+ await this.frame.page().touchscreen.touchStart(x, y);
+ }
+
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async touchMove(this: ElementHandle<Element>): Promise<void> {
+ await this.scrollIntoViewIfNeeded();
+ const {x, y} = await this.clickablePoint();
+ await this.frame.page().touchscreen.touchMove(x, y);
+ }
+
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async touchEnd(this: ElementHandle<Element>): Promise<void> {
+ await this.scrollIntoViewIfNeeded();
+ await this.frame.page().touchscreen.touchEnd();
+ }
+
+ /**
+ * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async focus(): Promise<void> {
+ await this.evaluate(element => {
+ if (!(element instanceof HTMLElement)) {
+ throw new Error('Cannot focus non-HTMLElement');
+ }
+ return element.focus();
+ });
+ }
+
+ /**
+ * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and
+ * `keyup` event for each character in the text.
+ *
+ * To press a special key, like `Control` or `ArrowDown`,
+ * use {@link ElementHandle.press}.
+ *
+ * @example
+ *
+ * ```ts
+ * await elementHandle.type('Hello'); // Types instantly
+ * await elementHandle.type('World', {delay: 100}); // Types slower, like a user
+ * ```
+ *
+ * @example
+ * An example of typing into a text field and then submitting the form:
+ *
+ * ```ts
+ * const elementHandle = await page.$('input');
+ * await elementHandle.type('some text');
+ * await elementHandle.press('Enter');
+ * ```
+ *
+ * @param options - Delay in milliseconds. Defaults to 0.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async type(
+ text: string,
+ options?: Readonly<KeyboardTypeOptions>
+ ): Promise<void> {
+ await this.focus();
+ await this.frame.page().keyboard.type(text, options);
+ }
+
+ /**
+ * Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}.
+ *
+ * @remarks
+ * If `key` is a single character and no modifier keys besides `Shift`
+ * are being held down, a `keypress`/`input` event will also be generated.
+ * The `text` option can be specified to force an input event to be generated.
+ *
+ * **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift`
+ * will type the text in upper case.
+ *
+ * @param key - Name of key to press, such as `ArrowLeft`.
+ * See {@link KeyInput} for a list of all key names.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async press(
+ key: KeyInput,
+ options?: Readonly<KeyPressOptions>
+ ): Promise<void> {
+ await this.focus();
+ await this.frame.page().keyboard.press(key, options);
+ }
+
+ async #clickableBox(): Promise<BoundingBox | null> {
+ const boxes = await this.evaluate(element => {
+ if (!(element instanceof Element)) {
+ return null;
+ }
+ return [...element.getClientRects()].map(rect => {
+ return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
+ });
+ });
+ if (!boxes?.length) {
+ return null;
+ }
+ await this.#intersectBoundingBoxesWithFrame(boxes);
+ let frame = this.frame;
+ let parentFrame: Frame | null | undefined;
+ while ((parentFrame = frame?.parentFrame())) {
+ using handle = await frame.frameElement();
+ if (!handle) {
+ throw new Error('Unsupported frame type');
+ }
+ const parentBox = await handle.evaluate(element => {
+ // Element is not visible.
+ if (element.getClientRects().length === 0) {
+ return null;
+ }
+ const rect = element.getBoundingClientRect();
+ const style = window.getComputedStyle(element);
+ return {
+ left:
+ rect.left +
+ parseInt(style.paddingLeft, 10) +
+ parseInt(style.borderLeftWidth, 10),
+ top:
+ rect.top +
+ parseInt(style.paddingTop, 10) +
+ parseInt(style.borderTopWidth, 10),
+ };
+ });
+ if (!parentBox) {
+ return null;
+ }
+ for (const box of boxes) {
+ box.x += parentBox.left;
+ box.y += parentBox.top;
+ }
+ await handle.#intersectBoundingBoxesWithFrame(boxes);
+ frame = parentFrame;
+ }
+ const box = boxes.find(box => {
+ return box.width >= 1 && box.height >= 1;
+ });
+ if (!box) {
+ return null;
+ }
+ return {
+ x: box.x,
+ y: box.y,
+ height: box.height,
+ width: box.width,
+ };
+ }
+
+ async #intersectBoundingBoxesWithFrame(boxes: BoundingBox[]) {
+ const {documentWidth, documentHeight} = await this.frame
+ .isolatedRealm()
+ .evaluate(() => {
+ return {
+ documentWidth: document.documentElement.clientWidth,
+ documentHeight: document.documentElement.clientHeight,
+ };
+ });
+ for (const box of boxes) {
+ intersectBoundingBox(box, documentWidth, documentHeight);
+ }
+ }
+
+ /**
+ * This method returns the bounding box of the element (relative to the main frame),
+ * or `null` if the element is {@link https://drafts.csswg.org/css-display-4/#box-generation | not part of the layout}
+ * (example: `display: none`).
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async boundingBox(): Promise<BoundingBox | null> {
+ const box = await this.evaluate(element => {
+ if (!(element instanceof Element)) {
+ return null;
+ }
+ // Element is not visible.
+ if (element.getClientRects().length === 0) {
+ return null;
+ }
+ const rect = element.getBoundingClientRect();
+ return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
+ });
+ if (!box) {
+ return null;
+ }
+ const offset = await this.#getTopLeftCornerOfFrame();
+ if (!offset) {
+ return null;
+ }
+ return {
+ x: box.x + offset.x,
+ y: box.y + offset.y,
+ height: box.height,
+ width: box.width,
+ };
+ }
+
+ /**
+ * This method returns boxes of the element,
+ * or `null` if the element is {@link https://drafts.csswg.org/css-display-4/#box-generation | not part of the layout}
+ * (example: `display: none`).
+ *
+ * @remarks
+ *
+ * Boxes are represented as an array of points;
+ * Each Point is an object `{x, y}`. Box points are sorted clock-wise.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async boxModel(): Promise<BoxModel | null> {
+ const model = await this.evaluate(element => {
+ if (!(element instanceof Element)) {
+ return null;
+ }
+ // Element is not visible.
+ if (element.getClientRects().length === 0) {
+ return null;
+ }
+ const rect = element.getBoundingClientRect();
+ const style = window.getComputedStyle(element);
+ const offsets = {
+ padding: {
+ left: parseInt(style.paddingLeft, 10),
+ top: parseInt(style.paddingTop, 10),
+ right: parseInt(style.paddingRight, 10),
+ bottom: parseInt(style.paddingBottom, 10),
+ },
+ margin: {
+ left: -parseInt(style.marginLeft, 10),
+ top: -parseInt(style.marginTop, 10),
+ right: -parseInt(style.marginRight, 10),
+ bottom: -parseInt(style.marginBottom, 10),
+ },
+ border: {
+ left: parseInt(style.borderLeft, 10),
+ top: parseInt(style.borderTop, 10),
+ right: parseInt(style.borderRight, 10),
+ bottom: parseInt(style.borderBottom, 10),
+ },
+ };
+ const border: Quad = [
+ {x: rect.left, y: rect.top},
+ {x: rect.left + rect.width, y: rect.top},
+ {x: rect.left + rect.width, y: rect.top + rect.bottom},
+ {x: rect.left, y: rect.top + rect.bottom},
+ ];
+ const padding = transformQuadWithOffsets(border, offsets.border);
+ const content = transformQuadWithOffsets(padding, offsets.padding);
+ const margin = transformQuadWithOffsets(border, offsets.margin);
+ return {
+ content,
+ padding,
+ border,
+ margin,
+ width: rect.width,
+ height: rect.height,
+ };
+
+ function transformQuadWithOffsets(
+ quad: Quad,
+ offsets: {top: number; left: number; right: number; bottom: number}
+ ): Quad {
+ return [
+ {
+ x: quad[0].x + offsets.left,
+ y: quad[0].y + offsets.top,
+ },
+ {
+ x: quad[1].x - offsets.right,
+ y: quad[1].y + offsets.top,
+ },
+ {
+ x: quad[2].x - offsets.right,
+ y: quad[2].y - offsets.bottom,
+ },
+ {
+ x: quad[3].x + offsets.left,
+ y: quad[3].y - offsets.bottom,
+ },
+ ];
+ }
+ });
+ if (!model) {
+ return null;
+ }
+ const offset = await this.#getTopLeftCornerOfFrame();
+ if (!offset) {
+ return null;
+ }
+ for (const attribute of [
+ 'content',
+ 'padding',
+ 'border',
+ 'margin',
+ ] as const) {
+ for (const point of model[attribute]) {
+ point.x += offset.x;
+ point.y += offset.y;
+ }
+ }
+ return model;
+ }
+
+ async #getTopLeftCornerOfFrame() {
+ const point = {x: 0, y: 0};
+ let frame = this.frame;
+ let parentFrame: Frame | null | undefined;
+ while ((parentFrame = frame?.parentFrame())) {
+ using handle = await frame.frameElement();
+ if (!handle) {
+ throw new Error('Unsupported frame type');
+ }
+ const parentBox = await handle.evaluate(element => {
+ // Element is not visible.
+ if (element.getClientRects().length === 0) {
+ return null;
+ }
+ const rect = element.getBoundingClientRect();
+ const style = window.getComputedStyle(element);
+ return {
+ left:
+ rect.left +
+ parseInt(style.paddingLeft, 10) +
+ parseInt(style.borderLeftWidth, 10),
+ top:
+ rect.top +
+ parseInt(style.paddingTop, 10) +
+ parseInt(style.borderTopWidth, 10),
+ };
+ });
+ if (!parentBox) {
+ return null;
+ }
+ point.x += parentBox.left;
+ point.y += parentBox.top;
+ frame = parentFrame;
+ }
+ return point;
+ }
+
+ /**
+ * This method scrolls element into view if needed, and then uses
+ * {@link Page.(screenshot:2) } to take a screenshot of the element.
+ * If the element is detached from DOM, the method throws an error.
+ */
+ async screenshot(
+ options: Readonly<ScreenshotOptions> & {encoding: 'base64'}
+ ): Promise<string>;
+ async screenshot(options?: Readonly<ScreenshotOptions>): Promise<Buffer>;
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async screenshot(
+ this: ElementHandle<Element>,
+ options: Readonly<ElementScreenshotOptions> = {}
+ ): Promise<string | Buffer> {
+ const {scrollIntoView = true} = options;
+
+ let clip = await this.#nonEmptyVisibleBoundingBox();
+
+ const page = this.frame.page();
+
+ // If the element is larger than the viewport, `captureBeyondViewport` will
+ // _not_ affect element rendering, so we need to adjust the viewport to
+ // properly render the element.
+ const viewport = page.viewport() ?? {
+ width: clip.width,
+ height: clip.height,
+ };
+ await using stack = new AsyncDisposableStack();
+ if (clip.width > viewport.width || clip.height > viewport.height) {
+ await this.frame.page().setViewport({
+ ...viewport,
+ width: Math.max(viewport.width, Math.ceil(clip.width)),
+ height: Math.max(viewport.height, Math.ceil(clip.height)),
+ });
+
+ stack.defer(async () => {
+ try {
+ await this.frame.page().setViewport(viewport);
+ } catch (error) {
+ debugError(error);
+ }
+ });
+ }
+
+ // Only scroll the element into view if the user wants it.
+ if (scrollIntoView) {
+ await this.scrollIntoViewIfNeeded();
+
+ // We measure again just in case.
+ clip = await this.#nonEmptyVisibleBoundingBox();
+ }
+
+ const [pageLeft, pageTop] = await this.evaluate(() => {
+ if (!window.visualViewport) {
+ throw new Error('window.visualViewport is not supported.');
+ }
+ return [
+ window.visualViewport.pageLeft,
+ window.visualViewport.pageTop,
+ ] as const;
+ });
+ clip.x += pageLeft;
+ clip.y += pageTop;
+
+ return await page.screenshot({...options, clip});
+ }
+
+ async #nonEmptyVisibleBoundingBox() {
+ const box = await this.boundingBox();
+ assert(box, 'Node is either not visible or not an HTMLElement');
+ assert(box.width !== 0, 'Node has 0 width.');
+ assert(box.height !== 0, 'Node has 0 height.');
+ return box;
+ }
+
+ /**
+ * @internal
+ */
+ protected async assertConnectedElement(): Promise<void> {
+ const error = await this.evaluate(async element => {
+ if (!element.isConnected) {
+ return 'Node is detached from document';
+ }
+ if (element.nodeType !== Node.ELEMENT_NODE) {
+ return 'Node is not of type HTMLElement';
+ }
+ return;
+ });
+
+ if (error) {
+ throw new Error(error);
+ }
+ }
+
+ /**
+ * @internal
+ */
+ protected async scrollIntoViewIfNeeded(
+ this: ElementHandle<Element>
+ ): Promise<void> {
+ if (
+ await this.isIntersectingViewport({
+ threshold: 1,
+ })
+ ) {
+ return;
+ }
+ await this.scrollIntoView();
+ }
+
+ /**
+ * Resolves to true if the element is visible in the current viewport. If an
+ * element is an SVG, we check if the svg owner element is in the viewport
+ * instead. See https://crbug.com/963246.
+ *
+ * @param options - Threshold for the intersection between 0 (no intersection) and 1
+ * (full intersection). Defaults to 1.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async isIntersectingViewport(
+ this: ElementHandle<Element>,
+ options: {
+ threshold?: number;
+ } = {}
+ ): Promise<boolean> {
+ await this.assertConnectedElement();
+ // eslint-disable-next-line rulesdir/use-using -- Returns `this`.
+ const handle = await this.#asSVGElementHandle();
+ using target = handle && (await handle.#getOwnerSVGElement());
+ return await ((target ?? this) as ElementHandle<Element>).evaluate(
+ async (element, threshold) => {
+ const visibleRatio = await new Promise<number>(resolve => {
+ const observer = new IntersectionObserver(entries => {
+ resolve(entries[0]!.intersectionRatio);
+ observer.disconnect();
+ });
+ observer.observe(element);
+ });
+ return threshold === 1 ? visibleRatio === 1 : visibleRatio > threshold;
+ },
+ options.threshold ?? 0
+ );
+ }
+
+ /**
+ * Scrolls the element into view using either the automation protocol client
+ * or by calling element.scrollIntoView.
+ */
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ async scrollIntoView(this: ElementHandle<Element>): Promise<void> {
+ await this.assertConnectedElement();
+ await this.evaluate(async (element): Promise<void> => {
+ element.scrollIntoView({
+ block: 'center',
+ inline: 'center',
+ behavior: 'instant',
+ });
+ });
+ }
+
+ /**
+ * Returns true if an element is an SVGElement (included svg, path, rect
+ * etc.).
+ */
+ async #asSVGElementHandle(
+ this: ElementHandle<Element>
+ ): Promise<ElementHandle<SVGElement> | null> {
+ if (
+ await this.evaluate(element => {
+ return element instanceof SVGElement;
+ })
+ ) {
+ return this as ElementHandle<SVGElement>;
+ } else {
+ return null;
+ }
+ }
+
+ async #getOwnerSVGElement(
+ this: ElementHandle<SVGElement>
+ ): Promise<ElementHandle<SVGSVGElement>> {
+ // SVGSVGElement.ownerSVGElement === null.
+ return await this.evaluateHandle(element => {
+ if (element instanceof SVGSVGElement) {
+ return element;
+ }
+ return element.ownerSVGElement!;
+ });
+ }
+
+ /**
+ * If the element is a form input, you can use {@link ElementHandle.autofill}
+ * to test if the form is compatible with the browser's autofill
+ * implementation. Throws an error if the form cannot be autofilled.
+ *
+ * @remarks
+ *
+ * Currently, Puppeteer supports auto-filling credit card information only and
+ * in Chrome in the new headless and headful modes only.
+ *
+ * ```ts
+ * // Select an input on the credit card form.
+ * const name = await page.waitForSelector('form #name');
+ * // Trigger autofill with the desired data.
+ * await name.autofill({
+ * creditCard: {
+ * number: '4444444444444444',
+ * name: 'John Smith',
+ * expiryMonth: '01',
+ * expiryYear: '2030',
+ * cvc: '123',
+ * },
+ * });
+ * ```
+ */
+ abstract autofill(data: AutofillData): Promise<void>;
+}
+
+/**
+ * @public
+ */
+export interface AutofillData {
+ creditCard: {
+ // See https://chromedevtools.github.io/devtools-protocol/tot/Autofill/#type-CreditCard.
+ number: string;
+ name: string;
+ expiryMonth: string;
+ expiryYear: string;
+ cvc: string;
+ };
+}
+
+function intersectBoundingBox(
+ box: BoundingBox,
+ width: number,
+ height: number
+): void {
+ box.width = Math.max(
+ box.x >= 0
+ ? Math.min(width - box.x, box.width)
+ : Math.min(width, box.width + box.x),
+ 0
+ );
+ box.height = Math.max(
+ box.y >= 0
+ ? Math.min(height - box.y, box.height)
+ : Math.min(height, box.height + box.y),
+ 0
+ );
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandleSymbol.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandleSymbol.ts
new file mode 100644
index 0000000000..6e5087b773
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandleSymbol.ts
@@ -0,0 +1,10 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export const _isElementHandle = Symbol('_isElementHandle');
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Environment.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Environment.ts
new file mode 100644
index 0000000000..c5a8d73d00
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Environment.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {CDPSession} from './CDPSession.js';
+import type {Realm} from './Realm.js';
+
+/**
+ * @internal
+ */
+export interface Environment {
+ get client(): CDPSession;
+ mainRealm(): Realm;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts
new file mode 100644
index 0000000000..757ec872c6
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Frame.ts
@@ -0,0 +1,1218 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type Protocol from 'devtools-protocol';
+
+import type {ClickOptions, ElementHandle} from '../api/ElementHandle.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+import type {
+ Page,
+ WaitForSelectorOptions,
+ WaitTimeoutOptions,
+} from '../api/Page.js';
+import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
+import type {IsolatedWorldChart} from '../cdp/IsolatedWorld.js';
+import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
+import {EventEmitter, type EventType} from '../common/EventEmitter.js';
+import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
+import {transposeIterableHandle} from '../common/HandleIterator.js';
+import {LazyArg} from '../common/LazyArg.js';
+import type {
+ Awaitable,
+ EvaluateFunc,
+ EvaluateFuncWith,
+ HandleFor,
+ NodeFor,
+} from '../common/types.js';
+import {
+ importFSPromises,
+ withSourcePuppeteerURLIfNone,
+} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {throwIfDisposed} from '../util/decorators.js';
+
+import type {CDPSession} from './CDPSession.js';
+import type {KeyboardTypeOptions} from './Input.js';
+import {
+ FunctionLocator,
+ type Locator,
+ NodeLocator,
+} from './locators/locators.js';
+import type {Realm} from './Realm.js';
+
+/**
+ * @public
+ */
+export interface WaitForOptions {
+ /**
+ * Maximum wait time in milliseconds. Pass 0 to disable the timeout.
+ *
+ * The default value can be changed by using the
+ * {@link Page.setDefaultTimeout} or {@link Page.setDefaultNavigationTimeout}
+ * methods.
+ *
+ * @defaultValue `30000`
+ */
+ timeout?: number;
+ /**
+ * When to consider waiting succeeds. Given an array of event strings, waiting
+ * is considered to be successful after all events have been fired.
+ *
+ * @defaultValue `'load'`
+ */
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+}
+
+/**
+ * @public
+ */
+export interface GoToOptions extends WaitForOptions {
+ /**
+ * If provided, it will take preference over the referer header value set by
+ * {@link Page.setExtraHTTPHeaders | page.setExtraHTTPHeaders()}.
+ */
+ referer?: string;
+ /**
+ * If provided, it will take preference over the referer-policy header value
+ * set by {@link Page.setExtraHTTPHeaders | page.setExtraHTTPHeaders()}.
+ */
+ referrerPolicy?: string;
+}
+
+/**
+ * @public
+ */
+export interface FrameWaitForFunctionOptions {
+ /**
+ * An interval at which the `pageFunction` is executed, defaults to `raf`. If
+ * `polling` is a number, then it is treated as an interval in milliseconds at
+ * which the function would be executed. If `polling` is a string, then it can
+ * be one of the following values:
+ *
+ * - `raf` - to constantly execute `pageFunction` in `requestAnimationFrame`
+ * callback. This is the tightest polling mode which is suitable to observe
+ * styling changes.
+ *
+ * - `mutation` - to execute `pageFunction` on every DOM mutation.
+ */
+ polling?: 'raf' | 'mutation' | number;
+ /**
+ * Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
+ * Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
+ * using {@link Page.setDefaultTimeout}.
+ */
+ timeout?: number;
+ /**
+ * A signal object that allows you to cancel a waitForFunction call.
+ */
+ signal?: AbortSignal;
+}
+
+/**
+ * @public
+ */
+export interface FrameAddScriptTagOptions {
+ /**
+ * URL of the script to be added.
+ */
+ url?: string;
+ /**
+ * Path to a JavaScript file to be injected into the frame.
+ *
+ * @remarks
+ * If `path` is a relative path, it is resolved relative to the current
+ * working directory (`process.cwd()` in Node.js).
+ */
+ path?: string;
+ /**
+ * JavaScript to be injected into the frame.
+ */
+ content?: string;
+ /**
+ * Sets the `type` of the script. Use `module` in order to load an ES2015 module.
+ */
+ type?: string;
+ /**
+ * Sets the `id` of the script.
+ */
+ id?: string;
+}
+
+/**
+ * @public
+ */
+export interface FrameAddStyleTagOptions {
+ /**
+ * the URL of the CSS file to be added.
+ */
+ url?: string;
+ /**
+ * The path to a CSS file to be injected into the frame.
+ * @remarks
+ * If `path` is a relative path, it is resolved relative to the current
+ * working directory (`process.cwd()` in Node.js).
+ */
+ path?: string;
+ /**
+ * Raw CSS content to be injected into the frame.
+ */
+ content?: string;
+}
+
+/**
+ * @public
+ */
+export interface FrameEvents extends Record<EventType, unknown> {
+ /** @internal */
+ [FrameEvent.FrameNavigated]: Protocol.Page.NavigationType;
+ /** @internal */
+ [FrameEvent.FrameSwapped]: undefined;
+ /** @internal */
+ [FrameEvent.LifecycleEvent]: undefined;
+ /** @internal */
+ [FrameEvent.FrameNavigatedWithinDocument]: undefined;
+ /** @internal */
+ [FrameEvent.FrameDetached]: Frame;
+ /** @internal */
+ [FrameEvent.FrameSwappedByActivation]: undefined;
+}
+
+/**
+ * We use symbols to prevent external parties listening to these events.
+ * They are internal to Puppeteer.
+ *
+ * @internal
+ */
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace FrameEvent {
+ export const FrameNavigated = Symbol('Frame.FrameNavigated');
+ export const FrameSwapped = Symbol('Frame.FrameSwapped');
+ export const LifecycleEvent = Symbol('Frame.LifecycleEvent');
+ export const FrameNavigatedWithinDocument = Symbol(
+ 'Frame.FrameNavigatedWithinDocument'
+ );
+ export const FrameDetached = Symbol('Frame.FrameDetached');
+ export const FrameSwappedByActivation = Symbol(
+ 'Frame.FrameSwappedByActivation'
+ );
+}
+
+/**
+ * @internal
+ */
+export const throwIfDetached = throwIfDisposed<Frame>(frame => {
+ return `Attempted to use detached Frame '${frame._id}'.`;
+});
+
+/**
+ * Represents a DOM frame.
+ *
+ * To understand frames, you can think of frames as `<iframe>` elements. Just
+ * like iframes, frames can be nested, and when JavaScript is executed in a
+ * frame, the JavaScript does not effect frames inside the ambient frame the
+ * JavaScript executes in.
+ *
+ * @example
+ * At any point in time, {@link Page | pages} expose their current frame
+ * tree via the {@link Page.mainFrame} and {@link Frame.childFrames} methods.
+ *
+ * @example
+ * An example of dumping frame tree:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.goto('https://www.google.com/chrome/browser/canary.html');
+ * dumpFrameTree(page.mainFrame(), '');
+ * await browser.close();
+ *
+ * function dumpFrameTree(frame, indent) {
+ * console.log(indent + frame.url());
+ * for (const child of frame.childFrames()) {
+ * dumpFrameTree(child, indent + ' ');
+ * }
+ * }
+ * })();
+ * ```
+ *
+ * @example
+ * An example of getting text from an iframe element:
+ *
+ * ```ts
+ * const frame = page.frames().find(frame => frame.name() === 'myframe');
+ * const text = await frame.$eval('.selector', element => element.textContent);
+ * console.log(text);
+ * ```
+ *
+ * @remarks
+ * Frame lifecycles are controlled by three events that are all dispatched on
+ * the parent {@link Frame.page | page}:
+ *
+ * - {@link PageEvent.FrameAttached}
+ * - {@link PageEvent.FrameNavigated}
+ * - {@link PageEvent.FrameDetached}
+ *
+ * @public
+ */
+export abstract class Frame extends EventEmitter<FrameEvents> {
+ /**
+ * @internal
+ */
+ _id!: string;
+ /**
+ * @internal
+ */
+ _parentId?: string;
+
+ /**
+ * @internal
+ */
+ worlds!: IsolatedWorldChart;
+
+ /**
+ * @internal
+ */
+ _name?: string;
+
+ /**
+ * @internal
+ */
+ _hasStartedLoading = false;
+
+ /**
+ * @internal
+ */
+ constructor() {
+ super();
+ }
+
+ /**
+ * The page associated with the frame.
+ */
+ abstract page(): Page;
+
+ /**
+ * Is `true` if the frame is an out-of-process (OOP) frame. Otherwise,
+ * `false`.
+ */
+ abstract isOOPFrame(): boolean;
+
+ /**
+ * Navigates the frame to the given `url`.
+ *
+ * @remarks
+ * Navigation to `about:blank` or navigation to the same URL with a different
+ * hash will succeed and return `null`.
+ *
+ * :::warning
+ *
+ * Headless mode doesn't support navigation to a PDF document. See the {@link
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=761295 | upstream
+ * issue}.
+ *
+ * :::
+ *
+ * @param url - URL to navigate the frame to. The URL should include scheme,
+ * e.g. `https://`
+ * @param options - Options to configure waiting behavior.
+ * @returns A promise which resolves to the main resource response. In case of
+ * multiple redirects, the navigation will resolve with the response of the
+ * last redirect.
+ * @throws If:
+ *
+ * - there's an SSL error (e.g. in case of self-signed certificates).
+ * - target URL is invalid.
+ * - the timeout is exceeded during navigation.
+ * - the remote server does not respond or is unreachable.
+ * - the main resource failed to load.
+ *
+ * This method will not throw an error when any valid HTTP status code is
+ * returned by the remote server, including 404 "Not Found" and 500 "Internal
+ * Server Error". The status code for such responses can be retrieved by
+ * calling {@link HTTPResponse.status}.
+ */
+ abstract goto(
+ url: string,
+ options?: {
+ referer?: string;
+ referrerPolicy?: string;
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ }
+ ): Promise<HTTPResponse | null>;
+
+ /**
+ * Waits for the frame to navigate. It is useful for when you run code which
+ * will indirectly cause the frame to navigate.
+ *
+ * Usage of the
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/History_API | History API}
+ * to change the URL is considered a navigation.
+ *
+ * @example
+ *
+ * ```ts
+ * const [response] = await Promise.all([
+ * // The navigation promise resolves after navigation has finished
+ * frame.waitForNavigation(),
+ * // Clicking the link will indirectly cause a navigation
+ * frame.click('a.my-link'),
+ * ]);
+ * ```
+ *
+ * @param options - Options to configure waiting behavior.
+ * @returns A promise which resolves to the main resource response.
+ */
+ abstract waitForNavigation(
+ options?: WaitForOptions
+ ): Promise<HTTPResponse | null>;
+
+ /**
+ * @internal
+ */
+ abstract get client(): CDPSession;
+
+ /**
+ * @internal
+ */
+ abstract mainRealm(): Realm;
+
+ /**
+ * @internal
+ */
+ abstract isolatedRealm(): Realm;
+
+ #_document: Promise<ElementHandle<Document>> | undefined;
+
+ /**
+ * @internal
+ */
+ #document(): Promise<ElementHandle<Document>> {
+ if (!this.#_document) {
+ this.#_document = this.isolatedRealm()
+ .evaluateHandle(() => {
+ return document;
+ })
+ .then(handle => {
+ return this.mainRealm().transferHandle(handle);
+ });
+ }
+ return this.#_document;
+ }
+
+ /**
+ * Used to clear the document handle that has been destroyed.
+ *
+ * @internal
+ */
+ clearDocumentHandle(): void {
+ this.#_document = undefined;
+ }
+
+ /**
+ * @internal
+ */
+ @throwIfDetached
+ async frameElement(): Promise<HandleFor<HTMLIFrameElement> | null> {
+ const parentFrame = this.parentFrame();
+ if (!parentFrame) {
+ return null;
+ }
+ using list = await parentFrame.isolatedRealm().evaluateHandle(() => {
+ return document.querySelectorAll('iframe');
+ });
+ for await (using iframe of transposeIterableHandle(list)) {
+ const frame = await iframe.contentFrame();
+ if (frame._id === this._id) {
+ return iframe.move();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Behaves identically to {@link Page.evaluateHandle} except it's run within
+ * the context of this frame.
+ *
+ * @see {@link Page.evaluateHandle} for details.
+ */
+ @throwIfDetached
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluateHandle.name,
+ pageFunction
+ );
+ return await this.mainRealm().evaluateHandle(pageFunction, ...args);
+ }
+
+ /**
+ * Behaves identically to {@link Page.evaluate} except it's run within the
+ * the context of this frame.
+ *
+ * @see {@link Page.evaluate} for details.
+ */
+ @throwIfDetached
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluate.name,
+ pageFunction
+ );
+ return await this.mainRealm().evaluate(pageFunction, ...args);
+ }
+
+ /**
+ * Creates a locator for the provided selector. See {@link Locator} for
+ * details and supported actions.
+ *
+ * @remarks
+ * Locators API is experimental and we will not follow semver for breaking
+ * change in the Locators API.
+ */
+ locator<Selector extends string>(
+ selector: Selector
+ ): Locator<NodeFor<Selector>>;
+
+ /**
+ * Creates a locator for the provided function. See {@link Locator} for
+ * details and supported actions.
+ *
+ * @remarks
+ * Locators API is experimental and we will not follow semver for breaking
+ * change in the Locators API.
+ */
+ locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
+
+ /**
+ * @internal
+ */
+ @throwIfDetached
+ locator<Selector extends string, Ret>(
+ selectorOrFunc: Selector | (() => Awaitable<Ret>)
+ ): Locator<NodeFor<Selector>> | Locator<Ret> {
+ if (typeof selectorOrFunc === 'string') {
+ return NodeLocator.create(this, selectorOrFunc);
+ } else {
+ return FunctionLocator.create(this, selectorOrFunc);
+ }
+ }
+ /**
+ * Queries the frame for an element matching the given selector.
+ *
+ * @param selector - The selector to query for.
+ * @returns A {@link ElementHandle | element handle} to the first element
+ * matching the given selector. Otherwise, `null`.
+ */
+ @throwIfDetached
+ async $<Selector extends string>(
+ selector: Selector
+ ): Promise<ElementHandle<NodeFor<Selector>> | null> {
+ // eslint-disable-next-line rulesdir/use-using -- This is cached.
+ const document = await this.#document();
+ return await document.$(selector);
+ }
+
+ /**
+ * Queries the frame for all elements matching the given selector.
+ *
+ * @param selector - The selector to query for.
+ * @returns An array of {@link ElementHandle | element handles} that point to
+ * elements matching the given selector.
+ */
+ @throwIfDetached
+ async $$<Selector extends string>(
+ selector: Selector
+ ): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
+ // eslint-disable-next-line rulesdir/use-using -- This is cached.
+ const document = await this.#document();
+ return await document.$$(selector);
+ }
+
+ /**
+ * Runs the given function on the first element matching the given selector in
+ * the frame.
+ *
+ * If the given function returns a promise, then this method will wait till
+ * the promise resolves.
+ *
+ * @example
+ *
+ * ```ts
+ * const searchValue = await frame.$eval('#search', el => el.value);
+ * ```
+ *
+ * @param selector - The selector to query for.
+ * @param pageFunction - The function to be evaluated in the frame's context.
+ * The first element matching the selector will be passed to the function as
+ * its first argument.
+ * @param args - Additional arguments to pass to `pageFunction`.
+ * @returns A promise to the result of the function.
+ */
+ @throwIfDetached
+ async $eval<
+ Selector extends string,
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
+ NodeFor<Selector>,
+ Params
+ >,
+ >(
+ selector: Selector,
+ pageFunction: string | Func,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
+ // eslint-disable-next-line rulesdir/use-using -- This is cached.
+ const document = await this.#document();
+ return await document.$eval(selector, pageFunction, ...args);
+ }
+
+ /**
+ * Runs the given function on an array of elements matching the given selector
+ * in the frame.
+ *
+ * If the given function returns a promise, then this method will wait till
+ * the promise resolves.
+ *
+ * @example
+ *
+ * ```ts
+ * const divsCounts = await frame.$$eval('div', divs => divs.length);
+ * ```
+ *
+ * @param selector - The selector to query for.
+ * @param pageFunction - The function to be evaluated in the frame's context.
+ * An array of elements matching the given selector will be passed to the
+ * function as its first argument.
+ * @param args - Additional arguments to pass to `pageFunction`.
+ * @returns A promise to the result of the function.
+ */
+ @throwIfDetached
+ async $$eval<
+ Selector extends string,
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<
+ Array<NodeFor<Selector>>,
+ Params
+ > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
+ >(
+ selector: Selector,
+ pageFunction: string | Func,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
+ // eslint-disable-next-line rulesdir/use-using -- This is cached.
+ const document = await this.#document();
+ return await document.$$eval(selector, pageFunction, ...args);
+ }
+
+ /**
+ * @deprecated Use {@link Frame.$$} with the `xpath` prefix.
+ *
+ * Example: `await frame.$$('xpath/' + xpathExpression)`
+ *
+ * This method evaluates the given XPath expression and returns the results.
+ * If `xpath` starts with `//` instead of `.//`, the dot will be appended
+ * automatically.
+ * @param expression - the XPath expression to evaluate.
+ */
+ @throwIfDetached
+ async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
+ // eslint-disable-next-line rulesdir/use-using -- This is cached.
+ const document = await this.#document();
+ return await document.$x(expression);
+ }
+
+ /**
+ * Waits for an element matching the given selector to appear in the frame.
+ *
+ * This method works across navigations.
+ *
+ * @example
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * let currentURL;
+ * page
+ * .mainFrame()
+ * .waitForSelector('img')
+ * .then(() => console.log('First URL with image: ' + currentURL));
+ *
+ * for (currentURL of [
+ * 'https://example.com',
+ * 'https://google.com',
+ * 'https://bbc.com',
+ * ]) {
+ * await page.goto(currentURL);
+ * }
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param selector - The selector to query and wait for.
+ * @param options - Options for customizing waiting behavior.
+ * @returns An element matching the given selector.
+ * @throws Throws if an element matching the given selector doesn't appear.
+ */
+ @throwIfDetached
+ async waitForSelector<Selector extends string>(
+ selector: Selector,
+ options: WaitForSelectorOptions = {}
+ ): Promise<ElementHandle<NodeFor<Selector>> | null> {
+ const {updatedSelector, QueryHandler} =
+ getQueryHandlerAndSelector(selector);
+ return (await QueryHandler.waitFor(
+ this,
+ updatedSelector,
+ options
+ )) as ElementHandle<NodeFor<Selector>> | null;
+ }
+
+ /**
+ * @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix.
+ *
+ * Example: `await frame.waitForSelector('xpath/' + xpathExpression)`
+ *
+ * The method evaluates the XPath expression relative to the Frame.
+ * If `xpath` starts with `//` instead of `.//`, the dot will be appended
+ * automatically.
+ *
+ * Wait for the `xpath` to appear in page. If at the moment of calling the
+ * method the `xpath` already exists, the method will return immediately. If
+ * the xpath doesn't appear after the `timeout` milliseconds of waiting, the
+ * function will throw.
+ *
+ * For a code example, see the example for {@link Frame.waitForSelector}. That
+ * function behaves identically other than taking a CSS selector rather than
+ * an XPath.
+ *
+ * @param xpath - the XPath expression to wait for.
+ * @param options - options to configure the visibility of the element and how
+ * long to wait before timing out.
+ */
+ @throwIfDetached
+ async waitForXPath(
+ xpath: string,
+ options: WaitForSelectorOptions = {}
+ ): Promise<ElementHandle<Node> | null> {
+ if (xpath.startsWith('//')) {
+ xpath = `.${xpath}`;
+ }
+ return await this.waitForSelector(`xpath/${xpath}`, options);
+ }
+
+ /**
+ * @example
+ * The `waitForFunction` can be used to observe viewport size change:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * . const browser = await puppeteer.launch();
+ * . const page = await browser.newPage();
+ * . const watchDog = page.mainFrame().waitForFunction('window.innerWidth < 100');
+ * . page.setViewport({width: 50, height: 50});
+ * . await watchDog;
+ * . await browser.close();
+ * })();
+ * ```
+ *
+ * To pass arguments from Node.js to the predicate of `page.waitForFunction` function:
+ *
+ * ```ts
+ * const selector = '.foo';
+ * await frame.waitForFunction(
+ * selector => !!document.querySelector(selector),
+ * {}, // empty options object
+ * selector
+ * );
+ * ```
+ *
+ * @param pageFunction - the function to evaluate in the frame context.
+ * @param options - options to configure the polling method and timeout.
+ * @param args - arguments to pass to the `pageFunction`.
+ * @returns the promise which resolve when the `pageFunction` returns a truthy value.
+ */
+ @throwIfDetached
+ async waitForFunction<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ options: FrameWaitForFunctionOptions = {},
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ return await (this.mainRealm().waitForFunction(
+ pageFunction,
+ options,
+ ...args
+ ) as Promise<HandleFor<Awaited<ReturnType<Func>>>>);
+ }
+ /**
+ * The full HTML contents of the frame, including the DOCTYPE.
+ */
+ @throwIfDetached
+ async content(): Promise<string> {
+ return await this.evaluate(() => {
+ let content = '';
+ for (const node of document.childNodes) {
+ switch (node) {
+ case document.documentElement:
+ content += document.documentElement.outerHTML;
+ break;
+ default:
+ content += new XMLSerializer().serializeToString(node);
+ break;
+ }
+ }
+
+ return content;
+ });
+ }
+
+ /**
+ * Set the content of the frame.
+ *
+ * @param html - HTML markup to assign to the page.
+ * @param options - Options to configure how long before timing out and at
+ * what point to consider the content setting successful.
+ */
+ abstract setContent(
+ html: string,
+ options?: {
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ }
+ ): Promise<void>;
+
+ /**
+ * @internal
+ */
+ async setFrameContent(content: string): Promise<void> {
+ return await this.evaluate(html => {
+ document.open();
+ document.write(html);
+ document.close();
+ }, content);
+ }
+
+ /**
+ * The frame's `name` attribute as specified in the tag.
+ *
+ * @remarks
+ * If the name is empty, it returns the `id` attribute instead.
+ *
+ * @remarks
+ * This value is calculated once when the frame is created, and will not
+ * update if the attribute is changed later.
+ */
+ name(): string {
+ return this._name || '';
+ }
+
+ /**
+ * The frame's URL.
+ */
+ abstract url(): string;
+
+ /**
+ * The parent frame, if any. Detached and main frames return `null`.
+ */
+ abstract parentFrame(): Frame | null;
+
+ /**
+ * An array of child frames.
+ */
+ abstract childFrames(): Frame[];
+
+ /**
+ * @returns `true` if the frame has detached. `false` otherwise.
+ */
+ abstract get detached(): boolean;
+
+ /**
+ * Is`true` if the frame has been detached. Otherwise, `false`.
+ *
+ * @deprecated Use the `detached` getter.
+ */
+ isDetached(): boolean {
+ return this.detached;
+ }
+
+ /**
+ * @internal
+ */
+ get disposed(): boolean {
+ return this.detached;
+ }
+
+ /**
+ * Adds a `<script>` tag into the page with the desired url or content.
+ *
+ * @param options - Options for the script.
+ * @returns An {@link ElementHandle | element handle} to the injected
+ * `<script>` element.
+ */
+ @throwIfDetached
+ async addScriptTag(
+ options: FrameAddScriptTagOptions
+ ): Promise<ElementHandle<HTMLScriptElement>> {
+ let {content = '', type} = options;
+ const {path} = options;
+ if (+!!options.url + +!!path + +!!content !== 1) {
+ throw new Error(
+ 'Exactly one of `url`, `path`, or `content` must be specified.'
+ );
+ }
+
+ if (path) {
+ const fs = await importFSPromises();
+ content = await fs.readFile(path, 'utf8');
+ content += `//# sourceURL=${path.replace(/\n/g, '')}`;
+ }
+
+ type = type ?? 'text/javascript';
+
+ return await this.mainRealm().transferHandle(
+ await this.isolatedRealm().evaluateHandle(
+ async ({Deferred}, {url, id, type, content}) => {
+ const deferred = Deferred.create<void>();
+ const script = document.createElement('script');
+ script.type = type;
+ script.text = content;
+ if (url) {
+ script.src = url;
+ script.addEventListener(
+ 'load',
+ () => {
+ return deferred.resolve();
+ },
+ {once: true}
+ );
+ script.addEventListener(
+ 'error',
+ event => {
+ deferred.reject(
+ new Error(event.message ?? 'Could not load script')
+ );
+ },
+ {once: true}
+ );
+ } else {
+ deferred.resolve();
+ }
+ if (id) {
+ script.id = id;
+ }
+ document.head.appendChild(script);
+ await deferred.valueOrThrow();
+ return script;
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ {...options, type, content}
+ )
+ );
+ }
+
+ /**
+ * Adds a `HTMLStyleElement` into the frame with the desired URL
+ *
+ * @returns An {@link ElementHandle | element handle} to the loaded `<style>`
+ * element.
+ */
+ async addStyleTag(
+ options: Omit<FrameAddStyleTagOptions, 'url'>
+ ): Promise<ElementHandle<HTMLStyleElement>>;
+
+ /**
+ * Adds a `HTMLLinkElement` into the frame with the desired URL
+ *
+ * @returns An {@link ElementHandle | element handle} to the loaded `<link>`
+ * element.
+ */
+ async addStyleTag(
+ options: FrameAddStyleTagOptions
+ ): Promise<ElementHandle<HTMLLinkElement>>;
+
+ /**
+ * @internal
+ */
+ @throwIfDetached
+ async addStyleTag(
+ options: FrameAddStyleTagOptions
+ ): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> {
+ let {content = ''} = options;
+ const {path} = options;
+ if (+!!options.url + +!!path + +!!content !== 1) {
+ throw new Error(
+ 'Exactly one of `url`, `path`, or `content` must be specified.'
+ );
+ }
+
+ if (path) {
+ const fs = await importFSPromises();
+
+ content = await fs.readFile(path, 'utf8');
+ content += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
+ options.content = content;
+ }
+
+ return await this.mainRealm().transferHandle(
+ await this.isolatedRealm().evaluateHandle(
+ async ({Deferred}, {url, content}) => {
+ const deferred = Deferred.create<void>();
+ let element: HTMLStyleElement | HTMLLinkElement;
+ if (!url) {
+ element = document.createElement('style');
+ element.appendChild(document.createTextNode(content!));
+ } else {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = url;
+ element = link;
+ }
+ element.addEventListener(
+ 'load',
+ () => {
+ deferred.resolve();
+ },
+ {once: true}
+ );
+ element.addEventListener(
+ 'error',
+ event => {
+ deferred.reject(
+ new Error(
+ (event as ErrorEvent).message ?? 'Could not load style'
+ )
+ );
+ },
+ {once: true}
+ );
+ document.head.appendChild(element);
+ await deferred.valueOrThrow();
+ return element;
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ options
+ )
+ );
+ }
+
+ /**
+ * Clicks the first element found that matches `selector`.
+ *
+ * @remarks
+ * If `click()` triggers a navigation event and there's a separate
+ * `page.waitForNavigation()` promise to be resolved, you may end up with a
+ * race condition that yields unexpected results. The correct pattern for
+ * click and wait for navigation is the following:
+ *
+ * ```ts
+ * const [response] = await Promise.all([
+ * page.waitForNavigation(waitOptions),
+ * frame.click(selector, clickOptions),
+ * ]);
+ * ```
+ *
+ * @param selector - The selector to query for.
+ */
+ @throwIfDetached
+ async click(
+ selector: string,
+ options: Readonly<ClickOptions> = {}
+ ): Promise<void> {
+ using handle = await this.$(selector);
+ assert(handle, `No element found for selector: ${selector}`);
+ await handle.click(options);
+ await handle.dispose();
+ }
+
+ /**
+ * Focuses the first element that matches the `selector`.
+ *
+ * @param selector - The selector to query for.
+ * @throws Throws if there's no element matching `selector`.
+ */
+ @throwIfDetached
+ async focus(selector: string): Promise<void> {
+ using handle = await this.$(selector);
+ assert(handle, `No element found for selector: ${selector}`);
+ await handle.focus();
+ }
+
+ /**
+ * Hovers the pointer over the center of the first element that matches the
+ * `selector`.
+ *
+ * @param selector - The selector to query for.
+ * @throws Throws if there's no element matching `selector`.
+ */
+ @throwIfDetached
+ async hover(selector: string): Promise<void> {
+ using handle = await this.$(selector);
+ assert(handle, `No element found for selector: ${selector}`);
+ await handle.hover();
+ }
+
+ /**
+ * Selects a set of value on the first `<select>` element that matches the
+ * `selector`.
+ *
+ * @example
+ *
+ * ```ts
+ * frame.select('select#colors', 'blue'); // single selection
+ * frame.select('select#colors', 'red', 'green', 'blue'); // multiple selections
+ * ```
+ *
+ * @param selector - The selector to query for.
+ * @param values - The array of values to select. If the `<select>` has the
+ * `multiple` attribute, all values are considered, otherwise only the first
+ * one is taken into account.
+ * @returns the list of values that were successfully selected.
+ * @throws Throws if there's no `<select>` matching `selector`.
+ */
+ @throwIfDetached
+ async select(selector: string, ...values: string[]): Promise<string[]> {
+ using handle = await this.$(selector);
+ assert(handle, `No element found for selector: ${selector}`);
+ return await handle.select(...values);
+ }
+
+ /**
+ * Taps the first element that matches the `selector`.
+ *
+ * @param selector - The selector to query for.
+ * @throws Throws if there's no element matching `selector`.
+ */
+ @throwIfDetached
+ async tap(selector: string): Promise<void> {
+ using handle = await this.$(selector);
+ assert(handle, `No element found for selector: ${selector}`);
+ await handle.tap();
+ }
+
+ /**
+ * Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character
+ * in the text.
+ *
+ * @remarks
+ * To press a special key, like `Control` or `ArrowDown`, use
+ * {@link Keyboard.press}.
+ *
+ * @example
+ *
+ * ```ts
+ * await frame.type('#mytextarea', 'Hello'); // Types instantly
+ * await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a user
+ * ```
+ *
+ * @param selector - the selector for the element to type into. If there are
+ * multiple the first will be used.
+ * @param text - text to type into the element
+ * @param options - takes one option, `delay`, which sets the time to wait
+ * between key presses in milliseconds. Defaults to `0`.
+ */
+ @throwIfDetached
+ async type(
+ selector: string,
+ text: string,
+ options?: Readonly<KeyboardTypeOptions>
+ ): Promise<void> {
+ using handle = await this.$(selector);
+ assert(handle, `No element found for selector: ${selector}`);
+ await handle.type(text, options);
+ }
+
+ /**
+ * @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`.
+ *
+ * Causes your script to wait for the given number of milliseconds.
+ *
+ * @remarks
+ * It's generally recommended to not wait for a number of seconds, but instead
+ * use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or
+ * {@link Frame.waitForFunction} to wait for exactly the conditions you want.
+ *
+ * @example
+ *
+ * Wait for 1 second:
+ *
+ * ```ts
+ * await frame.waitForTimeout(1000);
+ * ```
+ *
+ * @param milliseconds - the number of milliseconds to wait.
+ */
+ async waitForTimeout(milliseconds: number): Promise<void> {
+ return await new Promise(resolve => {
+ setTimeout(resolve, milliseconds);
+ });
+ }
+
+ /**
+ * The frame's title.
+ */
+ @throwIfDetached
+ async title(): Promise<string> {
+ return await this.isolatedRealm().evaluate(() => {
+ return document.title;
+ });
+ }
+
+ /**
+ * This method is typically coupled with an action that triggers a device
+ * request from an api such as WebBluetooth.
+ *
+ * :::caution
+ *
+ * This must be called before the device request is made. It will not return a
+ * currently active device prompt.
+ *
+ * :::
+ *
+ * @example
+ *
+ * ```ts
+ * const [devicePrompt] = Promise.all([
+ * frame.waitForDevicePrompt(),
+ * frame.click('#connect-bluetooth'),
+ * ]);
+ * await devicePrompt.select(
+ * await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+ * );
+ * ```
+ *
+ * @internal
+ */
+ abstract waitForDevicePrompt(
+ options?: WaitTimeoutOptions
+ ): Promise<DeviceRequestPrompt>;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts
new file mode 100644
index 0000000000..3c952371ee
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts
@@ -0,0 +1,521 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from './CDPSession.js';
+import type {Frame} from './Frame.js';
+import type {HTTPResponse} from './HTTPResponse.js';
+
+/**
+ * @public
+ */
+export interface ContinueRequestOverrides {
+ /**
+ * If set, the request URL will change. This is not a redirect.
+ */
+ url?: string;
+ method?: string;
+ postData?: string;
+ headers?: Record<string, string>;
+}
+
+/**
+ * @public
+ */
+export interface InterceptResolutionState {
+ action: InterceptResolutionAction;
+ priority?: number;
+}
+
+/**
+ * Required response data to fulfill a request with.
+ *
+ * @public
+ */
+export interface ResponseForRequest {
+ status: number;
+ /**
+ * Optional response headers. All values are converted to strings.
+ */
+ headers: Record<string, unknown>;
+ contentType: string;
+ body: string | Buffer;
+}
+
+/**
+ * Resource types for HTTPRequests as perceived by the rendering engine.
+ *
+ * @public
+ */
+export type ResourceType = Lowercase<Protocol.Network.ResourceType>;
+
+/**
+ * The default cooperative request interception resolution priority
+ *
+ * @public
+ */
+export const DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0;
+
+/**
+ * Represents an HTTP request sent by a page.
+ * @remarks
+ *
+ * Whenever the page sends a request, such as for a network resource, the
+ * following events are emitted by Puppeteer's `page`:
+ *
+ * - `request`: emitted when the request is issued by the page.
+ * - `requestfinished` - emitted when the response body is downloaded and the
+ * request is complete.
+ *
+ * If request fails at some point, then instead of `requestfinished` event the
+ * `requestfailed` event is emitted.
+ *
+ * All of these events provide an instance of `HTTPRequest` representing the
+ * request that occurred:
+ *
+ * ```
+ * page.on('request', request => ...)
+ * ```
+ *
+ * NOTE: HTTP Error responses, such as 404 or 503, are still successful
+ * responses from HTTP standpoint, so request will complete with
+ * `requestfinished` event.
+ *
+ * If request gets a 'redirect' response, the request is successfully finished
+ * with the `requestfinished` event, and a new request is issued to a
+ * redirected url.
+ *
+ * @public
+ */
+export abstract class HTTPRequest {
+ /**
+ * @internal
+ */
+ _requestId = '';
+ /**
+ * @internal
+ */
+ _interceptionId: string | undefined;
+ /**
+ * @internal
+ */
+ _failureText: string | null = null;
+ /**
+ * @internal
+ */
+ _response: HTTPResponse | null = null;
+ /**
+ * @internal
+ */
+ _fromMemoryCache = false;
+ /**
+ * @internal
+ */
+ _redirectChain: HTTPRequest[] = [];
+
+ /**
+ * Warning! Using this client can break Puppeteer. Use with caution.
+ *
+ * @experimental
+ */
+ abstract get client(): CDPSession;
+
+ /**
+ * @internal
+ */
+ constructor() {}
+
+ /**
+ * The URL of the request
+ */
+ abstract url(): string;
+
+ /**
+ * The `ContinueRequestOverrides` that will be used
+ * if the interception is allowed to continue (ie, `abort()` and
+ * `respond()` aren't called).
+ */
+ abstract continueRequestOverrides(): ContinueRequestOverrides;
+
+ /**
+ * The `ResponseForRequest` that gets used if the
+ * interception is allowed to respond (ie, `abort()` is not called).
+ */
+ abstract responseForRequest(): Partial<ResponseForRequest> | null;
+
+ /**
+ * The most recent reason for aborting the request
+ */
+ abstract abortErrorReason(): Protocol.Network.ErrorReason | null;
+
+ /**
+ * An InterceptResolutionState object describing the current resolution
+ * action and priority.
+ *
+ * InterceptResolutionState contains:
+ * action: InterceptResolutionAction
+ * priority?: number
+ *
+ * InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
+ * `disabled`, `none`, or `already-handled`.
+ */
+ abstract interceptResolutionState(): InterceptResolutionState;
+
+ /**
+ * Is `true` if the intercept resolution has already been handled,
+ * `false` otherwise.
+ */
+ abstract isInterceptResolutionHandled(): boolean;
+
+ /**
+ * Adds an async request handler to the processing queue.
+ * Deferred handlers are not guaranteed to execute in any particular order,
+ * but they are guaranteed to resolve before the request interception
+ * is finalized.
+ */
+ abstract enqueueInterceptAction(
+ pendingHandler: () => void | PromiseLike<unknown>
+ ): void;
+
+ /**
+ * Awaits pending interception handlers and then decides how to fulfill
+ * the request interception.
+ */
+ abstract finalizeInterceptions(): Promise<void>;
+
+ /**
+ * Contains the request's resource type as it was perceived by the rendering
+ * engine.
+ */
+ abstract resourceType(): ResourceType;
+
+ /**
+ * The method used (`GET`, `POST`, etc.)
+ */
+ abstract method(): string;
+
+ /**
+ * The request's post body, if any.
+ */
+ abstract postData(): string | undefined;
+
+ /**
+ * True when the request has POST data. Note that {@link HTTPRequest.postData}
+ * might still be undefined when this flag is true when the data is too long
+ * or not readily available in the decoded form. In that case, use
+ * {@link HTTPRequest.fetchPostData}.
+ */
+ abstract hasPostData(): boolean;
+
+ /**
+ * Fetches the POST data for the request from the browser.
+ */
+ abstract fetchPostData(): Promise<string | undefined>;
+
+ /**
+ * An object with HTTP headers associated with the request. All
+ * header names are lower-case.
+ */
+ abstract headers(): Record<string, string>;
+
+ /**
+ * A matching `HTTPResponse` object, or null if the response has not
+ * been received yet.
+ */
+ abstract response(): HTTPResponse | null;
+
+ /**
+ * The frame that initiated the request, or null if navigating to
+ * error pages.
+ */
+ abstract frame(): Frame | null;
+
+ /**
+ * True if the request is the driver of the current frame's navigation.
+ */
+ abstract isNavigationRequest(): boolean;
+
+ /**
+ * The initiator of the request.
+ */
+ abstract initiator(): Protocol.Network.Initiator | undefined;
+
+ /**
+ * A `redirectChain` is a chain of requests initiated to fetch a resource.
+ * @remarks
+ *
+ * `redirectChain` is shared between all the requests of the same chain.
+ *
+ * For example, if the website `http://example.com` has a single redirect to
+ * `https://example.com`, then the chain will contain one request:
+ *
+ * ```ts
+ * const response = await page.goto('http://example.com');
+ * const chain = response.request().redirectChain();
+ * console.log(chain.length); // 1
+ * console.log(chain[0].url()); // 'http://example.com'
+ * ```
+ *
+ * If the website `https://google.com` has no redirects, then the chain will be empty:
+ *
+ * ```ts
+ * const response = await page.goto('https://google.com');
+ * const chain = response.request().redirectChain();
+ * console.log(chain.length); // 0
+ * ```
+ *
+ * @returns the chain of requests - if a server responds with at least a
+ * single redirect, this chain will contain all requests that were redirected.
+ */
+ abstract redirectChain(): HTTPRequest[];
+
+ /**
+ * Access information about the request's failure.
+ *
+ * @remarks
+ *
+ * @example
+ *
+ * Example of logging all failed requests:
+ *
+ * ```ts
+ * page.on('requestfailed', request => {
+ * console.log(request.url() + ' ' + request.failure().errorText);
+ * });
+ * ```
+ *
+ * @returns `null` unless the request failed. If the request fails this can
+ * return an object with `errorText` containing a human-readable error
+ * message, e.g. `net::ERR_FAILED`. It is not guaranteed that there will be
+ * failure text if the request fails.
+ */
+ abstract failure(): {errorText: string} | null;
+
+ /**
+ * Continues request with optional request overrides.
+ *
+ * @example
+ *
+ * ```ts
+ * await page.setRequestInterception(true);
+ * page.on('request', request => {
+ * // Override headers
+ * const headers = Object.assign({}, request.headers(), {
+ * foo: 'bar', // set "foo" header
+ * origin: undefined, // remove "origin" header
+ * });
+ * request.continue({headers});
+ * });
+ * ```
+ *
+ * @param overrides - optional overrides to apply to the request.
+ * @param priority - If provided, intercept is resolved using cooperative
+ * handling rules. Otherwise, intercept is resolved immediately.
+ *
+ * @remarks
+ *
+ * To use this, request interception should be enabled with
+ * {@link Page.setRequestInterception}.
+ *
+ * Exception is immediately thrown if the request interception is not enabled.
+ */
+ abstract continue(
+ overrides?: ContinueRequestOverrides,
+ priority?: number
+ ): Promise<void>;
+
+ /**
+ * Fulfills a request with the given response.
+ *
+ * @example
+ * An example of fulfilling all requests with 404 responses:
+ *
+ * ```ts
+ * await page.setRequestInterception(true);
+ * page.on('request', request => {
+ * request.respond({
+ * status: 404,
+ * contentType: 'text/plain',
+ * body: 'Not Found!',
+ * });
+ * });
+ * ```
+ *
+ * NOTE: Mocking responses for dataURL requests is not supported.
+ * Calling `request.respond` for a dataURL request is a noop.
+ *
+ * @param response - the response to fulfill the request with.
+ * @param priority - If provided, intercept is resolved using
+ * cooperative handling rules. Otherwise, intercept is resolved
+ * immediately.
+ *
+ * @remarks
+ *
+ * To use this, request
+ * interception should be enabled with {@link Page.setRequestInterception}.
+ *
+ * Exception is immediately thrown if the request interception is not enabled.
+ */
+ abstract respond(
+ response: Partial<ResponseForRequest>,
+ priority?: number
+ ): Promise<void>;
+
+ /**
+ * Aborts a request.
+ *
+ * @param errorCode - optional error code to provide.
+ * @param priority - If provided, intercept is resolved using
+ * cooperative handling rules. Otherwise, intercept is resolved
+ * immediately.
+ *
+ * @remarks
+ *
+ * To use this, request interception should be enabled with
+ * {@link Page.setRequestInterception}. If it is not enabled, this method will
+ * throw an exception immediately.
+ */
+ abstract abort(errorCode?: ErrorCode, priority?: number): Promise<void>;
+}
+
+/**
+ * @public
+ */
+export enum InterceptResolutionAction {
+ Abort = 'abort',
+ Respond = 'respond',
+ Continue = 'continue',
+ Disabled = 'disabled',
+ None = 'none',
+ AlreadyHandled = 'already-handled',
+}
+
+/**
+ * @public
+ *
+ * @deprecated please use {@link InterceptResolutionAction} instead.
+ */
+export type InterceptResolutionStrategy = InterceptResolutionAction;
+
+/**
+ * @public
+ */
+export type ErrorCode =
+ | 'aborted'
+ | 'accessdenied'
+ | 'addressunreachable'
+ | 'blockedbyclient'
+ | 'blockedbyresponse'
+ | 'connectionaborted'
+ | 'connectionclosed'
+ | 'connectionfailed'
+ | 'connectionrefused'
+ | 'connectionreset'
+ | 'internetdisconnected'
+ | 'namenotresolved'
+ | 'timedout'
+ | 'failed';
+
+/**
+ * @public
+ */
+export type ActionResult = 'continue' | 'abort' | 'respond';
+
+/**
+ * @internal
+ */
+export function headersArray(
+ headers: Record<string, string | string[]>
+): Array<{name: string; value: string}> {
+ const result = [];
+ for (const name in headers) {
+ const value = headers[name];
+
+ if (!Object.is(value, undefined)) {
+ const values = Array.isArray(value) ? value : [value];
+
+ result.push(
+ ...values.map(value => {
+ return {name, value: value + ''};
+ })
+ );
+ }
+ }
+ return result;
+}
+
+/**
+ * @internal
+ *
+ * @remarks
+ * List taken from {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml}
+ * with extra 306 and 418 codes.
+ */
+export const STATUS_TEXTS: Record<string, string> = {
+ '100': 'Continue',
+ '101': 'Switching Protocols',
+ '102': 'Processing',
+ '103': 'Early Hints',
+ '200': 'OK',
+ '201': 'Created',
+ '202': 'Accepted',
+ '203': 'Non-Authoritative Information',
+ '204': 'No Content',
+ '205': 'Reset Content',
+ '206': 'Partial Content',
+ '207': 'Multi-Status',
+ '208': 'Already Reported',
+ '226': 'IM Used',
+ '300': 'Multiple Choices',
+ '301': 'Moved Permanently',
+ '302': 'Found',
+ '303': 'See Other',
+ '304': 'Not Modified',
+ '305': 'Use Proxy',
+ '306': 'Switch Proxy',
+ '307': 'Temporary Redirect',
+ '308': 'Permanent Redirect',
+ '400': 'Bad Request',
+ '401': 'Unauthorized',
+ '402': 'Payment Required',
+ '403': 'Forbidden',
+ '404': 'Not Found',
+ '405': 'Method Not Allowed',
+ '406': 'Not Acceptable',
+ '407': 'Proxy Authentication Required',
+ '408': 'Request Timeout',
+ '409': 'Conflict',
+ '410': 'Gone',
+ '411': 'Length Required',
+ '412': 'Precondition Failed',
+ '413': 'Payload Too Large',
+ '414': 'URI Too Long',
+ '415': 'Unsupported Media Type',
+ '416': 'Range Not Satisfiable',
+ '417': 'Expectation Failed',
+ '418': "I'm a teapot",
+ '421': 'Misdirected Request',
+ '422': 'Unprocessable Entity',
+ '423': 'Locked',
+ '424': 'Failed Dependency',
+ '425': 'Too Early',
+ '426': 'Upgrade Required',
+ '428': 'Precondition Required',
+ '429': 'Too Many Requests',
+ '431': 'Request Header Fields Too Large',
+ '451': 'Unavailable For Legal Reasons',
+ '500': 'Internal Server Error',
+ '501': 'Not Implemented',
+ '502': 'Bad Gateway',
+ '503': 'Service Unavailable',
+ '504': 'Gateway Timeout',
+ '505': 'HTTP Version Not Supported',
+ '506': 'Variant Also Negotiates',
+ '507': 'Insufficient Storage',
+ '508': 'Loop Detected',
+ '510': 'Not Extended',
+ '511': 'Network Authentication Required',
+} as const;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPResponse.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPResponse.ts
new file mode 100644
index 0000000000..906479eb43
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPResponse.ts
@@ -0,0 +1,129 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type Protocol from 'devtools-protocol';
+
+import type {SecurityDetails} from '../common/SecurityDetails.js';
+
+import type {Frame} from './Frame.js';
+import type {HTTPRequest} from './HTTPRequest.js';
+
+/**
+ * @public
+ */
+export interface RemoteAddress {
+ ip?: string;
+ port?: number;
+}
+
+/**
+ * The HTTPResponse class represents responses which are received by the
+ * {@link Page} class.
+ *
+ * @public
+ */
+export abstract class HTTPResponse {
+ /**
+ * @internal
+ */
+ constructor() {}
+
+ /**
+ * The IP address and port number used to connect to the remote
+ * server.
+ */
+ abstract remoteAddress(): RemoteAddress;
+
+ /**
+ * The URL of the response.
+ */
+ abstract url(): string;
+
+ /**
+ * True if the response was successful (status in the range 200-299).
+ */
+ ok(): boolean {
+ // TODO: document === 0 case?
+ const status = this.status();
+ return status === 0 || (status >= 200 && status <= 299);
+ }
+
+ /**
+ * The status code of the response (e.g., 200 for a success).
+ */
+ abstract status(): number;
+
+ /**
+ * The status text of the response (e.g. usually an "OK" for a
+ * success).
+ */
+ abstract statusText(): string;
+
+ /**
+ * An object with HTTP headers associated with the response. All
+ * header names are lower-case.
+ */
+ abstract headers(): Record<string, string>;
+
+ /**
+ * {@link SecurityDetails} if the response was received over the
+ * secure connection, or `null` otherwise.
+ */
+ abstract securityDetails(): SecurityDetails | null;
+
+ /**
+ * Timing information related to the response.
+ */
+ abstract timing(): Protocol.Network.ResourceTiming | null;
+
+ /**
+ * Promise which resolves to a buffer with response body.
+ */
+ abstract buffer(): Promise<Buffer>;
+
+ /**
+ * Promise which resolves to a text representation of response body.
+ */
+ async text(): Promise<string> {
+ const content = await this.buffer();
+ return content.toString('utf8');
+ }
+
+ /**
+ * Promise which resolves to a JSON representation of response body.
+ *
+ * @remarks
+ *
+ * This method will throw if the response body is not parsable via
+ * `JSON.parse`.
+ */
+ async json(): Promise<any> {
+ const content = await this.text();
+ return JSON.parse(content);
+ }
+
+ /**
+ * A matching {@link HTTPRequest} object.
+ */
+ abstract request(): HTTPRequest;
+
+ /**
+ * True if the response was served from either the browser's disk
+ * cache or memory cache.
+ */
+ abstract fromCache(): boolean;
+
+ /**
+ * True if the response was served by a service worker.
+ */
+ abstract fromServiceWorker(): boolean;
+
+ /**
+ * A {@link Frame} that initiated this response, or `null` if
+ * navigating to error pages.
+ */
+ abstract frame(): Frame | null;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Input.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Input.ts
new file mode 100644
index 0000000000..6b41ca8fe1
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Input.ts
@@ -0,0 +1,517 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {KeyInput} from '../common/USKeyboardLayout.js';
+
+import type {Point} from './ElementHandle.js';
+
+/**
+ * @public
+ */
+export interface KeyDownOptions {
+ /**
+ * @deprecated Do not use. This is automatically handled.
+ */
+ text?: string;
+ /**
+ * @deprecated Do not use. This is automatically handled.
+ */
+ commands?: string[];
+}
+
+/**
+ * @public
+ */
+export interface KeyboardTypeOptions {
+ delay?: number;
+}
+
+/**
+ * @public
+ */
+export type KeyPressOptions = KeyDownOptions & KeyboardTypeOptions;
+
+/**
+ * Keyboard provides an api for managing a virtual keyboard.
+ * The high level api is {@link Keyboard."type"},
+ * which takes raw characters and generates proper keydown, keypress/input,
+ * and keyup events on your page.
+ *
+ * @remarks
+ * For finer control, you can use {@link Keyboard.down},
+ * {@link Keyboard.up}, and {@link Keyboard.sendCharacter}
+ * to manually fire events as if they were generated from a real keyboard.
+ *
+ * On macOS, keyboard shortcuts like `⌘ A` -\> Select All do not work.
+ * See {@link https://github.com/puppeteer/puppeteer/issues/1313 | #1313}.
+ *
+ * @example
+ * An example of holding down `Shift` in order to select and delete some text:
+ *
+ * ```ts
+ * await page.keyboard.type('Hello World!');
+ * await page.keyboard.press('ArrowLeft');
+ *
+ * await page.keyboard.down('Shift');
+ * for (let i = 0; i < ' World'.length; i++)
+ * await page.keyboard.press('ArrowLeft');
+ * await page.keyboard.up('Shift');
+ *
+ * await page.keyboard.press('Backspace');
+ * // Result text will end up saying 'Hello!'
+ * ```
+ *
+ * @example
+ * An example of pressing `A`
+ *
+ * ```ts
+ * await page.keyboard.down('Shift');
+ * await page.keyboard.press('KeyA');
+ * await page.keyboard.up('Shift');
+ * ```
+ *
+ * @public
+ */
+export abstract class Keyboard {
+ /**
+ * @internal
+ */
+ constructor() {}
+
+ /**
+ * Dispatches a `keydown` event.
+ *
+ * @remarks
+ * If `key` is a single character and no modifier keys besides `Shift`
+ * are being held down, a `keypress`/`input` event will also generated.
+ * The `text` option can be specified to force an input event to be generated.
+ * If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`,
+ * subsequent key presses will be sent with that modifier active.
+ * To release the modifier key, use {@link Keyboard.up}.
+ *
+ * After the key is pressed once, subsequent calls to
+ * {@link Keyboard.down} will have
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat | repeat}
+ * set to true. To release the key, use {@link Keyboard.up}.
+ *
+ * Modifier keys DO influence {@link Keyboard.down}.
+ * Holding down `Shift` will type the text in upper case.
+ *
+ * @param key - Name of key to press, such as `ArrowLeft`.
+ * See {@link KeyInput} for a list of all key names.
+ *
+ * @param options - An object of options. Accepts text which, if specified,
+ * generates an input event with this text. Accepts commands which, if specified,
+ * is the commands of keyboard shortcuts,
+ * see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
+ */
+ abstract down(
+ key: KeyInput,
+ options?: Readonly<KeyDownOptions>
+ ): Promise<void>;
+
+ /**
+ * Dispatches a `keyup` event.
+ *
+ * @param key - Name of key to release, such as `ArrowLeft`.
+ * See {@link KeyInput | KeyInput}
+ * for a list of all key names.
+ */
+ abstract up(key: KeyInput): Promise<void>;
+
+ /**
+ * Dispatches a `keypress` and `input` event.
+ * This does not send a `keydown` or `keyup` event.
+ *
+ * @remarks
+ * Modifier keys DO NOT effect {@link Keyboard.sendCharacter | Keyboard.sendCharacter}.
+ * Holding down `Shift` will not type the text in upper case.
+ *
+ * @example
+ *
+ * ```ts
+ * page.keyboard.sendCharacter('嗨');
+ * ```
+ *
+ * @param char - Character to send into the page.
+ */
+ abstract sendCharacter(char: string): Promise<void>;
+
+ /**
+ * Sends a `keydown`, `keypress`/`input`,
+ * and `keyup` event for each character in the text.
+ *
+ * @remarks
+ * To press a special key, like `Control` or `ArrowDown`,
+ * use {@link Keyboard.press}.
+ *
+ * Modifier keys DO NOT effect `keyboard.type`.
+ * Holding down `Shift` will not type the text in upper case.
+ *
+ * @example
+ *
+ * ```ts
+ * await page.keyboard.type('Hello'); // Types instantly
+ * await page.keyboard.type('World', {delay: 100}); // Types slower, like a user
+ * ```
+ *
+ * @param text - A text to type into a focused element.
+ * @param options - An object of options. Accepts delay which,
+ * if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
+ * Defaults to 0.
+ */
+ abstract type(
+ text: string,
+ options?: Readonly<KeyboardTypeOptions>
+ ): Promise<void>;
+
+ /**
+ * Shortcut for {@link Keyboard.down}
+ * and {@link Keyboard.up}.
+ *
+ * @remarks
+ * If `key` is a single character and no modifier keys besides `Shift`
+ * are being held down, a `keypress`/`input` event will also generated.
+ * The `text` option can be specified to force an input event to be generated.
+ *
+ * Modifier keys DO effect {@link Keyboard.press}.
+ * Holding down `Shift` will type the text in upper case.
+ *
+ * @param key - Name of key to press, such as `ArrowLeft`.
+ * See {@link KeyInput} for a list of all key names.
+ *
+ * @param options - An object of options. Accepts text which, if specified,
+ * generates an input event with this text. Accepts delay which,
+ * if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
+ * Defaults to 0. Accepts commands which, if specified,
+ * is the commands of keyboard shortcuts,
+ * see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
+ */
+ abstract press(
+ key: KeyInput,
+ options?: Readonly<KeyPressOptions>
+ ): Promise<void>;
+}
+
+/**
+ * @public
+ */
+export interface MouseOptions {
+ /**
+ * Determines which button will be pressed.
+ *
+ * @defaultValue `'left'`
+ */
+ button?: MouseButton;
+ /**
+ * Determines the click count for the mouse event. This does not perform
+ * multiple clicks.
+ *
+ * @deprecated Use {@link MouseClickOptions.count}.
+ * @defaultValue `1`
+ */
+ clickCount?: number;
+}
+
+/**
+ * @public
+ */
+export interface MouseClickOptions extends MouseOptions {
+ /**
+ * Time (in ms) to delay the mouse release after the mouse press.
+ */
+ delay?: number;
+ /**
+ * Number of clicks to perform.
+ *
+ * @defaultValue `1`
+ */
+ count?: number;
+}
+
+/**
+ * @public
+ */
+export interface MouseWheelOptions {
+ deltaX?: number;
+ deltaY?: number;
+}
+
+/**
+ * @public
+ */
+export interface MouseMoveOptions {
+ /**
+ * Determines the number of movements to make from the current mouse position
+ * to the new one.
+ *
+ * @defaultValue `1`
+ */
+ steps?: number;
+}
+
+/**
+ * Enum of valid mouse buttons.
+ *
+ * @public
+ */
+export const MouseButton = Object.freeze({
+ Left: 'left',
+ Right: 'right',
+ Middle: 'middle',
+ Back: 'back',
+ Forward: 'forward',
+}) satisfies Record<string, Protocol.Input.MouseButton>;
+
+/**
+ * @public
+ */
+export type MouseButton = (typeof MouseButton)[keyof typeof MouseButton];
+
+/**
+ * The Mouse class operates in main-frame CSS pixels
+ * relative to the top-left corner of the viewport.
+ * @remarks
+ * Every `page` object has its own Mouse, accessible with [`page.mouse`](#pagemouse).
+ *
+ * @example
+ *
+ * ```ts
+ * // Using ‘page.mouse’ to trace a 100x100 square.
+ * await page.mouse.move(0, 0);
+ * await page.mouse.down();
+ * await page.mouse.move(0, 100);
+ * await page.mouse.move(100, 100);
+ * await page.mouse.move(100, 0);
+ * await page.mouse.move(0, 0);
+ * await page.mouse.up();
+ * ```
+ *
+ * **Note**: The mouse events trigger synthetic `MouseEvent`s.
+ * This means that it does not fully replicate the functionality of what a normal user
+ * would be able to do with their mouse.
+ *
+ * For example, dragging and selecting text is not possible using `page.mouse`.
+ * Instead, you can use the {@link https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/getSelection | `DocumentOrShadowRoot.getSelection()`} functionality implemented in the platform.
+ *
+ * @example
+ * For example, if you want to select all content between nodes:
+ *
+ * ```ts
+ * await page.evaluate(
+ * (from, to) => {
+ * const selection = from.getRootNode().getSelection();
+ * const range = document.createRange();
+ * range.setStartBefore(from);
+ * range.setEndAfter(to);
+ * selection.removeAllRanges();
+ * selection.addRange(range);
+ * },
+ * fromJSHandle,
+ * toJSHandle
+ * );
+ * ```
+ *
+ * If you then would want to copy-paste your selection, you can use the clipboard api:
+ *
+ * ```ts
+ * // The clipboard api does not allow you to copy, unless the tab is focused.
+ * await page.bringToFront();
+ * await page.evaluate(() => {
+ * // Copy the selected content to the clipboard
+ * document.execCommand('copy');
+ * // Obtain the content of the clipboard as a string
+ * return navigator.clipboard.readText();
+ * });
+ * ```
+ *
+ * **Note**: If you want access to the clipboard API,
+ * you have to give it permission to do so:
+ *
+ * ```ts
+ * await browser
+ * .defaultBrowserContext()
+ * .overridePermissions('<your origin>', [
+ * 'clipboard-read',
+ * 'clipboard-write',
+ * ]);
+ * ```
+ *
+ * @public
+ */
+export abstract class Mouse {
+ /**
+ * @internal
+ */
+ constructor() {}
+
+ /**
+ * Resets the mouse to the default state: No buttons pressed; position at
+ * (0,0).
+ */
+ abstract reset(): Promise<void>;
+
+ /**
+ * Moves the mouse to the given coordinate.
+ *
+ * @param x - Horizontal position of the mouse.
+ * @param y - Vertical position of the mouse.
+ * @param options - Options to configure behavior.
+ */
+ abstract move(
+ x: number,
+ y: number,
+ options?: Readonly<MouseMoveOptions>
+ ): Promise<void>;
+
+ /**
+ * Presses the mouse.
+ *
+ * @param options - Options to configure behavior.
+ */
+ abstract down(options?: Readonly<MouseOptions>): Promise<void>;
+
+ /**
+ * Releases the mouse.
+ *
+ * @param options - Options to configure behavior.
+ */
+ abstract up(options?: Readonly<MouseOptions>): Promise<void>;
+
+ /**
+ * Shortcut for `mouse.move`, `mouse.down` and `mouse.up`.
+ *
+ * @param x - Horizontal position of the mouse.
+ * @param y - Vertical position of the mouse.
+ * @param options - Options to configure behavior.
+ */
+ abstract click(
+ x: number,
+ y: number,
+ options?: Readonly<MouseClickOptions>
+ ): Promise<void>;
+
+ /**
+ * Dispatches a `mousewheel` event.
+ * @param options - Optional: `MouseWheelOptions`.
+ *
+ * @example
+ * An example of zooming into an element:
+ *
+ * ```ts
+ * await page.goto(
+ * 'https://mdn.mozillademos.org/en-US/docs/Web/API/Element/wheel_event$samples/Scaling_an_element_via_the_wheel?revision=1587366'
+ * );
+ *
+ * const elem = await page.$('div');
+ * const boundingBox = await elem.boundingBox();
+ * await page.mouse.move(
+ * boundingBox.x + boundingBox.width / 2,
+ * boundingBox.y + boundingBox.height / 2
+ * );
+ *
+ * await page.mouse.wheel({deltaY: -100});
+ * ```
+ */
+ abstract wheel(options?: Readonly<MouseWheelOptions>): Promise<void>;
+
+ /**
+ * Dispatches a `drag` event.
+ * @param start - starting point for drag
+ * @param target - point to drag to
+ */
+ abstract drag(start: Point, target: Point): Promise<Protocol.Input.DragData>;
+
+ /**
+ * Dispatches a `dragenter` event.
+ * @param target - point for emitting `dragenter` event
+ * @param data - drag data containing items and operations mask
+ */
+ abstract dragEnter(
+ target: Point,
+ data: Protocol.Input.DragData
+ ): Promise<void>;
+
+ /**
+ * Dispatches a `dragover` event.
+ * @param target - point for emitting `dragover` event
+ * @param data - drag data containing items and operations mask
+ */
+ abstract dragOver(
+ target: Point,
+ data: Protocol.Input.DragData
+ ): Promise<void>;
+
+ /**
+ * Performs a dragenter, dragover, and drop in sequence.
+ * @param target - point to drop on
+ * @param data - drag data containing items and operations mask
+ */
+ abstract drop(target: Point, data: Protocol.Input.DragData): Promise<void>;
+
+ /**
+ * Performs a drag, dragenter, dragover, and drop in sequence.
+ * @param start - point to drag from
+ * @param target - point to drop on
+ * @param options - An object of options. Accepts delay which,
+ * if specified, is the time to wait between `dragover` and `drop` in milliseconds.
+ * Defaults to 0.
+ */
+ abstract dragAndDrop(
+ start: Point,
+ target: Point,
+ options?: {delay?: number}
+ ): Promise<void>;
+}
+
+/**
+ * The Touchscreen class exposes touchscreen events.
+ * @public
+ */
+export abstract class Touchscreen {
+ /**
+ * @internal
+ */
+ constructor() {}
+
+ /**
+ * Dispatches a `touchstart` and `touchend` event.
+ * @param x - Horizontal position of the tap.
+ * @param y - Vertical position of the tap.
+ */
+ async tap(x: number, y: number): Promise<void> {
+ await this.touchStart(x, y);
+ await this.touchEnd();
+ }
+
+ /**
+ * Dispatches a `touchstart` event.
+ * @param x - Horizontal position of the tap.
+ * @param y - Vertical position of the tap.
+ */
+ abstract touchStart(x: number, y: number): Promise<void>;
+
+ /**
+ * Dispatches a `touchMove` event.
+ * @param x - Horizontal position of the move.
+ * @param y - Vertical position of the move.
+ *
+ * @remarks
+ *
+ * Not every `touchMove` call results in a `touchmove` event being emitted,
+ * depending on the browser's optimizations. For example, Chrome
+ * {@link https://developer.chrome.com/blog/a-more-compatible-smoother-touch/#chromes-new-model-the-throttled-async-touchmove-model | throttles}
+ * touch move events.
+ */
+ abstract touchMove(x: number, y: number): Promise<void>;
+
+ /**
+ * Dispatches a `touchend` event.
+ */
+ abstract touchEnd(): Promise<void>;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/JSHandle.ts
new file mode 100644
index 0000000000..52ca7fe8f8
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/JSHandle.ts
@@ -0,0 +1,212 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type Protocol from 'devtools-protocol';
+
+import type {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js';
+import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js';
+import {moveable, throwIfDisposed} from '../util/decorators.js';
+import {disposeSymbol, asyncDisposeSymbol} from '../util/disposable.js';
+
+import type {ElementHandle} from './ElementHandle.js';
+import type {Realm} from './Realm.js';
+
+/**
+ * Represents a reference to a JavaScript object. Instances can be created using
+ * {@link Page.evaluateHandle}.
+ *
+ * Handles prevent the referenced JavaScript object from being garbage-collected
+ * unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles
+ * are auto-disposed when their associated frame is navigated away or the parent
+ * context gets destroyed.
+ *
+ * Handles can be used as arguments for any evaluation function such as
+ * {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}.
+ * They are resolved to their referenced object.
+ *
+ * @example
+ *
+ * ```ts
+ * const windowHandle = await page.evaluateHandle(() => window);
+ * ```
+ *
+ * @public
+ */
+@moveable
+export abstract class JSHandle<T = unknown> {
+ declare move: () => this;
+
+ /**
+ * Used for nominally typing {@link JSHandle}.
+ */
+ declare _?: T;
+
+ /**
+ * @internal
+ */
+ constructor() {}
+
+ /**
+ * @internal
+ */
+ abstract get realm(): Realm;
+
+ /**
+ * @internal
+ */
+ abstract get disposed(): boolean;
+
+ /**
+ * Evaluates the given function with the current handle as its first argument.
+ */
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluate.name,
+ pageFunction
+ );
+ return await this.realm.evaluate(pageFunction, this, ...args);
+ }
+
+ /**
+ * Evaluates the given function with the current handle as its first argument.
+ *
+ */
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluateHandle.name,
+ pageFunction
+ );
+ return await this.realm.evaluateHandle(pageFunction, this, ...args);
+ }
+
+ /**
+ * Fetches a single property from the referenced object.
+ */
+ getProperty<K extends keyof T>(
+ propertyName: HandleOr<K>
+ ): Promise<HandleFor<T[K]>>;
+ getProperty(propertyName: string): Promise<JSHandle<unknown>>;
+
+ /**
+ * @internal
+ */
+ @throwIfDisposed()
+ async getProperty<K extends keyof T>(
+ propertyName: HandleOr<K>
+ ): Promise<HandleFor<T[K]>> {
+ return await this.evaluateHandle((object, propertyName) => {
+ return object[propertyName as K];
+ }, propertyName);
+ }
+
+ /**
+ * Gets a map of handles representing the properties of the current handle.
+ *
+ * @example
+ *
+ * ```ts
+ * const listHandle = await page.evaluateHandle(() => document.body.children);
+ * const properties = await listHandle.getProperties();
+ * const children = [];
+ * for (const property of properties.values()) {
+ * const element = property.asElement();
+ * if (element) {
+ * children.push(element);
+ * }
+ * }
+ * children; // holds elementHandles to all children of document.body
+ * ```
+ */
+ @throwIfDisposed()
+ async getProperties(): Promise<Map<string, JSHandle>> {
+ const propertyNames = await this.evaluate(object => {
+ const enumerableProperties = [];
+ const descriptors = Object.getOwnPropertyDescriptors(object);
+ for (const propertyName in descriptors) {
+ if (descriptors[propertyName]?.enumerable) {
+ enumerableProperties.push(propertyName);
+ }
+ }
+ return enumerableProperties;
+ });
+ const map = new Map<string, JSHandle>();
+ const results = await Promise.all(
+ propertyNames.map(key => {
+ return this.getProperty(key);
+ })
+ );
+ for (const [key, value] of Object.entries(propertyNames)) {
+ using handle = results[key as any];
+ if (handle) {
+ map.set(value, handle.move());
+ }
+ }
+ return map;
+ }
+
+ /**
+ * A vanilla object representing the serializable portions of the
+ * referenced object.
+ * @throws Throws if the object cannot be serialized due to circularity.
+ *
+ * @remarks
+ * If the object has a `toJSON` function, it **will not** be called.
+ */
+ abstract jsonValue(): Promise<T>;
+
+ /**
+ * Either `null` or the handle itself if the handle is an
+ * instance of {@link ElementHandle}.
+ */
+ abstract asElement(): ElementHandle<Node> | null;
+
+ /**
+ * Releases the object referenced by the handle for garbage collection.
+ */
+ abstract dispose(): Promise<void>;
+
+ /**
+ * Returns a string representation of the JSHandle.
+ *
+ * @remarks
+ * Useful during debugging.
+ */
+ abstract toString(): string;
+
+ /**
+ * @internal
+ */
+ abstract get id(): string | undefined;
+
+ /**
+ * Provides access to the
+ * {@link https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject | Protocol.Runtime.RemoteObject}
+ * backing this handle.
+ */
+ abstract remoteObject(): Protocol.Runtime.RemoteObject;
+
+ /** @internal */
+ [disposeSymbol](): void {
+ return void this.dispose().catch(debugError);
+ }
+
+ /** @internal */
+ [asyncDisposeSymbol](): Promise<void> {
+ return this.dispose();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts
new file mode 100644
index 0000000000..deb04628fd
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts
@@ -0,0 +1,3090 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Readable} from 'stream';
+
+import type {Protocol} from 'devtools-protocol';
+
+import {
+ concat,
+ EMPTY,
+ filter,
+ filterAsync,
+ first,
+ firstValueFrom,
+ from,
+ map,
+ merge,
+ mergeMap,
+ of,
+ race,
+ raceWith,
+ startWith,
+ switchMap,
+ takeUntil,
+ timer,
+ type Observable,
+} from '../../third_party/rxjs/rxjs.js';
+import type {HTTPRequest} from '../api/HTTPRequest.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+import type {Accessibility} from '../cdp/Accessibility.js';
+import type {Coverage} from '../cdp/Coverage.js';
+import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
+import type {Credentials, NetworkConditions} from '../cdp/NetworkManager.js';
+import type {Tracing} from '../cdp/Tracing.js';
+import type {ConsoleMessage} from '../common/ConsoleMessage.js';
+import type {Device} from '../common/Device.js';
+import {TargetCloseError} from '../common/Errors.js';
+import {
+ EventEmitter,
+ type EventsWithWildcard,
+ type EventType,
+ type Handler,
+} from '../common/EventEmitter.js';
+import type {FileChooser} from '../common/FileChooser.js';
+import type {PDFOptions} from '../common/PDFOptions.js';
+import {TimeoutSettings} from '../common/TimeoutSettings.js';
+import type {
+ Awaitable,
+ AwaitablePredicate,
+ EvaluateFunc,
+ EvaluateFuncWith,
+ HandleFor,
+ NodeFor,
+} from '../common/types.js';
+import {
+ debugError,
+ fromEmitterEvent,
+ importFSPromises,
+ isString,
+ NETWORK_IDLE_TIME,
+ timeout,
+ withSourcePuppeteerURLIfNone,
+} from '../common/util.js';
+import type {Viewport} from '../common/Viewport.js';
+import type {ScreenRecorder} from '../node/ScreenRecorder.js';
+import {guarded} from '../util/decorators.js';
+import {
+ AsyncDisposableStack,
+ asyncDisposeSymbol,
+ DisposableStack,
+ disposeSymbol,
+} from '../util/disposable.js';
+
+import type {Browser} from './Browser.js';
+import type {BrowserContext} from './BrowserContext.js';
+import type {CDPSession} from './CDPSession.js';
+import type {Dialog} from './Dialog.js';
+import type {
+ BoundingBox,
+ ClickOptions,
+ ElementHandle,
+} from './ElementHandle.js';
+import type {
+ Frame,
+ FrameAddScriptTagOptions,
+ FrameAddStyleTagOptions,
+ FrameWaitForFunctionOptions,
+ GoToOptions,
+ WaitForOptions,
+} from './Frame.js';
+import type {
+ Keyboard,
+ KeyboardTypeOptions,
+ Mouse,
+ Touchscreen,
+} from './Input.js';
+import type {JSHandle} from './JSHandle.js';
+import {
+ FunctionLocator,
+ Locator,
+ NodeLocator,
+ type AwaitedLocator,
+} from './locators/locators.js';
+import type {Target} from './Target.js';
+import type {WebWorker} from './WebWorker.js';
+
+/**
+ * @public
+ */
+export interface Metrics {
+ Timestamp?: number;
+ Documents?: number;
+ Frames?: number;
+ JSEventListeners?: number;
+ Nodes?: number;
+ LayoutCount?: number;
+ RecalcStyleCount?: number;
+ LayoutDuration?: number;
+ RecalcStyleDuration?: number;
+ ScriptDuration?: number;
+ TaskDuration?: number;
+ JSHeapUsedSize?: number;
+ JSHeapTotalSize?: number;
+}
+
+/**
+ * @public
+ */
+export interface WaitForNetworkIdleOptions extends WaitTimeoutOptions {
+ /**
+ * Time (in milliseconds) the network should be idle.
+ *
+ * @defaultValue `500`
+ */
+ idleTime?: number;
+ /**
+ * Maximum number concurrent of network connections to be considered inactive.
+ *
+ * @defaultValue `0`
+ */
+ concurrency?: number;
+}
+
+/**
+ * @public
+ */
+export interface WaitTimeoutOptions {
+ /**
+ * Maximum wait time in milliseconds. Pass 0 to disable the timeout.
+ *
+ * The default value can be changed by using the
+ * {@link Page.setDefaultTimeout} method.
+ *
+ * @defaultValue `30000`
+ */
+ timeout?: number;
+}
+
+/**
+ * @public
+ */
+export interface WaitForSelectorOptions {
+ /**
+ * Wait for the selected element to be present in DOM and to be visible, i.e.
+ * to not have `display: none` or `visibility: hidden` CSS properties.
+ *
+ * @defaultValue `false`
+ */
+ visible?: boolean;
+ /**
+ * Wait for the selected element to not be found in the DOM or to be hidden,
+ * i.e. have `display: none` or `visibility: hidden` CSS properties.
+ *
+ * @defaultValue `false`
+ */
+ hidden?: boolean;
+ /**
+ * Maximum time to wait in milliseconds. Pass `0` to disable timeout.
+ *
+ * The default value can be changed by using {@link Page.setDefaultTimeout}
+ *
+ * @defaultValue `30_000` (30 seconds)
+ */
+ timeout?: number;
+ /**
+ * A signal object that allows you to cancel a waitForSelector call.
+ */
+ signal?: AbortSignal;
+}
+
+/**
+ * @public
+ */
+export interface GeolocationOptions {
+ /**
+ * Latitude between `-90` and `90`.
+ */
+ longitude: number;
+ /**
+ * Longitude between `-180` and `180`.
+ */
+ latitude: number;
+ /**
+ * Optional non-negative accuracy value.
+ */
+ accuracy?: number;
+}
+
+/**
+ * @public
+ */
+export interface MediaFeature {
+ name: string;
+ value: string;
+}
+
+/**
+ * @public
+ */
+export interface ScreenshotClip extends BoundingBox {
+ /**
+ * @defaultValue `1`
+ */
+ scale?: number;
+}
+
+/**
+ * @public
+ */
+export interface ScreenshotOptions {
+ /**
+ * @defaultValue `false`
+ */
+ optimizeForSpeed?: boolean;
+ /**
+ * @defaultValue `'png'`
+ */
+ type?: 'png' | 'jpeg' | 'webp';
+ /**
+ * Quality of the image, between 0-100. Not applicable to `png` images.
+ */
+ quality?: number;
+ /**
+ * Capture the screenshot from the surface, rather than the view.
+ *
+ * @defaultValue `true`
+ */
+ fromSurface?: boolean;
+ /**
+ * When `true`, takes a screenshot of the full page.
+ *
+ * @defaultValue `false`
+ */
+ fullPage?: boolean;
+ /**
+ * Hides default white background and allows capturing screenshots with transparency.
+ *
+ * @defaultValue `false`
+ */
+ omitBackground?: boolean;
+ /**
+ * The file path to save the image to. The screenshot type will be inferred
+ * from file extension. If path is a relative path, then it is resolved
+ * relative to current working directory. If no path is provided, the image
+ * won't be saved to the disk.
+ */
+ path?: string;
+ /**
+ * Specifies the region of the page to clip.
+ */
+ clip?: ScreenshotClip;
+ /**
+ * Encoding of the image.
+ *
+ * @defaultValue `'binary'`
+ */
+ encoding?: 'base64' | 'binary';
+ /**
+ * Capture the screenshot beyond the viewport.
+ *
+ * @defaultValue `false` if there is no `clip`. `true` otherwise.
+ */
+ captureBeyondViewport?: boolean;
+}
+
+/**
+ * @public
+ * @experimental
+ */
+export interface ScreencastOptions {
+ /**
+ * File path to save the screencast to.
+ */
+ path?: `${string}.webm`;
+ /**
+ * Specifies the region of the viewport to crop.
+ */
+ crop?: BoundingBox;
+ /**
+ * Scales the output video.
+ *
+ * For example, `0.5` will shrink the width and height of the output video by
+ * half. `2` will double the width and height of the output video.
+ *
+ * @defaultValue `1`
+ */
+ scale?: number;
+ /**
+ * Specifies the speed to record at.
+ *
+ * For example, `0.5` will slowdown the output video by 50%. `2` will double the
+ * speed of the output video.
+ *
+ * @defaultValue `1`
+ */
+ speed?: number;
+ /**
+ * Path to the [ffmpeg](https://ffmpeg.org/).
+ *
+ * Required if `ffmpeg` is not in your PATH.
+ */
+ ffmpegPath?: string;
+}
+
+/**
+ * All the events that a page instance may emit.
+ *
+ * @public
+ */
+export const enum PageEvent {
+ /**
+ * Emitted when the page closes.
+ */
+ Close = 'close',
+ /**
+ * Emitted when JavaScript within the page calls one of console API methods,
+ * e.g. `console.log` or `console.dir`. Also emitted if the page throws an
+ * error or a warning.
+ *
+ * @remarks
+ * A `console` event provides a {@link ConsoleMessage} representing the
+ * console message that was logged.
+ *
+ * @example
+ * An example of handling `console` event:
+ *
+ * ```ts
+ * page.on('console', msg => {
+ * for (let i = 0; i < msg.args().length; ++i)
+ * console.log(`${i}: ${msg.args()[i]}`);
+ * });
+ * page.evaluate(() => console.log('hello', 5, {foo: 'bar'}));
+ * ```
+ */
+ Console = 'console',
+ /**
+ * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`,
+ * `confirm` or `beforeunload`. Puppeteer can respond to the dialog via
+ * {@link Dialog.accept} or {@link Dialog.dismiss}.
+ */
+ Dialog = 'dialog',
+ /**
+ * Emitted when the JavaScript
+ * {@link https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded | DOMContentLoaded }
+ * event is dispatched.
+ */
+ DOMContentLoaded = 'domcontentloaded',
+ /**
+ * Emitted when the page crashes. Will contain an `Error`.
+ */
+ Error = 'error',
+ /** Emitted when a frame is attached. Will contain a {@link Frame}. */
+ FrameAttached = 'frameattached',
+ /** Emitted when a frame is detached. Will contain a {@link Frame}. */
+ FrameDetached = 'framedetached',
+ /**
+ * Emitted when a frame is navigated to a new URL. Will contain a
+ * {@link Frame}.
+ */
+ FrameNavigated = 'framenavigated',
+ /**
+ * Emitted when the JavaScript
+ * {@link https://developer.mozilla.org/en-US/docs/Web/Events/load | load}
+ * event is dispatched.
+ */
+ Load = 'load',
+ /**
+ * Emitted when the JavaScript code makes a call to `console.timeStamp`. For
+ * the list of metrics see {@link Page.metrics | page.metrics}.
+ *
+ * @remarks
+ * Contains an object with two properties:
+ *
+ * - `title`: the title passed to `console.timeStamp`
+ * - `metrics`: object containing metrics as key/value pairs. The values will
+ * be `number`s.
+ */
+ Metrics = 'metrics',
+ /**
+ * Emitted when an uncaught exception happens within the page. Contains an
+ * `Error`.
+ */
+ PageError = 'pageerror',
+ /**
+ * Emitted when the page opens a new tab or window.
+ *
+ * Contains a {@link Page} corresponding to the popup window.
+ *
+ * @example
+ *
+ * ```ts
+ * const [popup] = await Promise.all([
+ * new Promise(resolve => page.once('popup', resolve)),
+ * page.click('a[target=_blank]'),
+ * ]);
+ * ```
+ *
+ * ```ts
+ * const [popup] = await Promise.all([
+ * new Promise(resolve => page.once('popup', resolve)),
+ * page.evaluate(() => window.open('https://example.com')),
+ * ]);
+ * ```
+ */
+ Popup = 'popup',
+ /**
+ * Emitted when a page issues a request and contains a {@link HTTPRequest}.
+ *
+ * @remarks
+ * The object is readonly. See {@link Page.setRequestInterception} for
+ * intercepting and mutating requests.
+ */
+ Request = 'request',
+ /**
+ * Emitted when a request ended up loading from cache. Contains a
+ * {@link HTTPRequest}.
+ *
+ * @remarks
+ * For certain requests, might contain undefined.
+ * {@link https://crbug.com/750469}
+ */
+ RequestServedFromCache = 'requestservedfromcache',
+ /**
+ * Emitted when a request fails, for example by timing out.
+ *
+ * Contains a {@link HTTPRequest}.
+ *
+ * @remarks
+ * HTTP Error responses, such as 404 or 503, are still successful responses
+ * from HTTP standpoint, so request will complete with `requestfinished` event
+ * and not with `requestfailed`.
+ */
+ RequestFailed = 'requestfailed',
+ /**
+ * Emitted when a request finishes successfully. Contains a
+ * {@link HTTPRequest}.
+ */
+ RequestFinished = 'requestfinished',
+ /**
+ * Emitted when a response is received. Contains a {@link HTTPResponse}.
+ */
+ Response = 'response',
+ /**
+ * Emitted when a dedicated
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker}
+ * is spawned by the page.
+ */
+ WorkerCreated = 'workercreated',
+ /**
+ * Emitted when a dedicated
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker}
+ * is destroyed by the page.
+ */
+ WorkerDestroyed = 'workerdestroyed',
+}
+
+export {
+ /**
+ * All the events that a page instance may emit.
+ *
+ * @deprecated Use {@link PageEvent}.
+ */
+ PageEvent as PageEmittedEvents,
+};
+
+/**
+ * Denotes the objects received by callback functions for page events.
+ *
+ * See {@link PageEvent} for more detail on the events and when they are
+ * emitted.
+ *
+ * @public
+ */
+export interface PageEvents extends Record<EventType, unknown> {
+ [PageEvent.Close]: undefined;
+ [PageEvent.Console]: ConsoleMessage;
+ [PageEvent.Dialog]: Dialog;
+ [PageEvent.DOMContentLoaded]: undefined;
+ [PageEvent.Error]: Error;
+ [PageEvent.FrameAttached]: Frame;
+ [PageEvent.FrameDetached]: Frame;
+ [PageEvent.FrameNavigated]: Frame;
+ [PageEvent.Load]: undefined;
+ [PageEvent.Metrics]: {title: string; metrics: Metrics};
+ [PageEvent.PageError]: Error;
+ [PageEvent.Popup]: Page | null;
+ [PageEvent.Request]: HTTPRequest;
+ [PageEvent.Response]: HTTPResponse;
+ [PageEvent.RequestFailed]: HTTPRequest;
+ [PageEvent.RequestFinished]: HTTPRequest;
+ [PageEvent.RequestServedFromCache]: HTTPRequest;
+ [PageEvent.WorkerCreated]: WebWorker;
+ [PageEvent.WorkerDestroyed]: WebWorker;
+}
+
+export type {
+ /**
+ * @deprecated Use {@link PageEvents}.
+ */
+ PageEvents as PageEventObject,
+};
+
+/**
+ * @public
+ */
+export interface NewDocumentScriptEvaluation {
+ identifier: string;
+}
+
+/**
+ * @internal
+ */
+export function setDefaultScreenshotOptions(options: ScreenshotOptions): void {
+ options.optimizeForSpeed ??= false;
+ options.type ??= 'png';
+ options.fromSurface ??= true;
+ options.fullPage ??= false;
+ options.omitBackground ??= false;
+ options.encoding ??= 'binary';
+ options.captureBeyondViewport ??= true;
+}
+
+/**
+ * Page provides methods to interact with a single tab or
+ * {@link https://developer.chrome.com/extensions/background_pages | extension background page}
+ * in the browser.
+ *
+ * :::note
+ *
+ * One Browser instance might have multiple Page instances.
+ *
+ * :::
+ *
+ * @example
+ * This example creates a page, navigates it to a URL, and then saves a screenshot:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.goto('https://example.com');
+ * await page.screenshot({path: 'screenshot.png'});
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * The Page class extends from Puppeteer's {@link EventEmitter} class and will
+ * emit various events which are documented in the {@link PageEvent} enum.
+ *
+ * @example
+ * This example logs a message for a single page `load` event:
+ *
+ * ```ts
+ * page.once('load', () => console.log('Page loaded!'));
+ * ```
+ *
+ * To unsubscribe from events use the {@link EventEmitter.off} method:
+ *
+ * ```ts
+ * function logRequest(interceptedRequest) {
+ * console.log('A request was made:', interceptedRequest.url());
+ * }
+ * page.on('request', logRequest);
+ * // Sometime later...
+ * page.off('request', logRequest);
+ * ```
+ *
+ * @public
+ */
+export abstract class Page extends EventEmitter<PageEvents> {
+ /**
+ * @internal
+ */
+ _isDragging = false;
+ /**
+ * @internal
+ */
+ _timeoutSettings = new TimeoutSettings();
+
+ #requestHandlers = new WeakMap<Handler<HTTPRequest>, Handler<HTTPRequest>>();
+
+ #requestsInFlight = 0;
+ #inflight$: Observable<number>;
+
+ /**
+ * @internal
+ */
+ constructor() {
+ super();
+
+ this.#inflight$ = fromEmitterEvent(this, PageEvent.Request).pipe(
+ takeUntil(fromEmitterEvent(this, PageEvent.Close)),
+ mergeMap(request => {
+ return concat(
+ of(1),
+ race(
+ fromEmitterEvent(this, PageEvent.Response).pipe(
+ filter(response => {
+ return response.request()._requestId === request._requestId;
+ })
+ ),
+ fromEmitterEvent(this, PageEvent.RequestFailed).pipe(
+ filter(failure => {
+ return failure._requestId === request._requestId;
+ })
+ ),
+ fromEmitterEvent(this, PageEvent.RequestFinished).pipe(
+ filter(success => {
+ return success._requestId === request._requestId;
+ })
+ )
+ ).pipe(
+ map(() => {
+ return -1;
+ })
+ )
+ );
+ })
+ );
+
+ this.#inflight$.subscribe(count => {
+ this.#requestsInFlight += count;
+ });
+ }
+
+ /**
+ * `true` if the service worker are being bypassed, `false` otherwise.
+ */
+ abstract isServiceWorkerBypassed(): boolean;
+
+ /**
+ * `true` if drag events are being intercepted, `false` otherwise.
+ *
+ * @deprecated We no longer support intercepting drag payloads. Use the new
+ * drag APIs found on {@link ElementHandle} to drag (or just use the
+ * {@link Page | Page.mouse}).
+ */
+ abstract isDragInterceptionEnabled(): boolean;
+
+ /**
+ * `true` if the page has JavaScript enabled, `false` otherwise.
+ */
+ abstract isJavaScriptEnabled(): boolean;
+
+ /**
+ * Listen to page events.
+ *
+ * @remarks
+ * This method exists to define event typings and handle proper wireup of
+ * cooperative request interception. Actual event listening and dispatching is
+ * delegated to {@link EventEmitter}.
+ *
+ * @internal
+ */
+ override on<K extends keyof EventsWithWildcard<PageEvents>>(
+ type: K,
+ handler: (event: EventsWithWildcard<PageEvents>[K]) => void
+ ): this {
+ if (type !== PageEvent.Request) {
+ return super.on(type, handler);
+ }
+ let wrapper = this.#requestHandlers.get(
+ handler as (event: PageEvents[PageEvent.Request]) => void
+ );
+ if (wrapper === undefined) {
+ wrapper = (event: HTTPRequest) => {
+ event.enqueueInterceptAction(() => {
+ return handler(event as EventsWithWildcard<PageEvents>[K]);
+ });
+ };
+ this.#requestHandlers.set(
+ handler as (event: PageEvents[PageEvent.Request]) => void,
+ wrapper
+ );
+ }
+ return super.on(
+ type,
+ wrapper as (event: EventsWithWildcard<PageEvents>[K]) => void
+ );
+ }
+
+ /**
+ * @internal
+ */
+ override off<K extends keyof EventsWithWildcard<PageEvents>>(
+ type: K,
+ handler: (event: EventsWithWildcard<PageEvents>[K]) => void
+ ): this {
+ if (type === PageEvent.Request) {
+ handler =
+ (this.#requestHandlers.get(
+ handler as (
+ event: EventsWithWildcard<PageEvents>[PageEvent.Request]
+ ) => void
+ ) as (event: EventsWithWildcard<PageEvents>[K]) => void) || handler;
+ }
+ return super.off(type, handler);
+ }
+
+ /**
+ * This method is typically coupled with an action that triggers file
+ * choosing.
+ *
+ * :::caution
+ *
+ * This must be called before the file chooser is launched. It will not return
+ * a currently active file chooser.
+ *
+ * :::
+ *
+ * @remarks
+ * In the "headful" browser, this method results in the native file picker
+ * dialog `not showing up` for the user.
+ *
+ * @example
+ * The following example clicks a button that issues a file chooser
+ * and then responds with `/tmp/myfile.pdf` as if a user has selected this file.
+ *
+ * ```ts
+ * const [fileChooser] = await Promise.all([
+ * page.waitForFileChooser(),
+ * page.click('#upload-file-button'),
+ * // some button that triggers file selection
+ * ]);
+ * await fileChooser.accept(['/tmp/myfile.pdf']);
+ * ```
+ */
+ abstract waitForFileChooser(
+ options?: WaitTimeoutOptions
+ ): Promise<FileChooser>;
+
+ /**
+ * Sets the page's geolocation.
+ *
+ * @remarks
+ * Consider using {@link BrowserContext.overridePermissions} to grant
+ * permissions for the page to read its geolocation.
+ *
+ * @example
+ *
+ * ```ts
+ * await page.setGeolocation({latitude: 59.95, longitude: 30.31667});
+ * ```
+ */
+ abstract setGeolocation(options: GeolocationOptions): Promise<void>;
+
+ /**
+ * A target this page was created from.
+ */
+ abstract target(): Target;
+
+ /**
+ * Get the browser the page belongs to.
+ */
+ abstract browser(): Browser;
+
+ /**
+ * Get the browser context that the page belongs to.
+ */
+ abstract browserContext(): BrowserContext;
+
+ /**
+ * The page's main frame.
+ *
+ * @remarks
+ * Page is guaranteed to have a main frame which persists during navigations.
+ */
+ abstract mainFrame(): Frame;
+
+ /**
+ * Creates a Chrome Devtools Protocol session attached to the page.
+ */
+ abstract createCDPSession(): Promise<CDPSession>;
+
+ /**
+ * {@inheritDoc Keyboard}
+ */
+ abstract get keyboard(): Keyboard;
+
+ /**
+ * {@inheritDoc Touchscreen}
+ */
+ abstract get touchscreen(): Touchscreen;
+
+ /**
+ * {@inheritDoc Coverage}
+ */
+ abstract get coverage(): Coverage;
+
+ /**
+ * {@inheritDoc Tracing}
+ */
+ abstract get tracing(): Tracing;
+
+ /**
+ * {@inheritDoc Accessibility}
+ */
+ abstract get accessibility(): Accessibility;
+
+ /**
+ * An array of all frames attached to the page.
+ */
+ abstract frames(): Frame[];
+
+ /**
+ * All of the dedicated {@link
+ * https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API |
+ * WebWorkers} associated with the page.
+ *
+ * @remarks
+ * This does not contain ServiceWorkers
+ */
+ abstract workers(): WebWorker[];
+
+ /**
+ * Activating request interception enables {@link HTTPRequest.abort},
+ * {@link HTTPRequest.continue} and {@link HTTPRequest.respond} methods. This
+ * provides the capability to modify network requests that are made by a page.
+ *
+ * Once request interception is enabled, every request will stall unless it's
+ * continued, responded or aborted; or completed using the browser cache.
+ *
+ * See the
+ * {@link https://pptr.dev/next/guides/request-interception|Request interception guide}
+ * for more details.
+ *
+ * @example
+ * An example of a naïve request interceptor that aborts all image requests:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.setRequestInterception(true);
+ * page.on('request', interceptedRequest => {
+ * if (
+ * interceptedRequest.url().endsWith('.png') ||
+ * interceptedRequest.url().endsWith('.jpg')
+ * )
+ * interceptedRequest.abort();
+ * else interceptedRequest.continue();
+ * });
+ * await page.goto('https://example.com');
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param value - Whether to enable request interception.
+ */
+ abstract setRequestInterception(value: boolean): Promise<void>;
+
+ /**
+ * Toggles ignoring of service worker for each request.
+ *
+ * @param bypass - Whether to bypass service worker and load from network.
+ */
+ abstract setBypassServiceWorker(bypass: boolean): Promise<void>;
+
+ /**
+ * @param enabled - Whether to enable drag interception.
+ *
+ * @deprecated We no longer support intercepting drag payloads. Use the new
+ * drag APIs found on {@link ElementHandle} to drag (or just use the
+ * {@link Page | Page.mouse}).
+ */
+ abstract setDragInterception(enabled: boolean): Promise<void>;
+
+ /**
+ * Sets the network connection to offline.
+ *
+ * It does not change the parameters used in {@link Page.emulateNetworkConditions}
+ *
+ * @param enabled - When `true`, enables offline mode for the page.
+ */
+ abstract setOfflineMode(enabled: boolean): Promise<void>;
+
+ /**
+ * This does not affect WebSockets and WebRTC PeerConnections (see
+ * https://crbug.com/563644). To set the page offline, you can use
+ * {@link Page.setOfflineMode}.
+ *
+ * A list of predefined network conditions can be used by importing
+ * {@link PredefinedNetworkConditions}.
+ *
+ * @example
+ *
+ * ```ts
+ * import {PredefinedNetworkConditions} from 'puppeteer';
+ * const slow3G = PredefinedNetworkConditions['Slow 3G'];
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.emulateNetworkConditions(slow3G);
+ * await page.goto('https://www.google.com');
+ * // other actions...
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param networkConditions - Passing `null` disables network condition
+ * emulation.
+ */
+ abstract emulateNetworkConditions(
+ networkConditions: NetworkConditions | null
+ ): Promise<void>;
+
+ /**
+ * This setting will change the default maximum navigation time for the
+ * following methods and related shortcuts:
+ *
+ * - {@link Page.goBack | page.goBack(options)}
+ *
+ * - {@link Page.goForward | page.goForward(options)}
+ *
+ * - {@link Page.goto | page.goto(url,options)}
+ *
+ * - {@link Page.reload | page.reload(options)}
+ *
+ * - {@link Page.setContent | page.setContent(html,options)}
+ *
+ * - {@link Page.waitForNavigation | page.waitForNavigation(options)}
+ * @param timeout - Maximum navigation time in milliseconds.
+ */
+ abstract setDefaultNavigationTimeout(timeout: number): void;
+
+ /**
+ * @param timeout - Maximum time in milliseconds.
+ */
+ abstract setDefaultTimeout(timeout: number): void;
+
+ /**
+ * Maximum time in milliseconds.
+ */
+ abstract getDefaultTimeout(): number;
+
+ /**
+ * Creates a locator for the provided selector. See {@link Locator} for
+ * details and supported actions.
+ *
+ * @remarks
+ * Locators API is experimental and we will not follow semver for breaking
+ * change in the Locators API.
+ */
+ locator<Selector extends string>(
+ selector: Selector
+ ): Locator<NodeFor<Selector>>;
+
+ /**
+ * Creates a locator for the provided function. See {@link Locator} for
+ * details and supported actions.
+ *
+ * @remarks
+ * Locators API is experimental and we will not follow semver for breaking
+ * change in the Locators API.
+ */
+ locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
+ locator<Selector extends string, Ret>(
+ selectorOrFunc: Selector | (() => Awaitable<Ret>)
+ ): Locator<NodeFor<Selector>> | Locator<Ret> {
+ if (typeof selectorOrFunc === 'string') {
+ return NodeLocator.create(this, selectorOrFunc);
+ } else {
+ return FunctionLocator.create(this, selectorOrFunc);
+ }
+ }
+
+ /**
+ * A shortcut for {@link Locator.race} that does not require static imports.
+ *
+ * @internal
+ */
+ locatorRace<Locators extends readonly unknown[] | []>(
+ locators: Locators
+ ): Locator<AwaitedLocator<Locators[number]>> {
+ return Locator.race(locators);
+ }
+
+ /**
+ * Runs `document.querySelector` within the page. If no element matches the
+ * selector, the return value resolves to `null`.
+ *
+ * @param selector - A `selector` to query page for
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
+ * to query page for.
+ */
+ async $<Selector extends string>(
+ selector: Selector
+ ): Promise<ElementHandle<NodeFor<Selector>> | null> {
+ return await this.mainFrame().$(selector);
+ }
+
+ /**
+ * The method runs `document.querySelectorAll` within the page. If no elements
+ * match the selector, the return value resolves to `[]`.
+ *
+ * @param selector - A `selector` to query page for
+ *
+ * @remarks
+ *
+ * Shortcut for {@link Frame.$$ | Page.mainFrame().$$(selector) }.
+ */
+ async $$<Selector extends string>(
+ selector: Selector
+ ): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
+ return await this.mainFrame().$$(selector);
+ }
+
+ /**
+ * @remarks
+ *
+ * The only difference between {@link Page.evaluate | page.evaluate} and
+ * `page.evaluateHandle` is that `evaluateHandle` will return the value
+ * wrapped in an in-page object.
+ *
+ * If the function passed to `page.evaluateHandle` returns a Promise, the
+ * function will wait for the promise to resolve and return its value.
+ *
+ * You can pass a string instead of a function (although functions are
+ * recommended as they are easier to debug and use with TypeScript):
+ *
+ * @example
+ *
+ * ```ts
+ * const aHandle = await page.evaluateHandle('document');
+ * ```
+ *
+ * @example
+ * {@link JSHandle} instances can be passed as arguments to the `pageFunction`:
+ *
+ * ```ts
+ * const aHandle = await page.evaluateHandle(() => document.body);
+ * const resultHandle = await page.evaluateHandle(
+ * body => body.innerHTML,
+ * aHandle
+ * );
+ * console.log(await resultHandle.jsonValue());
+ * await resultHandle.dispose();
+ * ```
+ *
+ * Most of the time this function returns a {@link JSHandle},
+ * but if `pageFunction` returns a reference to an element,
+ * you instead get an {@link ElementHandle} back:
+ *
+ * @example
+ *
+ * ```ts
+ * const button = await page.evaluateHandle(() =>
+ * document.querySelector('button')
+ * );
+ * // can call `click` because `button` is an `ElementHandle`
+ * await button.click();
+ * ```
+ *
+ * The TypeScript definitions assume that `evaluateHandle` returns
+ * a `JSHandle`, but if you know it's going to return an
+ * `ElementHandle`, pass it as the generic argument:
+ *
+ * ```ts
+ * const button = await page.evaluateHandle<ElementHandle>(...);
+ * ```
+ *
+ * @param pageFunction - a function that is run within the page
+ * @param args - arguments to be passed to the pageFunction
+ */
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluateHandle.name,
+ pageFunction
+ );
+ return await this.mainFrame().evaluateHandle(pageFunction, ...args);
+ }
+
+ /**
+ * This method iterates the JavaScript heap and finds all objects with the
+ * given prototype.
+ *
+ * @example
+ *
+ * ```ts
+ * // Create a Map object
+ * await page.evaluate(() => (window.map = new Map()));
+ * // Get a handle to the Map object prototype
+ * const mapPrototype = await page.evaluateHandle(() => Map.prototype);
+ * // Query all map instances into an array
+ * const mapInstances = await page.queryObjects(mapPrototype);
+ * // Count amount of map objects in heap
+ * const count = await page.evaluate(maps => maps.length, mapInstances);
+ * await mapInstances.dispose();
+ * await mapPrototype.dispose();
+ * ```
+ *
+ * @param prototypeHandle - a handle to the object prototype.
+ * @returns Promise which resolves to a handle to an array of objects with
+ * this prototype.
+ */
+ abstract queryObjects<Prototype>(
+ prototypeHandle: JSHandle<Prototype>
+ ): Promise<JSHandle<Prototype[]>>;
+
+ /**
+ * This method runs `document.querySelector` within the page and passes the
+ * result as the first argument to the `pageFunction`.
+ *
+ * @remarks
+ *
+ * If no element is found matching `selector`, the method will throw an error.
+ *
+ * If `pageFunction` returns a promise `$eval` will wait for the promise to
+ * resolve and then return its value.
+ *
+ * @example
+ *
+ * ```ts
+ * const searchValue = await page.$eval('#search', el => el.value);
+ * const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
+ * const html = await page.$eval('.main-container', el => el.outerHTML);
+ * ```
+ *
+ * If you are using TypeScript, you may have to provide an explicit type to the
+ * first argument of the `pageFunction`.
+ * By default it is typed as `Element`, but you may need to provide a more
+ * specific sub-type:
+ *
+ * @example
+ *
+ * ```ts
+ * // if you don't provide HTMLInputElement here, TS will error
+ * // as `value` is not on `Element`
+ * const searchValue = await page.$eval(
+ * '#search',
+ * (el: HTMLInputElement) => el.value
+ * );
+ * ```
+ *
+ * The compiler should be able to infer the return type
+ * from the `pageFunction` you provide. If it is unable to, you can use the generic
+ * type to tell the compiler what return type you expect from `$eval`:
+ *
+ * @example
+ *
+ * ```ts
+ * // The compiler can infer the return type in this case, but if it can't
+ * // or if you want to be more explicit, provide it as the generic type.
+ * const searchValue = await page.$eval<string>(
+ * '#search',
+ * (el: HTMLInputElement) => el.value
+ * );
+ * ```
+ *
+ * @param selector - the
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
+ * to query for
+ * @param pageFunction - the function to be evaluated in the page context.
+ * Will be passed the result of `document.querySelector(selector)` as its
+ * first argument.
+ * @param args - any additional arguments to pass through to `pageFunction`.
+ *
+ * @returns The result of calling `pageFunction`. If it returns an element it
+ * is wrapped in an {@link ElementHandle}, else the raw value itself is
+ * returned.
+ */
+ async $eval<
+ Selector extends string,
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
+ NodeFor<Selector>,
+ Params
+ >,
+ >(
+ selector: Selector,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
+ return await this.mainFrame().$eval(selector, pageFunction, ...args);
+ }
+
+ /**
+ * This method runs `Array.from(document.querySelectorAll(selector))` within
+ * the page and passes the result as the first argument to the `pageFunction`.
+ *
+ * @remarks
+ * If `pageFunction` returns a promise `$$eval` will wait for the promise to
+ * resolve and then return its value.
+ *
+ * @example
+ *
+ * ```ts
+ * // get the amount of divs on the page
+ * const divCount = await page.$$eval('div', divs => divs.length);
+ *
+ * // get the text content of all the `.options` elements:
+ * const options = await page.$$eval('div > span.options', options => {
+ * return options.map(option => option.textContent);
+ * });
+ * ```
+ *
+ * If you are using TypeScript, you may have to provide an explicit type to the
+ * first argument of the `pageFunction`.
+ * By default it is typed as `Element[]`, but you may need to provide a more
+ * specific sub-type:
+ *
+ * @example
+ *
+ * ```ts
+ * // if you don't provide HTMLInputElement here, TS will error
+ * // as `value` is not on `Element`
+ * await page.$$eval('input', (elements: HTMLInputElement[]) => {
+ * return elements.map(e => e.value);
+ * });
+ * ```
+ *
+ * The compiler should be able to infer the return type
+ * from the `pageFunction` you provide. If it is unable to, you can use the generic
+ * type to tell the compiler what return type you expect from `$$eval`:
+ *
+ * @example
+ *
+ * ```ts
+ * // The compiler can infer the return type in this case, but if it can't
+ * // or if you want to be more explicit, provide it as the generic type.
+ * const allInputValues = await page.$$eval<string[]>(
+ * 'input',
+ * (elements: HTMLInputElement[]) => elements.map(e => e.textContent)
+ * );
+ * ```
+ *
+ * @param selector - the
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
+ * to query for
+ * @param pageFunction - the function to be evaluated in the page context.
+ * Will be passed the result of
+ * `Array.from(document.querySelectorAll(selector))` as its first argument.
+ * @param args - any additional arguments to pass through to `pageFunction`.
+ *
+ * @returns The result of calling `pageFunction`. If it returns an element it
+ * is wrapped in an {@link ElementHandle}, else the raw value itself is
+ * returned.
+ */
+ async $$eval<
+ Selector extends string,
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<
+ Array<NodeFor<Selector>>,
+ Params
+ > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
+ >(
+ selector: Selector,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
+ return await this.mainFrame().$$eval(selector, pageFunction, ...args);
+ }
+
+ /**
+ * The method evaluates the XPath expression relative to the page document as
+ * its context node. If there are no such elements, the method resolves to an
+ * empty array.
+ *
+ * @remarks
+ * Shortcut for {@link Frame.$x | Page.mainFrame().$x(expression) }.
+ *
+ * @param expression - Expression to evaluate
+ */
+ async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
+ return await this.mainFrame().$x(expression);
+ }
+
+ /**
+ * If no URLs are specified, this method returns cookies for the current page
+ * URL. If URLs are specified, only cookies for those URLs are returned.
+ */
+ abstract cookies(...urls: string[]): Promise<Protocol.Network.Cookie[]>;
+
+ abstract deleteCookie(
+ ...cookies: Protocol.Network.DeleteCookiesRequest[]
+ ): Promise<void>;
+
+ /**
+ * @example
+ *
+ * ```ts
+ * await page.setCookie(cookieObject1, cookieObject2);
+ * ```
+ */
+ abstract setCookie(...cookies: Protocol.Network.CookieParam[]): Promise<void>;
+
+ /**
+ * Adds a `<script>` tag into the page with the desired URL or content.
+ *
+ * @remarks
+ * Shortcut for
+ * {@link Frame.addScriptTag | page.mainFrame().addScriptTag(options)}.
+ *
+ * @param options - Options for the script.
+ * @returns An {@link ElementHandle | element handle} to the injected
+ * `<script>` element.
+ */
+ async addScriptTag(
+ options: FrameAddScriptTagOptions
+ ): Promise<ElementHandle<HTMLScriptElement>> {
+ return await this.mainFrame().addScriptTag(options);
+ }
+
+ /**
+ * Adds a `<link rel="stylesheet">` tag into the page with the desired URL or
+ * a `<style type="text/css">` tag with the content.
+ *
+ * Shortcut for
+ * {@link Frame.(addStyleTag:2) | page.mainFrame().addStyleTag(options)}.
+ *
+ * @returns An {@link ElementHandle | element handle} to the injected `<link>`
+ * or `<style>` element.
+ */
+ async addStyleTag(
+ options: Omit<FrameAddStyleTagOptions, 'url'>
+ ): Promise<ElementHandle<HTMLStyleElement>>;
+ async addStyleTag(
+ options: FrameAddStyleTagOptions
+ ): Promise<ElementHandle<HTMLLinkElement>>;
+ async addStyleTag(
+ options: FrameAddStyleTagOptions
+ ): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> {
+ return await this.mainFrame().addStyleTag(options);
+ }
+
+ /**
+ * The method adds a function called `name` on the page's `window` object.
+ * When called, the function executes `puppeteerFunction` in node.js and
+ * returns a `Promise` which resolves to the return value of
+ * `puppeteerFunction`.
+ *
+ * If the puppeteerFunction returns a `Promise`, it will be awaited.
+ *
+ * :::note
+ *
+ * Functions installed via `page.exposeFunction` survive navigations.
+ *
+ * :::note
+ *
+ * @example
+ * An example of adding an `md5` function into the page:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ * import crypto from 'crypto';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * page.on('console', msg => console.log(msg.text()));
+ * await page.exposeFunction('md5', text =>
+ * crypto.createHash('md5').update(text).digest('hex')
+ * );
+ * await page.evaluate(async () => {
+ * // use window.md5 to compute hashes
+ * const myString = 'PUPPETEER';
+ * const myHash = await window.md5(myString);
+ * console.log(`md5 of ${myString} is ${myHash}`);
+ * });
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @example
+ * An example of adding a `window.readfile` function into the page:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ * import fs from 'fs';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * page.on('console', msg => console.log(msg.text()));
+ * await page.exposeFunction('readfile', async filePath => {
+ * return new Promise((resolve, reject) => {
+ * fs.readFile(filePath, 'utf8', (err, text) => {
+ * if (err) reject(err);
+ * else resolve(text);
+ * });
+ * });
+ * });
+ * await page.evaluate(async () => {
+ * // use window.readfile to read contents of a file
+ * const content = await window.readfile('/etc/hosts');
+ * console.log(content);
+ * });
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param name - Name of the function on the window object
+ * @param pptrFunction - Callback function which will be called in Puppeteer's
+ * context.
+ */
+ abstract exposeFunction(
+ name: string,
+ pptrFunction: Function | {default: Function}
+ ): Promise<void>;
+
+ /**
+ * The method removes a previously added function via ${@link Page.exposeFunction}
+ * called `name` from the page's `window` object.
+ */
+ abstract removeExposedFunction(name: string): Promise<void>;
+
+ /**
+ * Provide credentials for `HTTP authentication`.
+ *
+ * @remarks
+ * To disable authentication, pass `null`.
+ */
+ abstract authenticate(credentials: Credentials): Promise<void>;
+
+ /**
+ * The extra HTTP headers will be sent with every request the page initiates.
+ *
+ * :::tip
+ *
+ * All HTTP header names are lowercased. (HTTP headers are
+ * case-insensitive, so this shouldn’t impact your server code.)
+ *
+ * :::
+ *
+ * :::note
+ *
+ * page.setExtraHTTPHeaders does not guarantee the order of headers in
+ * the outgoing requests.
+ *
+ * :::
+ *
+ * @param headers - An object containing additional HTTP headers to be sent
+ * with every request. All header values must be strings.
+ */
+ abstract setExtraHTTPHeaders(headers: Record<string, string>): Promise<void>;
+
+ /**
+ * @param userAgent - Specific user agent to use in this page
+ * @param userAgentData - Specific user agent client hint data to use in this
+ * page
+ * @returns Promise which resolves when the user agent is set.
+ */
+ abstract setUserAgent(
+ userAgent: string,
+ userAgentMetadata?: Protocol.Emulation.UserAgentMetadata
+ ): Promise<void>;
+
+ /**
+ * Object containing metrics as key/value pairs.
+ *
+ * @returns
+ *
+ * - `Timestamp` : The timestamp when the metrics sample was taken.
+ *
+ * - `Documents` : Number of documents in the page.
+ *
+ * - `Frames` : Number of frames in the page.
+ *
+ * - `JSEventListeners` : Number of events in the page.
+ *
+ * - `Nodes` : Number of DOM nodes in the page.
+ *
+ * - `LayoutCount` : Total number of full or partial page layout.
+ *
+ * - `RecalcStyleCount` : Total number of page style recalculations.
+ *
+ * - `LayoutDuration` : Combined durations of all page layouts.
+ *
+ * - `RecalcStyleDuration` : Combined duration of all page style
+ * recalculations.
+ *
+ * - `ScriptDuration` : Combined duration of JavaScript execution.
+ *
+ * - `TaskDuration` : Combined duration of all tasks performed by the browser.
+ *
+ * - `JSHeapUsedSize` : Used JavaScript heap size.
+ *
+ * - `JSHeapTotalSize` : Total JavaScript heap size.
+ *
+ * @remarks
+ * All timestamps are in monotonic time: monotonically increasing time
+ * in seconds since an arbitrary point in the past.
+ */
+ abstract metrics(): Promise<Metrics>;
+
+ /**
+ * The page's URL.
+ *
+ * @remarks
+ *
+ * Shortcut for {@link Frame.url | page.mainFrame().url()}.
+ */
+ url(): string {
+ return this.mainFrame().url();
+ }
+
+ /**
+ * The full HTML contents of the page, including the DOCTYPE.
+ */
+ async content(): Promise<string> {
+ return await this.mainFrame().content();
+ }
+
+ /**
+ * Set the content of the page.
+ *
+ * @param html - HTML markup to assign to the page.
+ * @param options - Parameters that has some properties.
+ *
+ * @remarks
+ *
+ * The parameter `options` might have the following options.
+ *
+ * - `timeout` : Maximum time in milliseconds for resources to load, defaults
+ * to 30 seconds, pass `0` to disable timeout. The default value can be
+ * changed by using the {@link Page.setDefaultNavigationTimeout} or
+ * {@link Page.setDefaultTimeout} methods.
+ *
+ * - `waitUntil`: When to consider setting markup succeeded, defaults to
+ * `load`. Given an array of event strings, setting content is considered
+ * to be successful after all events have been fired. Events can be
+ * either:<br/>
+ * - `load` : consider setting content to be finished when the `load` event
+ * is fired.<br/>
+ * - `domcontentloaded` : consider setting content to be finished when the
+ * `DOMContentLoaded` event is fired.<br/>
+ * - `networkidle0` : consider setting content to be finished when there are
+ * no more than 0 network connections for at least `500` ms.<br/>
+ * - `networkidle2` : consider setting content to be finished when there are
+ * no more than 2 network connections for at least `500` ms.
+ */
+ async setContent(html: string, options?: WaitForOptions): Promise<void> {
+ await this.mainFrame().setContent(html, options);
+ }
+
+ /**
+ * Navigates the page to the given `url`.
+ *
+ * @remarks
+ *
+ * Navigation to `about:blank` or navigation to the same URL with a different
+ * hash will succeed and return `null`.
+ *
+ * :::warning
+ *
+ * Headless mode doesn't support navigation to a PDF document. See the {@link
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=761295 | upstream
+ * issue}.
+ *
+ * :::
+ *
+ * Shortcut for {@link Frame.goto | page.mainFrame().goto(url, options)}.
+ *
+ * @param url - URL to navigate page to. The URL should include scheme, e.g.
+ * `https://`
+ * @param options - Options to configure waiting behavior.
+ * @returns A promise which resolves to the main resource response. In case of
+ * multiple redirects, the navigation will resolve with the response of the
+ * last redirect.
+ * @throws If:
+ *
+ * - there's an SSL error (e.g. in case of self-signed certificates).
+ * - target URL is invalid.
+ * - the timeout is exceeded during navigation.
+ * - the remote server does not respond or is unreachable.
+ * - the main resource failed to load.
+ *
+ * This method will not throw an error when any valid HTTP status code is
+ * returned by the remote server, including 404 "Not Found" and 500 "Internal
+ * Server Error". The status code for such responses can be retrieved by
+ * calling {@link HTTPResponse.status}.
+ */
+ async goto(url: string, options?: GoToOptions): Promise<HTTPResponse | null> {
+ return await this.mainFrame().goto(url, options);
+ }
+
+ /**
+ * Reloads the page.
+ *
+ * @param options - Options to configure waiting behavior.
+ * @returns A promise which resolves to the main resource response. In case of
+ * multiple redirects, the navigation will resolve with the response of the
+ * last redirect.
+ */
+ abstract reload(options?: WaitForOptions): Promise<HTTPResponse | null>;
+
+ /**
+ * Waits for the page to navigate to a new URL or to reload. It is useful when
+ * you run code that will indirectly cause the page to navigate.
+ *
+ * @example
+ *
+ * ```ts
+ * const [response] = await Promise.all([
+ * page.waitForNavigation(), // The promise resolves after navigation has finished
+ * page.click('a.my-link'), // Clicking the link will indirectly cause a navigation
+ * ]);
+ * ```
+ *
+ * @remarks
+ *
+ * Usage of the
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/History_API | History API}
+ * to change the URL is considered a navigation.
+ *
+ * @param options - Navigation parameters which might have the following
+ * properties:
+ * @returns A `Promise` which resolves to the main resource response.
+ *
+ * - In case of multiple redirects, the navigation will resolve with the
+ * response of the last redirect.
+ * - In case of navigation to a different anchor or navigation due to History
+ * API usage, the navigation will resolve with `null`.
+ */
+ async waitForNavigation(
+ options: WaitForOptions = {}
+ ): Promise<HTTPResponse | null> {
+ return await this.mainFrame().waitForNavigation(options);
+ }
+
+ /**
+ * @param urlOrPredicate - A URL or predicate to wait for
+ * @param options - Optional waiting parameters
+ * @returns Promise which resolves to the matched request
+ * @example
+ *
+ * ```ts
+ * const firstRequest = await page.waitForRequest(
+ * 'https://example.com/resource'
+ * );
+ * const finalRequest = await page.waitForRequest(
+ * request => request.url() === 'https://example.com'
+ * );
+ * return finalRequest.response()?.ok();
+ * ```
+ *
+ * @remarks
+ * Optional Waiting Parameters have:
+ *
+ * - `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds, pass
+ * `0` to disable the timeout. The default value can be changed by using the
+ * {@link Page.setDefaultTimeout} method.
+ */
+ waitForRequest(
+ urlOrPredicate: string | AwaitablePredicate<HTTPRequest>,
+ options: WaitTimeoutOptions = {}
+ ): Promise<HTTPRequest> {
+ const {timeout: ms = this._timeoutSettings.timeout()} = options;
+ if (typeof urlOrPredicate === 'string') {
+ const url = urlOrPredicate;
+ urlOrPredicate = (request: HTTPRequest) => {
+ return request.url() === url;
+ };
+ }
+ const observable$ = fromEmitterEvent(this, PageEvent.Request).pipe(
+ filterAsync(urlOrPredicate),
+ raceWith(
+ timeout(ms),
+ fromEmitterEvent(this, PageEvent.Close).pipe(
+ map(() => {
+ throw new TargetCloseError('Page closed!');
+ })
+ )
+ )
+ );
+ return firstValueFrom(observable$);
+ }
+
+ /**
+ * @param urlOrPredicate - A URL or predicate to wait for.
+ * @param options - Optional waiting parameters
+ * @returns Promise which resolves to the matched response.
+ * @example
+ *
+ * ```ts
+ * const firstResponse = await page.waitForResponse(
+ * 'https://example.com/resource'
+ * );
+ * const finalResponse = await page.waitForResponse(
+ * response =>
+ * response.url() === 'https://example.com' && response.status() === 200
+ * );
+ * const finalResponse = await page.waitForResponse(async response => {
+ * return (await response.text()).includes('<html>');
+ * });
+ * return finalResponse.ok();
+ * ```
+ *
+ * @remarks
+ * Optional Parameter have:
+ *
+ * - `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds,
+ * pass `0` to disable the timeout. The default value can be changed by using
+ * the {@link Page.setDefaultTimeout} method.
+ */
+ waitForResponse(
+ urlOrPredicate: string | AwaitablePredicate<HTTPResponse>,
+ options: WaitTimeoutOptions = {}
+ ): Promise<HTTPResponse> {
+ const {timeout: ms = this._timeoutSettings.timeout()} = options;
+ if (typeof urlOrPredicate === 'string') {
+ const url = urlOrPredicate;
+ urlOrPredicate = (response: HTTPResponse) => {
+ return response.url() === url;
+ };
+ }
+ const observable$ = fromEmitterEvent(this, PageEvent.Response).pipe(
+ filterAsync(urlOrPredicate),
+ raceWith(
+ timeout(ms),
+ fromEmitterEvent(this, PageEvent.Close).pipe(
+ map(() => {
+ throw new TargetCloseError('Page closed!');
+ })
+ )
+ )
+ );
+ return firstValueFrom(observable$);
+ }
+
+ /**
+ * Waits for the network to be idle.
+ *
+ * @param options - Options to configure waiting behavior.
+ * @returns A promise which resolves once the network is idle.
+ */
+ waitForNetworkIdle(options: WaitForNetworkIdleOptions = {}): Promise<void> {
+ return firstValueFrom(this.waitForNetworkIdle$(options));
+ }
+
+ /**
+ * @internal
+ */
+ waitForNetworkIdle$(
+ options: WaitForNetworkIdleOptions = {}
+ ): Observable<void> {
+ const {
+ timeout: ms = this._timeoutSettings.timeout(),
+ idleTime = NETWORK_IDLE_TIME,
+ concurrency = 0,
+ } = options;
+
+ return this.#inflight$.pipe(
+ startWith(this.#requestsInFlight),
+ switchMap(() => {
+ if (this.#requestsInFlight > concurrency) {
+ return EMPTY;
+ } else {
+ return timer(idleTime);
+ }
+ }),
+ map(() => {}),
+ raceWith(
+ timeout(ms),
+ fromEmitterEvent(this, PageEvent.Close).pipe(
+ map(() => {
+ throw new TargetCloseError('Page closed!');
+ })
+ )
+ )
+ );
+ }
+
+ /**
+ * Waits for a frame matching the given conditions to appear.
+ *
+ * @example
+ *
+ * ```ts
+ * const frame = await page.waitForFrame(async frame => {
+ * return frame.name() === 'Test';
+ * });
+ * ```
+ */
+ async waitForFrame(
+ urlOrPredicate: string | ((frame: Frame) => Awaitable<boolean>),
+ options: WaitTimeoutOptions = {}
+ ): Promise<Frame> {
+ const {timeout: ms = this.getDefaultTimeout()} = options;
+
+ if (isString(urlOrPredicate)) {
+ urlOrPredicate = (frame: Frame) => {
+ return urlOrPredicate === frame.url();
+ };
+ }
+
+ return await firstValueFrom(
+ merge(
+ fromEmitterEvent(this, PageEvent.FrameAttached),
+ fromEmitterEvent(this, PageEvent.FrameNavigated),
+ from(this.frames())
+ ).pipe(
+ filterAsync(urlOrPredicate),
+ first(),
+ raceWith(
+ timeout(ms),
+ fromEmitterEvent(this, PageEvent.Close).pipe(
+ map(() => {
+ throw new TargetCloseError('Page closed.');
+ })
+ )
+ )
+ )
+ );
+ }
+
+ /**
+ * This method navigate to the previous page in history.
+ * @param options - Navigation parameters
+ * @returns Promise which resolves to the main resource response. In case of
+ * multiple redirects, the navigation will resolve with the response of the
+ * last redirect. If can not go back, resolves to `null`.
+ * @remarks
+ * The argument `options` might have the following properties:
+ *
+ * - `timeout` : Maximum navigation time in milliseconds, defaults to 30
+ * seconds, pass 0 to disable timeout. The default value can be changed by
+ * using the {@link Page.setDefaultNavigationTimeout} or
+ * {@link Page.setDefaultTimeout} methods.
+ *
+ * - `waitUntil` : When to consider navigation succeeded, defaults to `load`.
+ * Given an array of event strings, navigation is considered to be
+ * successful after all events have been fired. Events can be either:<br/>
+ * - `load` : consider navigation to be finished when the load event is
+ * fired.<br/>
+ * - `domcontentloaded` : consider navigation to be finished when the
+ * DOMContentLoaded event is fired.<br/>
+ * - `networkidle0` : consider navigation to be finished when there are no
+ * more than 0 network connections for at least `500` ms.<br/>
+ * - `networkidle2` : consider navigation to be finished when there are no
+ * more than 2 network connections for at least `500` ms.
+ */
+ abstract goBack(options?: WaitForOptions): Promise<HTTPResponse | null>;
+
+ /**
+ * This method navigate to the next page in history.
+ * @param options - Navigation Parameter
+ * @returns Promise which resolves to the main resource response. In case of
+ * multiple redirects, the navigation will resolve with the response of the
+ * last redirect. If can not go forward, resolves to `null`.
+ * @remarks
+ * The argument `options` might have the following properties:
+ *
+ * - `timeout` : Maximum navigation time in milliseconds, defaults to 30
+ * seconds, pass 0 to disable timeout. The default value can be changed by
+ * using the {@link Page.setDefaultNavigationTimeout} or
+ * {@link Page.setDefaultTimeout} methods.
+ *
+ * - `waitUntil`: When to consider navigation succeeded, defaults to `load`.
+ * Given an array of event strings, navigation is considered to be
+ * successful after all events have been fired. Events can be either:<br/>
+ * - `load` : consider navigation to be finished when the load event is
+ * fired.<br/>
+ * - `domcontentloaded` : consider navigation to be finished when the
+ * DOMContentLoaded event is fired.<br/>
+ * - `networkidle0` : consider navigation to be finished when there are no
+ * more than 0 network connections for at least `500` ms.<br/>
+ * - `networkidle2` : consider navigation to be finished when there are no
+ * more than 2 network connections for at least `500` ms.
+ */
+ abstract goForward(options?: WaitForOptions): Promise<HTTPResponse | null>;
+
+ /**
+ * Brings page to front (activates tab).
+ */
+ abstract bringToFront(): Promise<void>;
+
+ /**
+ * Emulates a given device's metrics and user agent.
+ *
+ * To aid emulation, Puppeteer provides a list of known devices that can be
+ * via {@link KnownDevices}.
+ *
+ * @remarks
+ * This method is a shortcut for calling two methods:
+ * {@link Page.setUserAgent} and {@link Page.setViewport}.
+ *
+ * This method will resize the page. A lot of websites don't expect phones to
+ * change size, so you should emulate before navigating to the page.
+ *
+ * @example
+ *
+ * ```ts
+ * import {KnownDevices} from 'puppeteer';
+ * const iPhone = KnownDevices['iPhone 6'];
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.emulate(iPhone);
+ * await page.goto('https://www.google.com');
+ * // other actions...
+ * await browser.close();
+ * })();
+ * ```
+ */
+ async emulate(device: Device): Promise<void> {
+ await Promise.all([
+ this.setUserAgent(device.userAgent),
+ this.setViewport(device.viewport),
+ ]);
+ }
+
+ /**
+ * @param enabled - Whether or not to enable JavaScript on the page.
+ * @remarks
+ * NOTE: changing this value won't affect scripts that have already been run.
+ * It will take full effect on the next navigation.
+ */
+ abstract setJavaScriptEnabled(enabled: boolean): Promise<void>;
+
+ /**
+ * Toggles bypassing page's Content-Security-Policy.
+ * @param enabled - sets bypassing of page's Content-Security-Policy.
+ * @remarks
+ * NOTE: CSP bypassing happens at the moment of CSP initialization rather than
+ * evaluation. Usually, this means that `page.setBypassCSP` should be called
+ * before navigating to the domain.
+ */
+ abstract setBypassCSP(enabled: boolean): Promise<void>;
+
+ /**
+ * @param type - Changes the CSS media type of the page. The only allowed
+ * values are `screen`, `print` and `null`. Passing `null` disables CSS media
+ * emulation.
+ * @example
+ *
+ * ```ts
+ * await page.evaluate(() => matchMedia('screen').matches);
+ * // → true
+ * await page.evaluate(() => matchMedia('print').matches);
+ * // → false
+ *
+ * await page.emulateMediaType('print');
+ * await page.evaluate(() => matchMedia('screen').matches);
+ * // → false
+ * await page.evaluate(() => matchMedia('print').matches);
+ * // → true
+ *
+ * await page.emulateMediaType(null);
+ * await page.evaluate(() => matchMedia('screen').matches);
+ * // → true
+ * await page.evaluate(() => matchMedia('print').matches);
+ * // → false
+ * ```
+ */
+ abstract emulateMediaType(type?: string): Promise<void>;
+
+ /**
+ * Enables CPU throttling to emulate slow CPUs.
+ * @param factor - slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).
+ */
+ abstract emulateCPUThrottling(factor: number | null): Promise<void>;
+
+ /**
+ * @param features - `<?Array<Object>>` Given an array of media feature
+ * objects, emulates CSS media features on the page. Each media feature object
+ * must have the following properties:
+ * @example
+ *
+ * ```ts
+ * await page.emulateMediaFeatures([
+ * {name: 'prefers-color-scheme', value: 'dark'},
+ * ]);
+ * await page.evaluate(
+ * () => matchMedia('(prefers-color-scheme: dark)').matches
+ * );
+ * // → true
+ * await page.evaluate(
+ * () => matchMedia('(prefers-color-scheme: light)').matches
+ * );
+ * // → false
+ *
+ * await page.emulateMediaFeatures([
+ * {name: 'prefers-reduced-motion', value: 'reduce'},
+ * ]);
+ * await page.evaluate(
+ * () => matchMedia('(prefers-reduced-motion: reduce)').matches
+ * );
+ * // → true
+ * await page.evaluate(
+ * () => matchMedia('(prefers-reduced-motion: no-preference)').matches
+ * );
+ * // → false
+ *
+ * await page.emulateMediaFeatures([
+ * {name: 'prefers-color-scheme', value: 'dark'},
+ * {name: 'prefers-reduced-motion', value: 'reduce'},
+ * ]);
+ * await page.evaluate(
+ * () => matchMedia('(prefers-color-scheme: dark)').matches
+ * );
+ * // → true
+ * await page.evaluate(
+ * () => matchMedia('(prefers-color-scheme: light)').matches
+ * );
+ * // → false
+ * await page.evaluate(
+ * () => matchMedia('(prefers-reduced-motion: reduce)').matches
+ * );
+ * // → true
+ * await page.evaluate(
+ * () => matchMedia('(prefers-reduced-motion: no-preference)').matches
+ * );
+ * // → false
+ *
+ * await page.emulateMediaFeatures([{name: 'color-gamut', value: 'p3'}]);
+ * await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches);
+ * // → true
+ * await page.evaluate(() => matchMedia('(color-gamut: p3)').matches);
+ * // → true
+ * await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches);
+ * // → false
+ * ```
+ */
+ abstract emulateMediaFeatures(features?: MediaFeature[]): Promise<void>;
+
+ /**
+ * @param timezoneId - Changes the timezone of the page. See
+ * {@link https://source.chromium.org/chromium/chromium/deps/icu.git/+/faee8bc70570192d82d2978a71e2a615788597d1:source/data/misc/metaZones.txt | ICU’s metaZones.txt}
+ * for a list of supported timezone IDs. Passing
+ * `null` disables timezone emulation.
+ */
+ abstract emulateTimezone(timezoneId?: string): Promise<void>;
+
+ /**
+ * Emulates the idle state.
+ * If no arguments set, clears idle state emulation.
+ *
+ * @example
+ *
+ * ```ts
+ * // set idle emulation
+ * await page.emulateIdleState({isUserActive: true, isScreenUnlocked: false});
+ *
+ * // do some checks here
+ * ...
+ *
+ * // clear idle emulation
+ * await page.emulateIdleState();
+ * ```
+ *
+ * @param overrides - Mock idle state. If not set, clears idle overrides
+ */
+ abstract emulateIdleState(overrides?: {
+ isUserActive: boolean;
+ isScreenUnlocked: boolean;
+ }): Promise<void>;
+
+ /**
+ * Simulates the given vision deficiency on the page.
+ *
+ * @example
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.goto('https://v8.dev/blog/10-years');
+ *
+ * await page.emulateVisionDeficiency('achromatopsia');
+ * await page.screenshot({path: 'achromatopsia.png'});
+ *
+ * await page.emulateVisionDeficiency('deuteranopia');
+ * await page.screenshot({path: 'deuteranopia.png'});
+ *
+ * await page.emulateVisionDeficiency('blurredVision');
+ * await page.screenshot({path: 'blurred-vision.png'});
+ *
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param type - the type of deficiency to simulate, or `'none'` to reset.
+ */
+ abstract emulateVisionDeficiency(
+ type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
+ ): Promise<void>;
+
+ /**
+ * `page.setViewport` will resize the page. A lot of websites don't expect
+ * phones to change size, so you should set the viewport before navigating to
+ * the page.
+ *
+ * In the case of multiple pages in a single browser, each page can have its
+ * own viewport size.
+ * @example
+ *
+ * ```ts
+ * const page = await browser.newPage();
+ * await page.setViewport({
+ * width: 640,
+ * height: 480,
+ * deviceScaleFactor: 1,
+ * });
+ * await page.goto('https://example.com');
+ * ```
+ *
+ * @param viewport -
+ * @remarks
+ * NOTE: in certain cases, setting viewport will reload the page in order to
+ * set the isMobile or hasTouch properties.
+ */
+ abstract setViewport(viewport: Viewport): Promise<void>;
+
+ /**
+ * Returns the current page viewport settings without checking the actual page
+ * viewport.
+ *
+ * This is either the viewport set with the previous {@link Page.setViewport}
+ * call or the default viewport set via
+ * {@link BrowserConnectOptions | BrowserConnectOptions.defaultViewport}.
+ */
+ abstract viewport(): Viewport | null;
+
+ /**
+ * Evaluates a function in the page's context and returns the result.
+ *
+ * If the function passed to `page.evaluate` returns a Promise, the
+ * function will wait for the promise to resolve and return its value.
+ *
+ * @example
+ *
+ * ```ts
+ * const result = await frame.evaluate(() => {
+ * return Promise.resolve(8 * 7);
+ * });
+ * console.log(result); // prints "56"
+ * ```
+ *
+ * You can pass a string instead of a function (although functions are
+ * recommended as they are easier to debug and use with TypeScript):
+ *
+ * @example
+ *
+ * ```ts
+ * const aHandle = await page.evaluate('1 + 2');
+ * ```
+ *
+ * To get the best TypeScript experience, you should pass in as the
+ * generic the type of `pageFunction`:
+ *
+ * ```ts
+ * const aHandle = await page.evaluate(() => 2);
+ * ```
+ *
+ * @example
+ *
+ * {@link ElementHandle} instances (including {@link JSHandle}s) can be passed
+ * as arguments to the `pageFunction`:
+ *
+ * ```ts
+ * const bodyHandle = await page.$('body');
+ * const html = await page.evaluate(body => body.innerHTML, bodyHandle);
+ * await bodyHandle.dispose();
+ * ```
+ *
+ * @param pageFunction - a function that is run within the page
+ * @param args - arguments to be passed to the pageFunction
+ *
+ * @returns the return value of `pageFunction`.
+ */
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluate.name,
+ pageFunction
+ );
+ return await this.mainFrame().evaluate(pageFunction, ...args);
+ }
+
+ /**
+ * Adds a function which would be invoked in one of the following scenarios:
+ *
+ * - whenever the page is navigated
+ *
+ * - whenever the child frame is attached or navigated. In this case, the
+ * function is invoked in the context of the newly attached frame.
+ *
+ * The function is invoked after the document was created but before any of
+ * its scripts were run. This is useful to amend the JavaScript environment,
+ * e.g. to seed `Math.random`.
+ * @param pageFunction - Function to be evaluated in browser context
+ * @param args - Arguments to pass to `pageFunction`
+ * @example
+ * An example of overriding the navigator.languages property before the page loads:
+ *
+ * ```ts
+ * // preload.js
+ *
+ * // overwrite the `languages` property to use a custom getter
+ * Object.defineProperty(navigator, 'languages', {
+ * get: function () {
+ * return ['en-US', 'en', 'bn'];
+ * },
+ * });
+ *
+ * // In your puppeteer script, assuming the preload.js file is
+ * // in same folder of our script.
+ * const preloadFile = fs.readFileSync('./preload.js', 'utf8');
+ * await page.evaluateOnNewDocument(preloadFile);
+ * ```
+ */
+ abstract evaluateOnNewDocument<
+ Params extends unknown[],
+ Func extends (...args: Params) => unknown = (...args: Params) => unknown,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<NewDocumentScriptEvaluation>;
+
+ /**
+ * Removes script that injected into page by Page.evaluateOnNewDocument.
+ *
+ * @param identifier - script identifier
+ */
+ abstract removeScriptToEvaluateOnNewDocument(
+ identifier: string
+ ): Promise<void>;
+
+ /**
+ * Toggles ignoring cache for each request based on the enabled state. By
+ * default, caching is enabled.
+ * @param enabled - sets the `enabled` state of cache
+ * @defaultValue `true`
+ */
+ abstract setCacheEnabled(enabled?: boolean): Promise<void>;
+
+ /**
+ * @internal
+ */
+ async _maybeWriteBufferToFile(
+ path: string | undefined,
+ buffer: Buffer
+ ): Promise<void> {
+ if (!path) {
+ return;
+ }
+
+ const fs = await importFSPromises();
+
+ await fs.writeFile(path, buffer);
+ }
+
+ /**
+ * Captures a screencast of this {@link Page | page}.
+ *
+ * @example
+ * Recording a {@link Page | page}:
+ *
+ * ```
+ * import puppeteer from 'puppeteer';
+ *
+ * // Launch a browser
+ * const browser = await puppeteer.launch();
+ *
+ * // Create a new page
+ * const page = await browser.newPage();
+ *
+ * // Go to your site.
+ * await page.goto("https://www.example.com");
+ *
+ * // Start recording.
+ * const recorder = await page.screencast({path: 'recording.webm'});
+ *
+ * // Do something.
+ *
+ * // Stop recording.
+ * await recorder.stop();
+ *
+ * browser.close();
+ * ```
+ *
+ * @param options - Configures screencast behavior.
+ *
+ * @experimental
+ *
+ * @remarks
+ *
+ * All recordings will be {@link https://www.webmproject.org/ | WebM} format using
+ * the {@link https://www.webmproject.org/vp9/ | VP9} video codec. The FPS is 30.
+ *
+ * You must have {@link https://ffmpeg.org/ | ffmpeg} installed on your system.
+ */
+ async screencast(
+ options: Readonly<ScreencastOptions> = {}
+ ): Promise<ScreenRecorder> {
+ const [{ScreenRecorder}, [width, height, devicePixelRatio]] =
+ await Promise.all([
+ import('../node/ScreenRecorder.js'),
+ this.#getNativePixelDimensions(),
+ ]);
+
+ let crop: BoundingBox | undefined;
+ if (options.crop) {
+ const {
+ x,
+ y,
+ width: cropWidth,
+ height: cropHeight,
+ } = roundRectangle(normalizeRectangle(options.crop));
+ if (x < 0 || y < 0) {
+ throw new Error(
+ `\`crop.x\` and \`crop.y\` must be greater than or equal to 0.`
+ );
+ }
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ throw new Error(
+ `\`crop.height\` and \`crop.width\` must be greater than or equal to 0.`
+ );
+ }
+
+ const viewportWidth = width / devicePixelRatio;
+ const viewportHeight = height / devicePixelRatio;
+ if (x + cropWidth > viewportWidth) {
+ throw new Error(
+ `\`crop.width\` cannot be larger than the viewport width (${viewportWidth}).`
+ );
+ }
+ if (y + cropHeight > viewportHeight) {
+ throw new Error(
+ `\`crop.height\` cannot be larger than the viewport height (${viewportHeight}).`
+ );
+ }
+
+ crop = {
+ x: x * devicePixelRatio,
+ y: y * devicePixelRatio,
+ width: cropWidth * devicePixelRatio,
+ height: cropHeight * devicePixelRatio,
+ };
+ }
+ if (options.speed !== undefined && options.speed <= 0) {
+ throw new Error(`\`speed\` must be greater than 0.`);
+ }
+ if (options.scale !== undefined && options.scale <= 0) {
+ throw new Error(`\`scale\` must be greater than 0.`);
+ }
+
+ const recorder = new ScreenRecorder(this, width, height, {
+ ...options,
+ path: options.ffmpegPath,
+ crop,
+ });
+ try {
+ await this._startScreencast();
+ } catch (error) {
+ void recorder.stop();
+ throw error;
+ }
+ if (options.path) {
+ const {createWriteStream} = await import('fs');
+ const stream = createWriteStream(options.path, 'binary');
+ recorder.pipe(stream);
+ }
+ return recorder;
+ }
+
+ #screencastSessionCount = 0;
+ #startScreencastPromise: Promise<void> | undefined;
+
+ /**
+ * @internal
+ */
+ async _startScreencast(): Promise<void> {
+ ++this.#screencastSessionCount;
+ if (!this.#startScreencastPromise) {
+ this.#startScreencastPromise = this.mainFrame()
+ .client.send('Page.startScreencast', {format: 'png'})
+ .then(() => {
+ // Wait for the first frame.
+ return new Promise(resolve => {
+ return this.mainFrame().client.once('Page.screencastFrame', () => {
+ return resolve();
+ });
+ });
+ });
+ }
+ await this.#startScreencastPromise;
+ }
+
+ /**
+ * @internal
+ */
+ async _stopScreencast(): Promise<void> {
+ --this.#screencastSessionCount;
+ if (!this.#startScreencastPromise) {
+ return;
+ }
+ this.#startScreencastPromise = undefined;
+ if (this.#screencastSessionCount === 0) {
+ await this.mainFrame().client.send('Page.stopScreencast');
+ }
+ }
+
+ /**
+ * Gets the native, non-emulated dimensions of the viewport.
+ */
+ async #getNativePixelDimensions(): Promise<
+ readonly [width: number, height: number, devicePixelRatio: number]
+ > {
+ const viewport = this.viewport();
+ using stack = new DisposableStack();
+ if (viewport && viewport.deviceScaleFactor !== 0) {
+ await this.setViewport({...viewport, deviceScaleFactor: 0});
+ stack.defer(() => {
+ void this.setViewport(viewport).catch(debugError);
+ });
+ }
+ return await this.mainFrame()
+ .isolatedRealm()
+ .evaluate(() => {
+ return [
+ window.visualViewport!.width * window.devicePixelRatio,
+ window.visualViewport!.height * window.devicePixelRatio,
+ window.devicePixelRatio,
+ ] as const;
+ });
+ }
+
+ /**
+ * Captures a screenshot of this {@link Page | page}.
+ *
+ * @param options - Configures screenshot behavior.
+ */
+ async screenshot(
+ options: Readonly<ScreenshotOptions> & {encoding: 'base64'}
+ ): Promise<string>;
+ async screenshot(options?: Readonly<ScreenshotOptions>): Promise<Buffer>;
+ @guarded(function () {
+ return this.browser();
+ })
+ async screenshot(
+ userOptions: Readonly<ScreenshotOptions> = {}
+ ): Promise<Buffer | string> {
+ await this.bringToFront();
+
+ // TODO: use structuredClone after Node 16 support is dropped.
+ const options = {
+ ...userOptions,
+ clip: userOptions.clip
+ ? {
+ ...userOptions.clip,
+ }
+ : undefined,
+ };
+ if (options.type === undefined && options.path !== undefined) {
+ const filePath = options.path;
+ // Note we cannot use Node.js here due to browser compatability.
+ const extension = filePath
+ .slice(filePath.lastIndexOf('.') + 1)
+ .toLowerCase();
+ switch (extension) {
+ case 'png':
+ options.type = 'png';
+ break;
+ case 'jpeg':
+ case 'jpg':
+ options.type = 'jpeg';
+ break;
+ case 'webp':
+ options.type = 'webp';
+ break;
+ }
+ }
+ if (options.quality !== undefined) {
+ if (options.quality < 0 && options.quality > 100) {
+ throw new Error(
+ `Expected 'quality' (${options.quality}) to be between 0 and 100, inclusive.`
+ );
+ }
+ if (
+ options.type === undefined ||
+ !['jpeg', 'webp'].includes(options.type)
+ ) {
+ throw new Error(
+ `${options.type ?? 'png'} screenshots do not support 'quality'.`
+ );
+ }
+ }
+ if (options.clip) {
+ if (options.clip.width <= 0) {
+ throw new Error("'width' in 'clip' must be positive.");
+ }
+ if (options.clip.height <= 0) {
+ throw new Error("'height' in 'clip' must be positive.");
+ }
+ }
+
+ setDefaultScreenshotOptions(options);
+
+ await using stack = new AsyncDisposableStack();
+ if (options.clip) {
+ if (options.fullPage) {
+ throw new Error("'clip' and 'fullPage' are mutually exclusive");
+ }
+
+ options.clip = roundRectangle(normalizeRectangle(options.clip));
+ } else {
+ if (options.fullPage) {
+ // If `captureBeyondViewport` is `false`, then we set the viewport to
+ // capture the full page. Note this may be affected by on-page CSS and
+ // JavaScript.
+ if (!options.captureBeyondViewport) {
+ const scrollDimensions = await this.mainFrame()
+ .isolatedRealm()
+ .evaluate(() => {
+ const element = document.documentElement;
+ return {
+ width: element.scrollWidth,
+ height: element.scrollHeight,
+ };
+ });
+ const viewport = this.viewport();
+ await this.setViewport({
+ ...viewport,
+ ...scrollDimensions,
+ });
+ stack.defer(async () => {
+ if (viewport) {
+ await this.setViewport(viewport).catch(debugError);
+ } else {
+ await this.setViewport({
+ width: 0,
+ height: 0,
+ }).catch(debugError);
+ }
+ });
+ }
+ } else {
+ options.captureBeyondViewport = false;
+ }
+ }
+
+ const data = await this._screenshot(options);
+ if (options.encoding === 'base64') {
+ return data;
+ }
+ const buffer = Buffer.from(data, 'base64');
+ await this._maybeWriteBufferToFile(options.path, buffer);
+ return buffer;
+ }
+
+ /**
+ * @internal
+ */
+ abstract _screenshot(options: Readonly<ScreenshotOptions>): Promise<string>;
+
+ /**
+ * Generates a PDF of the page with the `print` CSS media type.
+ *
+ * @param options - options for generating the PDF.
+ *
+ * @remarks
+ *
+ * To generate a PDF with the `screen` media type, call
+ * {@link Page.emulateMediaType | `page.emulateMediaType('screen')`} before
+ * calling `page.pdf()`.
+ *
+ * By default, `page.pdf()` generates a pdf with modified colors for printing.
+ * Use the
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-print-color-adjust | `-webkit-print-color-adjust`}
+ * property to force rendering of exact colors.
+ */
+ abstract createPDFStream(options?: PDFOptions): Promise<Readable>;
+
+ /**
+ * {@inheritDoc Page.createPDFStream}
+ */
+ abstract pdf(options?: PDFOptions): Promise<Buffer>;
+
+ /**
+ * The page's title
+ *
+ * @remarks
+ *
+ * Shortcut for {@link Frame.title | page.mainFrame().title()}.
+ */
+ async title(): Promise<string> {
+ return await this.mainFrame().title();
+ }
+
+ abstract close(options?: {runBeforeUnload?: boolean}): Promise<void>;
+
+ /**
+ * Indicates that the page has been closed.
+ * @returns
+ */
+ abstract isClosed(): boolean;
+
+ /**
+ * {@inheritDoc Mouse}
+ */
+ abstract get mouse(): Mouse;
+
+ /**
+ * This method fetches an element with `selector`, scrolls it into view if
+ * needed, and then uses {@link Page | Page.mouse} to click in the center of the
+ * element. If there's no element matching `selector`, the method throws an
+ * error.
+ *
+ * @remarks
+ *
+ * Bear in mind that if `click()` triggers a navigation event and
+ * there's a separate `page.waitForNavigation()` promise to be resolved, you
+ * may end up with a race condition that yields unexpected results. The
+ * correct pattern for click and wait for navigation is the following:
+ *
+ * ```ts
+ * const [response] = await Promise.all([
+ * page.waitForNavigation(waitOptions),
+ * page.click(selector, clickOptions),
+ * ]);
+ * ```
+ *
+ * Shortcut for {@link Frame.click | page.mainFrame().click(selector[, options]) }.
+ * @param selector - A `selector` to search for element to click. If there are
+ * multiple elements satisfying the `selector`, the first will be clicked
+ * @param options - `Object`
+ * @returns Promise which resolves when the element matching `selector` is
+ * successfully clicked. The Promise will be rejected if there is no element
+ * matching `selector`.
+ */
+ click(selector: string, options?: Readonly<ClickOptions>): Promise<void> {
+ return this.mainFrame().click(selector, options);
+ }
+
+ /**
+ * This method fetches an element with `selector` and focuses it. If there's no
+ * element matching `selector`, the method throws an error.
+ * @param selector - A
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector }
+ * of an element to focus. If there are multiple elements satisfying the
+ * selector, the first will be focused.
+ * @returns Promise which resolves when the element matching selector is
+ * successfully focused. The promise will be rejected if there is no element
+ * matching selector.
+ *
+ * @remarks
+ *
+ * Shortcut for {@link Frame.focus | page.mainFrame().focus(selector)}.
+ */
+ focus(selector: string): Promise<void> {
+ return this.mainFrame().focus(selector);
+ }
+
+ /**
+ * This method fetches an element with `selector`, scrolls it into view if
+ * needed, and then uses {@link Page | Page.mouse}
+ * to hover over the center of the element.
+ * If there's no element matching `selector`, the method throws an error.
+ * @param selector - A
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
+ * to search for element to hover. If there are multiple elements satisfying
+ * the selector, the first will be hovered.
+ * @returns Promise which resolves when the element matching `selector` is
+ * successfully hovered. Promise gets rejected if there's no element matching
+ * `selector`.
+ *
+ * @remarks
+ *
+ * Shortcut for {@link Page.hover | page.mainFrame().hover(selector)}.
+ */
+ hover(selector: string): Promise<void> {
+ return this.mainFrame().hover(selector);
+ }
+
+ /**
+ * Triggers a `change` and `input` event once all the provided options have been
+ * selected. If there's no `<select>` element matching `selector`, the method
+ * throws an error.
+ *
+ * @example
+ *
+ * ```ts
+ * page.select('select#colors', 'blue'); // single selection
+ * page.select('select#colors', 'red', 'green', 'blue'); // multiple selections
+ * ```
+ *
+ * @param selector - A
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | Selector}
+ * to query the page for
+ * @param values - Values of options to select. If the `<select>` has the
+ * `multiple` attribute, all values are considered, otherwise only the first one
+ * is taken into account.
+ * @returns
+ *
+ * @remarks
+ *
+ * Shortcut for {@link Frame.select | page.mainFrame().select()}
+ */
+ select(selector: string, ...values: string[]): Promise<string[]> {
+ return this.mainFrame().select(selector, ...values);
+ }
+
+ /**
+ * This method fetches an element with `selector`, scrolls it into view if
+ * needed, and then uses {@link Page | Page.touchscreen}
+ * to tap in the center of the element.
+ * If there's no element matching `selector`, the method throws an error.
+ * @param selector - A
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | Selector}
+ * to search for element to tap. If there are multiple elements satisfying the
+ * selector, the first will be tapped.
+ *
+ * @remarks
+ *
+ * Shortcut for {@link Frame.tap | page.mainFrame().tap(selector)}.
+ */
+ tap(selector: string): Promise<void> {
+ return this.mainFrame().tap(selector);
+ }
+
+ /**
+ * Sends a `keydown`, `keypress/input`, and `keyup` event for each character
+ * in the text.
+ *
+ * To press a special key, like `Control` or `ArrowDown`, use {@link Keyboard.press}.
+ * @example
+ *
+ * ```ts
+ * await page.type('#mytextarea', 'Hello');
+ * // Types instantly
+ * await page.type('#mytextarea', 'World', {delay: 100});
+ * // Types slower, like a user
+ * ```
+ *
+ * @param selector - A
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
+ * of an element to type into. If there are multiple elements satisfying the
+ * selector, the first will be used.
+ * @param text - A text to type into a focused element.
+ * @param options - have property `delay` which is the Time to wait between
+ * key presses in milliseconds. Defaults to `0`.
+ * @returns
+ */
+ type(
+ selector: string,
+ text: string,
+ options?: Readonly<KeyboardTypeOptions>
+ ): Promise<void> {
+ return this.mainFrame().type(selector, text, options);
+ }
+
+ /**
+ * @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`.
+ *
+ * Causes your script to wait for the given number of milliseconds.
+ *
+ * @remarks
+ *
+ * It's generally recommended to not wait for a number of seconds, but instead
+ * use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or
+ * {@link Frame.waitForFunction} to wait for exactly the conditions you want.
+ *
+ * @example
+ *
+ * Wait for 1 second:
+ *
+ * ```ts
+ * await page.waitForTimeout(1000);
+ * ```
+ *
+ * @param milliseconds - the number of milliseconds to wait.
+ */
+ waitForTimeout(milliseconds: number): Promise<void> {
+ return this.mainFrame().waitForTimeout(milliseconds);
+ }
+
+ /**
+ * Wait for the `selector` to appear in page. If at the moment of calling the
+ * method the `selector` already exists, the method will return immediately. If
+ * the `selector` doesn't appear after the `timeout` milliseconds of waiting, the
+ * function will throw.
+ *
+ * @example
+ * This method works across navigations:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * let currentURL;
+ * page
+ * .waitForSelector('img')
+ * .then(() => console.log('First URL with image: ' + currentURL));
+ * for (currentURL of [
+ * 'https://example.com',
+ * 'https://google.com',
+ * 'https://bbc.com',
+ * ]) {
+ * await page.goto(currentURL);
+ * }
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param selector - A
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
+ * of an element to wait for
+ * @param options - Optional waiting parameters
+ * @returns Promise which resolves when element specified by selector string
+ * is added to DOM. Resolves to `null` if waiting for hidden: `true` and
+ * selector is not found in DOM.
+ *
+ * @remarks
+ * The optional Parameter in Arguments `options` are:
+ *
+ * - `visible`: A boolean wait for element to be present in DOM and to be
+ * visible, i.e. to not have `display: none` or `visibility: hidden` CSS
+ * properties. Defaults to `false`.
+ *
+ * - `hidden`: Wait for element to not be found in the DOM or to be hidden,
+ * i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to
+ * `false`.
+ *
+ * - `timeout`: maximum time to wait for in milliseconds. Defaults to `30000`
+ * (30 seconds). Pass `0` to disable timeout. The default value can be changed
+ * by using the {@link Page.setDefaultTimeout} method.
+ */
+ async waitForSelector<Selector extends string>(
+ selector: Selector,
+ options: WaitForSelectorOptions = {}
+ ): Promise<ElementHandle<NodeFor<Selector>> | null> {
+ return await this.mainFrame().waitForSelector(selector, options);
+ }
+
+ /**
+ * Wait for the `xpath` to appear in page. If at the moment of calling the
+ * method the `xpath` already exists, the method will return immediately. If
+ * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
+ * function will throw.
+ *
+ * @example
+ * This method works across navigation
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * let currentURL;
+ * page
+ * .waitForXPath('//img')
+ * .then(() => console.log('First URL with image: ' + currentURL));
+ * for (currentURL of [
+ * 'https://example.com',
+ * 'https://google.com',
+ * 'https://bbc.com',
+ * ]) {
+ * await page.goto(currentURL);
+ * }
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @param xpath - A
+ * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an
+ * element to wait for
+ * @param options - Optional waiting parameters
+ * @returns Promise which resolves when element specified by xpath string is
+ * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
+ * not found in DOM, otherwise resolves to `ElementHandle`.
+ * @remarks
+ * The optional Argument `options` have properties:
+ *
+ * - `visible`: A boolean to wait for element to be present in DOM and to be
+ * visible, i.e. to not have `display: none` or `visibility: hidden` CSS
+ * properties. Defaults to `false`.
+ *
+ * - `hidden`: A boolean wait for element to not be found in the DOM or to be
+ * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
+ * Defaults to `false`.
+ *
+ * - `timeout`: A number which is maximum time to wait for in milliseconds.
+ * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default
+ * value can be changed by using the {@link Page.setDefaultTimeout} method.
+ */
+ waitForXPath(
+ xpath: string,
+ options?: WaitForSelectorOptions
+ ): Promise<ElementHandle<Node> | null> {
+ return this.mainFrame().waitForXPath(xpath, options);
+ }
+
+ /**
+ * Waits for the provided function, `pageFunction`, to return a truthy value when
+ * evaluated in the page's context.
+ *
+ * @example
+ * {@link Page.waitForFunction} can be used to observe a viewport size change:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * const watchDog = page.waitForFunction('window.innerWidth < 100');
+ * await page.setViewport({width: 50, height: 50});
+ * await watchDog;
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @example
+ * Arguments can be passed from Node.js to `pageFunction`:
+ *
+ * ```ts
+ * const selector = '.foo';
+ * await page.waitForFunction(
+ * selector => !!document.querySelector(selector),
+ * {},
+ * selector
+ * );
+ * ```
+ *
+ * @example
+ * The provided `pageFunction` can be asynchronous:
+ *
+ * ```ts
+ * const username = 'github-username';
+ * await page.waitForFunction(
+ * async username => {
+ * const githubResponse = await fetch(
+ * `https://api.github.com/users/${username}`
+ * );
+ * const githubUser = await githubResponse.json();
+ * // show the avatar
+ * const img = document.createElement('img');
+ * img.src = githubUser.avatar_url;
+ * // wait 3 seconds
+ * await new Promise((resolve, reject) => setTimeout(resolve, 3000));
+ * img.remove();
+ * },
+ * {},
+ * username
+ * );
+ * ```
+ *
+ * @param pageFunction - Function to be evaluated in browser context until it returns a
+ * truthy value.
+ * @param options - Options for configuring waiting behavior.
+ */
+ waitForFunction<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ options?: FrameWaitForFunctionOptions,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ return this.mainFrame().waitForFunction(pageFunction, options, ...args);
+ }
+
+ /**
+ * This method is typically coupled with an action that triggers a device
+ * request from an api such as WebBluetooth.
+ *
+ * :::caution
+ *
+ * This must be called before the device request is made. It will not return a
+ * currently active device prompt.
+ *
+ * :::
+ *
+ * @example
+ *
+ * ```ts
+ * const [devicePrompt] = Promise.all([
+ * page.waitForDevicePrompt(),
+ * page.click('#connect-bluetooth'),
+ * ]);
+ * await devicePrompt.select(
+ * await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+ * );
+ * ```
+ */
+ abstract waitForDevicePrompt(
+ options?: WaitTimeoutOptions
+ ): Promise<DeviceRequestPrompt>;
+
+ /** @internal */
+ [disposeSymbol](): void {
+ return void this.close().catch(debugError);
+ }
+
+ /** @internal */
+ [asyncDisposeSymbol](): Promise<void> {
+ return this.close();
+ }
+}
+
+/**
+ * @internal
+ */
+export const supportedMetrics = new Set<string>([
+ 'Timestamp',
+ 'Documents',
+ 'Frames',
+ 'JSEventListeners',
+ 'Nodes',
+ 'LayoutCount',
+ 'RecalcStyleCount',
+ 'LayoutDuration',
+ 'RecalcStyleDuration',
+ 'ScriptDuration',
+ 'TaskDuration',
+ 'JSHeapUsedSize',
+ 'JSHeapTotalSize',
+]);
+
+/** @see https://w3c.github.io/webdriver-bidi/#normalize-rect */
+function normalizeRectangle<BoundingBoxType extends BoundingBox>(
+ clip: Readonly<BoundingBoxType>
+): BoundingBoxType {
+ return {
+ ...clip,
+ ...(clip.width < 0
+ ? {
+ x: clip.x + clip.width,
+ width: -clip.width,
+ }
+ : {
+ x: clip.x,
+ width: clip.width,
+ }),
+ ...(clip.height < 0
+ ? {
+ y: clip.y + clip.height,
+ height: -clip.height,
+ }
+ : {
+ y: clip.y,
+ height: clip.height,
+ }),
+ };
+}
+
+function roundRectangle<BoundingBoxType extends BoundingBox>(
+ clip: Readonly<BoundingBoxType>
+): BoundingBoxType {
+ const x = Math.round(clip.x);
+ const y = Math.round(clip.y);
+ const width = Math.round(clip.width + clip.x - x);
+ const height = Math.round(clip.height + clip.y - y);
+ return {...clip, x, y, width, height};
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Realm.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Realm.ts
new file mode 100644
index 0000000000..eee1f2c1dd
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Realm.ts
@@ -0,0 +1,104 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {TimeoutSettings} from '../common/TimeoutSettings.js';
+import type {
+ EvaluateFunc,
+ HandleFor,
+ InnerLazyParams,
+} from '../common/types.js';
+import {TaskManager, WaitTask} from '../common/WaitTask.js';
+import {disposeSymbol} from '../util/disposable.js';
+
+import type {ElementHandle} from './ElementHandle.js';
+import type {Environment} from './Environment.js';
+import type {JSHandle} from './JSHandle.js';
+
+/**
+ * @internal
+ */
+export abstract class Realm implements Disposable {
+ protected readonly timeoutSettings: TimeoutSettings;
+ readonly taskManager = new TaskManager();
+
+ constructor(timeoutSettings: TimeoutSettings) {
+ this.timeoutSettings = timeoutSettings;
+ }
+
+ abstract get environment(): Environment;
+
+ abstract adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
+ abstract transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
+ abstract evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
+ abstract evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>>;
+
+ async waitForFunction<
+ Params extends unknown[],
+ Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
+ InnerLazyParams<Params>
+ >,
+ >(
+ pageFunction: Func | string,
+ options: {
+ polling?: 'raf' | 'mutation' | number;
+ timeout?: number;
+ root?: ElementHandle<Node>;
+ signal?: AbortSignal;
+ } = {},
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ const {
+ polling = 'raf',
+ timeout = this.timeoutSettings.timeout(),
+ root,
+ signal,
+ } = options;
+ if (typeof polling === 'number' && polling < 0) {
+ throw new Error('Cannot poll with non-positive interval');
+ }
+ const waitTask = new WaitTask(
+ this,
+ {
+ polling,
+ root,
+ timeout,
+ signal,
+ },
+ pageFunction as unknown as
+ | ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
+ | string,
+ ...args
+ );
+ return await waitTask.result;
+ }
+
+ abstract adoptBackendNode(backendNodeId?: number): Promise<JSHandle<Node>>;
+
+ get disposed(): boolean {
+ return this.#disposed;
+ }
+
+ #disposed = false;
+ /** @internal */
+ [disposeSymbol](): void {
+ this.#disposed = true;
+ this.taskManager.terminateAll(
+ new Error('waitForFunction failed: frame got detached.')
+ );
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Target.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Target.ts
new file mode 100644
index 0000000000..f91b91df12
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Target.ts
@@ -0,0 +1,95 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Browser} from './Browser.js';
+import type {BrowserContext} from './BrowserContext.js';
+import type {CDPSession} from './CDPSession.js';
+import type {Page} from './Page.js';
+import type {WebWorker} from './WebWorker.js';
+
+/**
+ * @public
+ */
+export enum TargetType {
+ PAGE = 'page',
+ BACKGROUND_PAGE = 'background_page',
+ SERVICE_WORKER = 'service_worker',
+ SHARED_WORKER = 'shared_worker',
+ BROWSER = 'browser',
+ WEBVIEW = 'webview',
+ OTHER = 'other',
+ /**
+ * @internal
+ */
+ TAB = 'tab',
+}
+
+/**
+ * Target represents a
+ * {@link https://chromedevtools.github.io/devtools-protocol/tot/Target/ | CDP target}.
+ * In CDP a target is something that can be debugged such a frame, a page or a
+ * worker.
+ * @public
+ */
+export abstract class Target {
+ /**
+ * @internal
+ */
+ protected constructor() {}
+
+ /**
+ * If the target is not of type `"service_worker"` or `"shared_worker"`, returns `null`.
+ */
+ async worker(): Promise<WebWorker | null> {
+ return null;
+ }
+
+ /**
+ * If the target is not of type `"page"`, `"webview"` or `"background_page"`,
+ * returns `null`.
+ */
+ async page(): Promise<Page | null> {
+ return null;
+ }
+
+ /**
+ * Forcefully creates a page for a target of any type. It is useful if you
+ * want to handle a CDP target of type `other` as a page. If you deal with a
+ * regular page target, use {@link Target.page}.
+ */
+ abstract asPage(): Promise<Page>;
+
+ abstract url(): string;
+
+ /**
+ * Creates a Chrome Devtools Protocol session attached to the target.
+ */
+ abstract createCDPSession(): Promise<CDPSession>;
+
+ /**
+ * Identifies what kind of target this is.
+ *
+ * @remarks
+ *
+ * See {@link https://developer.chrome.com/extensions/background_pages | docs} for more info about background pages.
+ */
+ abstract type(): TargetType;
+
+ /**
+ * Get the browser the target belongs to.
+ */
+ abstract browser(): Browser;
+
+ /**
+ * Get the browser context the target belongs to.
+ */
+ abstract browserContext(): BrowserContext;
+
+ /**
+ * Get the target that opened this target. Top-level targets return `null`.
+ */
+ abstract opener(): Target | undefined;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts
new file mode 100644
index 0000000000..4de287f146
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/WebWorker.ts
@@ -0,0 +1,134 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {EventEmitter, type EventType} from '../common/EventEmitter.js';
+import {TimeoutSettings} from '../common/TimeoutSettings.js';
+import type {EvaluateFunc, HandleFor} from '../common/types.js';
+import {withSourcePuppeteerURLIfNone} from '../common/util.js';
+
+import type {CDPSession} from './CDPSession.js';
+import type {Realm} from './Realm.js';
+
+/**
+ * This class represents a
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker}.
+ *
+ * @remarks
+ * The events `workercreated` and `workerdestroyed` are emitted on the page
+ * object to signal the worker lifecycle.
+ *
+ * @example
+ *
+ * ```ts
+ * page.on('workercreated', worker =>
+ * console.log('Worker created: ' + worker.url())
+ * );
+ * page.on('workerdestroyed', worker =>
+ * console.log('Worker destroyed: ' + worker.url())
+ * );
+ *
+ * console.log('Current workers:');
+ * for (const worker of page.workers()) {
+ * console.log(' ' + worker.url());
+ * }
+ * ```
+ *
+ * @public
+ */
+export abstract class WebWorker extends EventEmitter<
+ Record<EventType, unknown>
+> {
+ /**
+ * @internal
+ */
+ readonly timeoutSettings = new TimeoutSettings();
+
+ readonly #url: string;
+
+ /**
+ * @internal
+ */
+ constructor(url: string) {
+ super();
+
+ this.#url = url;
+ }
+
+ /**
+ * @internal
+ */
+ abstract mainRealm(): Realm;
+
+ /**
+ * The URL of this web worker.
+ */
+ url(): string {
+ return this.#url;
+ }
+
+ /**
+ * The CDP session client the WebWorker belongs to.
+ */
+ abstract get client(): CDPSession;
+
+ /**
+ * Evaluates a given function in the {@link WebWorker | worker}.
+ *
+ * @remarks If the given function returns a promise,
+ * {@link WebWorker.evaluate | evaluate} will wait for the promise to resolve.
+ *
+ * As a rule of thumb, if the return value of the given function is more
+ * complicated than a JSON object (e.g. most classes), then
+ * {@link WebWorker.evaluate | evaluate} will _likely_ return some truncated
+ * value (or `{}`). This is because we are not returning the actual return
+ * value, but a deserialized version as a result of transferring the return
+ * value through a protocol to Puppeteer.
+ *
+ * In general, you should use
+ * {@link WebWorker.evaluateHandle | evaluateHandle} if
+ * {@link WebWorker.evaluate | evaluate} cannot serialize the return value
+ * properly or you need a mutable {@link JSHandle | handle} to the return
+ * object.
+ *
+ * @param func - Function to be evaluated.
+ * @param args - Arguments to pass into `func`.
+ * @returns The result of `func`.
+ */
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(func: Func | string, ...args: Params): Promise<Awaited<ReturnType<Func>>> {
+ func = withSourcePuppeteerURLIfNone(this.evaluate.name, func);
+ return await this.mainRealm().evaluate(func, ...args);
+ }
+
+ /**
+ * Evaluates a given function in the {@link WebWorker | worker}.
+ *
+ * @remarks If the given function returns a promise,
+ * {@link WebWorker.evaluate | evaluate} will wait for the promise to resolve.
+ *
+ * In general, you should use
+ * {@link WebWorker.evaluateHandle | evaluateHandle} if
+ * {@link WebWorker.evaluate | evaluate} cannot serialize the return value
+ * properly or you need a mutable {@link JSHandle | handle} to the return
+ * object.
+ *
+ * @param func - Function to be evaluated.
+ * @param args - Arguments to pass into `func`.
+ * @returns A {@link JSHandle | handle} to the return value of `func`.
+ */
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ func: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ func = withSourcePuppeteerURLIfNone(this.evaluateHandle.name, func);
+ return await this.mainRealm().evaluateHandle(func, ...args);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/api.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/api.ts
new file mode 100644
index 0000000000..d2bf832a6d
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/api.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './Browser.js';
+export * from './BrowserContext.js';
+export * from './CDPSession.js';
+export * from './Dialog.js';
+export * from './ElementHandle.js';
+export * from './Environment.js';
+export * from './Frame.js';
+export * from './HTTPRequest.js';
+export * from './HTTPResponse.js';
+export * from './Input.js';
+export * from './JSHandle.js';
+export * from './Page.js';
+export * from './Realm.js';
+export * from './Target.js';
+export * from './WebWorker.js';
+export * from './locators/locators.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts
new file mode 100644
index 0000000000..7bec11e38e
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/locators/locators.ts
@@ -0,0 +1,1088 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {
+ Observable,
+ OperatorFunction,
+} from '../../../third_party/rxjs/rxjs.js';
+import {
+ EMPTY,
+ catchError,
+ defaultIfEmpty,
+ defer,
+ filter,
+ first,
+ firstValueFrom,
+ from,
+ fromEvent,
+ identity,
+ ignoreElements,
+ map,
+ merge,
+ mergeMap,
+ noop,
+ pipe,
+ race,
+ raceWith,
+ retry,
+ tap,
+ throwIfEmpty,
+} from '../../../third_party/rxjs/rxjs.js';
+import type {EventType} from '../../common/EventEmitter.js';
+import {EventEmitter} from '../../common/EventEmitter.js';
+import type {Awaitable, HandleFor, NodeFor} from '../../common/types.js';
+import {debugError, timeout} from '../../common/util.js';
+import type {
+ BoundingBox,
+ ClickOptions,
+ ElementHandle,
+} from '../ElementHandle.js';
+import type {Frame} from '../Frame.js';
+import type {Page} from '../Page.js';
+
+/**
+ * @public
+ */
+export type VisibilityOption = 'hidden' | 'visible' | null;
+/**
+ * @public
+ */
+export interface LocatorOptions {
+ /**
+ * Whether to wait for the element to be `visible` or `hidden`. `null` to
+ * disable visibility checks.
+ */
+ visibility: VisibilityOption;
+ /**
+ * Total timeout for the entire locator operation.
+ *
+ * Pass `0` to disable timeout.
+ *
+ * @defaultValue `Page.getDefaultTimeout()`
+ */
+ timeout: number;
+ /**
+ * Whether to scroll the element into viewport if not in the viewprot already.
+ * @defaultValue `true`
+ */
+ ensureElementIsInTheViewport: boolean;
+ /**
+ * Whether to wait for input elements to become enabled before the action.
+ * Applicable to `click` and `fill` actions.
+ * @defaultValue `true`
+ */
+ waitForEnabled: boolean;
+ /**
+ * Whether to wait for the element's bounding box to be same between two
+ * animation frames.
+ * @defaultValue `true`
+ */
+ waitForStableBoundingBox: boolean;
+}
+/**
+ * @public
+ */
+export interface ActionOptions {
+ signal?: AbortSignal;
+}
+/**
+ * @public
+ */
+export type LocatorClickOptions = ClickOptions & ActionOptions;
+/**
+ * @public
+ */
+export interface LocatorScrollOptions extends ActionOptions {
+ scrollTop?: number;
+ scrollLeft?: number;
+}
+/**
+ * All the events that a locator instance may emit.
+ *
+ * @public
+ */
+export enum LocatorEvent {
+ /**
+ * Emitted every time before the locator performs an action on the located element(s).
+ */
+ Action = 'action',
+}
+export {
+ /**
+ * @deprecated Use {@link LocatorEvent}.
+ */
+ LocatorEvent as LocatorEmittedEvents,
+};
+/**
+ * @public
+ */
+export interface LocatorEvents extends Record<EventType, unknown> {
+ [LocatorEvent.Action]: undefined;
+}
+export type {
+ /**
+ * @deprecated Use {@link LocatorEvents}.
+ */
+ LocatorEvents as LocatorEventObject,
+};
+/**
+ * Locators describe a strategy of locating objects and performing an action on
+ * them. If the action fails because the object is not ready for the action, the
+ * whole operation is retried. Various preconditions for a successful action are
+ * checked automatically.
+ *
+ * @public
+ */
+export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
+ /**
+ * Creates a race between multiple locators but ensures that only a single one
+ * acts.
+ *
+ * @public
+ */
+ static race<Locators extends readonly unknown[] | []>(
+ locators: Locators
+ ): Locator<AwaitedLocator<Locators[number]>> {
+ return RaceLocator.create(locators);
+ }
+
+ /**
+ * Used for nominally typing {@link Locator}.
+ */
+ declare _?: T;
+
+ /**
+ * @internal
+ */
+ protected visibility: VisibilityOption = null;
+ /**
+ * @internal
+ */
+ protected _timeout = 30000;
+ #ensureElementIsInTheViewport = true;
+ #waitForEnabled = true;
+ #waitForStableBoundingBox = true;
+
+ /**
+ * @internal
+ */
+ protected operators = {
+ conditions: (
+ conditions: Array<Action<T, never>>,
+ signal?: AbortSignal
+ ): OperatorFunction<HandleFor<T>, HandleFor<T>> => {
+ return mergeMap((handle: HandleFor<T>) => {
+ return merge(
+ ...conditions.map(condition => {
+ return condition(handle, signal);
+ })
+ ).pipe(defaultIfEmpty(handle));
+ });
+ },
+ retryAndRaceWithSignalAndTimer: <T>(
+ signal?: AbortSignal
+ ): OperatorFunction<T, T> => {
+ const candidates = [];
+ if (signal) {
+ candidates.push(
+ fromEvent(signal, 'abort').pipe(
+ map(() => {
+ throw signal.reason;
+ })
+ )
+ );
+ }
+ candidates.push(timeout(this._timeout));
+ return pipe(
+ retry({delay: RETRY_DELAY}),
+ raceWith<T, never[]>(...candidates)
+ );
+ },
+ };
+
+ // Determines when the locator will timeout for actions.
+ get timeout(): number {
+ return this._timeout;
+ }
+
+ setTimeout(timeout: number): Locator<T> {
+ const locator = this._clone();
+ locator._timeout = timeout;
+ return locator;
+ }
+
+ setVisibility<NodeType extends Node>(
+ this: Locator<NodeType>,
+ visibility: VisibilityOption
+ ): Locator<NodeType> {
+ const locator = this._clone();
+ locator.visibility = visibility;
+ return locator;
+ }
+
+ setWaitForEnabled<NodeType extends Node>(
+ this: Locator<NodeType>,
+ value: boolean
+ ): Locator<NodeType> {
+ const locator = this._clone();
+ locator.#waitForEnabled = value;
+ return locator;
+ }
+
+ setEnsureElementIsInTheViewport<ElementType extends Element>(
+ this: Locator<ElementType>,
+ value: boolean
+ ): Locator<ElementType> {
+ const locator = this._clone();
+ locator.#ensureElementIsInTheViewport = value;
+ return locator;
+ }
+
+ setWaitForStableBoundingBox<ElementType extends Element>(
+ this: Locator<ElementType>,
+ value: boolean
+ ): Locator<ElementType> {
+ const locator = this._clone();
+ locator.#waitForStableBoundingBox = value;
+ return locator;
+ }
+
+ /**
+ * @internal
+ */
+ copyOptions<T>(locator: Locator<T>): this {
+ this._timeout = locator._timeout;
+ this.visibility = locator.visibility;
+ this.#waitForEnabled = locator.#waitForEnabled;
+ this.#ensureElementIsInTheViewport = locator.#ensureElementIsInTheViewport;
+ this.#waitForStableBoundingBox = locator.#waitForStableBoundingBox;
+ return this;
+ }
+
+ /**
+ * If the element has a "disabled" property, wait for the element to be
+ * enabled.
+ */
+ #waitForEnabledIfNeeded = <ElementType extends Node>(
+ handle: HandleFor<ElementType>,
+ signal?: AbortSignal
+ ): Observable<never> => {
+ if (!this.#waitForEnabled) {
+ return EMPTY;
+ }
+ return from(
+ handle.frame.waitForFunction(
+ element => {
+ if (!(element instanceof HTMLElement)) {
+ return true;
+ }
+ const isNativeFormControl = [
+ 'BUTTON',
+ 'INPUT',
+ 'SELECT',
+ 'TEXTAREA',
+ 'OPTION',
+ 'OPTGROUP',
+ ].includes(element.nodeName);
+ return !isNativeFormControl || !element.hasAttribute('disabled');
+ },
+ {
+ timeout: this._timeout,
+ signal,
+ },
+ handle
+ )
+ ).pipe(ignoreElements());
+ };
+
+ /**
+ * Compares the bounding box of the element for two consecutive animation
+ * frames and waits till they are the same.
+ */
+ #waitForStableBoundingBoxIfNeeded = <ElementType extends Element>(
+ handle: HandleFor<ElementType>
+ ): Observable<never> => {
+ if (!this.#waitForStableBoundingBox) {
+ return EMPTY;
+ }
+ return defer(() => {
+ // Note we don't use waitForFunction because that relies on RAF.
+ return from(
+ handle.evaluate(element => {
+ return new Promise<[BoundingBox, BoundingBox]>(resolve => {
+ window.requestAnimationFrame(() => {
+ const rect1 = element.getBoundingClientRect();
+ window.requestAnimationFrame(() => {
+ const rect2 = element.getBoundingClientRect();
+ resolve([
+ {
+ x: rect1.x,
+ y: rect1.y,
+ width: rect1.width,
+ height: rect1.height,
+ },
+ {
+ x: rect2.x,
+ y: rect2.y,
+ width: rect2.width,
+ height: rect2.height,
+ },
+ ]);
+ });
+ });
+ });
+ })
+ );
+ }).pipe(
+ first(([rect1, rect2]) => {
+ return (
+ rect1.x === rect2.x &&
+ rect1.y === rect2.y &&
+ rect1.width === rect2.width &&
+ rect1.height === rect2.height
+ );
+ }),
+ retry({delay: RETRY_DELAY}),
+ ignoreElements()
+ );
+ };
+
+ /**
+ * Checks if the element is in the viewport and auto-scrolls it if it is not.
+ */
+ #ensureElementIsInTheViewportIfNeeded = <ElementType extends Element>(
+ handle: HandleFor<ElementType>
+ ): Observable<never> => {
+ if (!this.#ensureElementIsInTheViewport) {
+ return EMPTY;
+ }
+ return from(handle.isIntersectingViewport({threshold: 0})).pipe(
+ filter(isIntersectingViewport => {
+ return !isIntersectingViewport;
+ }),
+ mergeMap(() => {
+ return from(handle.scrollIntoView());
+ }),
+ mergeMap(() => {
+ return defer(() => {
+ return from(handle.isIntersectingViewport({threshold: 0}));
+ }).pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
+ })
+ );
+ };
+
+ #click<ElementType extends Element>(
+ this: Locator<ElementType>,
+ options?: Readonly<LocatorClickOptions>
+ ): Observable<void> {
+ const signal = options?.signal;
+ return this._wait(options).pipe(
+ this.operators.conditions(
+ [
+ this.#ensureElementIsInTheViewportIfNeeded,
+ this.#waitForStableBoundingBoxIfNeeded,
+ this.#waitForEnabledIfNeeded,
+ ],
+ signal
+ ),
+ tap(() => {
+ return this.emit(LocatorEvent.Action, undefined);
+ }),
+ mergeMap(handle => {
+ return from(handle.click(options)).pipe(
+ catchError(err => {
+ void handle.dispose().catch(debugError);
+ throw err;
+ })
+ );
+ }),
+ this.operators.retryAndRaceWithSignalAndTimer(signal)
+ );
+ }
+
+ #fill<ElementType extends Element>(
+ this: Locator<ElementType>,
+ value: string,
+ options?: Readonly<ActionOptions>
+ ): Observable<void> {
+ const signal = options?.signal;
+ return this._wait(options).pipe(
+ this.operators.conditions(
+ [
+ this.#ensureElementIsInTheViewportIfNeeded,
+ this.#waitForStableBoundingBoxIfNeeded,
+ this.#waitForEnabledIfNeeded,
+ ],
+ signal
+ ),
+ tap(() => {
+ return this.emit(LocatorEvent.Action, undefined);
+ }),
+ mergeMap(handle => {
+ return from(
+ (handle as unknown as ElementHandle<HTMLElement>).evaluate(el => {
+ if (el instanceof HTMLSelectElement) {
+ return 'select';
+ }
+ if (el instanceof HTMLTextAreaElement) {
+ return 'typeable-input';
+ }
+ if (el instanceof HTMLInputElement) {
+ if (
+ new Set([
+ 'textarea',
+ 'text',
+ 'url',
+ 'tel',
+ 'search',
+ 'password',
+ 'number',
+ 'email',
+ ]).has(el.type)
+ ) {
+ return 'typeable-input';
+ } else {
+ return 'other-input';
+ }
+ }
+
+ if (el.isContentEditable) {
+ return 'contenteditable';
+ }
+
+ return 'unknown';
+ })
+ )
+ .pipe(
+ mergeMap(inputType => {
+ switch (inputType) {
+ case 'select':
+ return from(handle.select(value).then(noop));
+ case 'contenteditable':
+ case 'typeable-input':
+ return from(
+ (
+ handle as unknown as ElementHandle<HTMLInputElement>
+ ).evaluate((input, newValue) => {
+ const currentValue = input.isContentEditable
+ ? input.innerText
+ : input.value;
+
+ // Clear the input if the current value does not match the filled
+ // out value.
+ if (
+ newValue.length <= currentValue.length ||
+ !newValue.startsWith(input.value)
+ ) {
+ if (input.isContentEditable) {
+ input.innerText = '';
+ } else {
+ input.value = '';
+ }
+ return newValue;
+ }
+ const originalValue = input.isContentEditable
+ ? input.innerText
+ : input.value;
+
+ // If the value is partially filled out, only type the rest. Move
+ // cursor to the end of the common prefix.
+ if (input.isContentEditable) {
+ input.innerText = '';
+ input.innerText = originalValue;
+ } else {
+ input.value = '';
+ input.value = originalValue;
+ }
+ return newValue.substring(originalValue.length);
+ }, value)
+ ).pipe(
+ mergeMap(textToType => {
+ return from(handle.type(textToType));
+ })
+ );
+ case 'other-input':
+ return from(handle.focus()).pipe(
+ mergeMap(() => {
+ return from(
+ handle.evaluate((input, value) => {
+ (input as HTMLInputElement).value = value;
+ input.dispatchEvent(
+ new Event('input', {bubbles: true})
+ );
+ input.dispatchEvent(
+ new Event('change', {bubbles: true})
+ );
+ }, value)
+ );
+ })
+ );
+ case 'unknown':
+ throw new Error(`Element cannot be filled out.`);
+ }
+ })
+ )
+ .pipe(
+ catchError(err => {
+ void handle.dispose().catch(debugError);
+ throw err;
+ })
+ );
+ }),
+ this.operators.retryAndRaceWithSignalAndTimer(signal)
+ );
+ }
+
+ #hover<ElementType extends Element>(
+ this: Locator<ElementType>,
+ options?: Readonly<ActionOptions>
+ ): Observable<void> {
+ const signal = options?.signal;
+ return this._wait(options).pipe(
+ this.operators.conditions(
+ [
+ this.#ensureElementIsInTheViewportIfNeeded,
+ this.#waitForStableBoundingBoxIfNeeded,
+ ],
+ signal
+ ),
+ tap(() => {
+ return this.emit(LocatorEvent.Action, undefined);
+ }),
+ mergeMap(handle => {
+ return from(handle.hover()).pipe(
+ catchError(err => {
+ void handle.dispose().catch(debugError);
+ throw err;
+ })
+ );
+ }),
+ this.operators.retryAndRaceWithSignalAndTimer(signal)
+ );
+ }
+
+ #scroll<ElementType extends Element>(
+ this: Locator<ElementType>,
+ options?: Readonly<LocatorScrollOptions>
+ ): Observable<void> {
+ const signal = options?.signal;
+ return this._wait(options).pipe(
+ this.operators.conditions(
+ [
+ this.#ensureElementIsInTheViewportIfNeeded,
+ this.#waitForStableBoundingBoxIfNeeded,
+ ],
+ signal
+ ),
+ tap(() => {
+ return this.emit(LocatorEvent.Action, undefined);
+ }),
+ mergeMap(handle => {
+ return from(
+ handle.evaluate(
+ (el, scrollTop, scrollLeft) => {
+ if (scrollTop !== undefined) {
+ el.scrollTop = scrollTop;
+ }
+ if (scrollLeft !== undefined) {
+ el.scrollLeft = scrollLeft;
+ }
+ },
+ options?.scrollTop,
+ options?.scrollLeft
+ )
+ ).pipe(
+ catchError(err => {
+ void handle.dispose().catch(debugError);
+ throw err;
+ })
+ );
+ }),
+ this.operators.retryAndRaceWithSignalAndTimer(signal)
+ );
+ }
+
+ /**
+ * @internal
+ */
+ abstract _clone(): Locator<T>;
+
+ /**
+ * @internal
+ */
+ abstract _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>>;
+
+ /**
+ * Clones the locator.
+ */
+ clone(): Locator<T> {
+ return this._clone();
+ }
+
+ /**
+ * Waits for the locator to get a handle from the page.
+ *
+ * @public
+ */
+ async waitHandle(options?: Readonly<ActionOptions>): Promise<HandleFor<T>> {
+ return await firstValueFrom(
+ this._wait(options).pipe(
+ this.operators.retryAndRaceWithSignalAndTimer(options?.signal)
+ )
+ );
+ }
+
+ /**
+ * Waits for the locator to get the serialized value from the page.
+ *
+ * Note this requires the value to be JSON-serializable.
+ *
+ * @public
+ */
+ async wait(options?: Readonly<ActionOptions>): Promise<T> {
+ using handle = await this.waitHandle(options);
+ return await handle.jsonValue();
+ }
+
+ /**
+ * Maps the locator using the provided mapper.
+ *
+ * @public
+ */
+ map<To>(mapper: Mapper<T, To>): Locator<To> {
+ return new MappedLocator(this._clone(), handle => {
+ // SAFETY: TypeScript cannot deduce the type.
+ return (handle as any).evaluateHandle(mapper);
+ });
+ }
+
+ /**
+ * Creates an expectation that is evaluated against located values.
+ *
+ * If the expectations do not match, then the locator will retry.
+ *
+ * @public
+ */
+ filter<S extends T>(predicate: Predicate<T, S>): Locator<S> {
+ return new FilteredLocator(this._clone(), async (handle, signal) => {
+ await (handle as ElementHandle<Node>).frame.waitForFunction(
+ predicate,
+ {signal, timeout: this._timeout},
+ handle
+ );
+ return true;
+ });
+ }
+
+ /**
+ * Creates an expectation that is evaluated against located handles.
+ *
+ * If the expectations do not match, then the locator will retry.
+ *
+ * @internal
+ */
+ filterHandle<S extends T>(
+ predicate: Predicate<HandleFor<T>, HandleFor<S>>
+ ): Locator<S> {
+ return new FilteredLocator(this._clone(), predicate);
+ }
+
+ /**
+ * Maps the locator using the provided mapper.
+ *
+ * @internal
+ */
+ mapHandle<To>(mapper: HandleMapper<T, To>): Locator<To> {
+ return new MappedLocator(this._clone(), mapper);
+ }
+
+ click<ElementType extends Element>(
+ this: Locator<ElementType>,
+ options?: Readonly<LocatorClickOptions>
+ ): Promise<void> {
+ return firstValueFrom(this.#click(options));
+ }
+
+ /**
+ * Fills out the input identified by the locator using the provided value. The
+ * type of the input is determined at runtime and the appropriate fill-out
+ * method is chosen based on the type. contenteditable, selector, inputs are
+ * supported.
+ */
+ fill<ElementType extends Element>(
+ this: Locator<ElementType>,
+ value: string,
+ options?: Readonly<ActionOptions>
+ ): Promise<void> {
+ return firstValueFrom(this.#fill(value, options));
+ }
+
+ hover<ElementType extends Element>(
+ this: Locator<ElementType>,
+ options?: Readonly<ActionOptions>
+ ): Promise<void> {
+ return firstValueFrom(this.#hover(options));
+ }
+
+ scroll<ElementType extends Element>(
+ this: Locator<ElementType>,
+ options?: Readonly<LocatorScrollOptions>
+ ): Promise<void> {
+ return firstValueFrom(this.#scroll(options));
+ }
+}
+
+/**
+ * @internal
+ */
+export class FunctionLocator<T> extends Locator<T> {
+ static create<Ret>(
+ pageOrFrame: Page | Frame,
+ func: () => Awaitable<Ret>
+ ): Locator<Ret> {
+ return new FunctionLocator<Ret>(pageOrFrame, func).setTimeout(
+ 'getDefaultTimeout' in pageOrFrame
+ ? pageOrFrame.getDefaultTimeout()
+ : pageOrFrame.page().getDefaultTimeout()
+ );
+ }
+
+ #pageOrFrame: Page | Frame;
+ #func: () => Awaitable<T>;
+
+ private constructor(pageOrFrame: Page | Frame, func: () => Awaitable<T>) {
+ super();
+
+ this.#pageOrFrame = pageOrFrame;
+ this.#func = func;
+ }
+
+ override _clone(): FunctionLocator<T> {
+ return new FunctionLocator(this.#pageOrFrame, this.#func);
+ }
+
+ _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
+ const signal = options?.signal;
+ return defer(() => {
+ return from(
+ this.#pageOrFrame.waitForFunction(this.#func, {
+ timeout: this.timeout,
+ signal,
+ })
+ );
+ }).pipe(throwIfEmpty());
+ }
+}
+
+/**
+ * @public
+ */
+export type Predicate<From, To extends From = From> =
+ | ((value: From) => value is To)
+ | ((value: From) => Awaitable<boolean>);
+/**
+ * @internal
+ */
+export type HandlePredicate<From, To extends From = From> =
+ | ((value: HandleFor<From>, signal?: AbortSignal) => value is HandleFor<To>)
+ | ((value: HandleFor<From>, signal?: AbortSignal) => Awaitable<boolean>);
+
+/**
+ * @internal
+ */
+export abstract class DelegatedLocator<T, U> extends Locator<U> {
+ #delegate: Locator<T>;
+
+ constructor(delegate: Locator<T>) {
+ super();
+
+ this.#delegate = delegate;
+ this.copyOptions(this.#delegate);
+ }
+
+ protected get delegate(): Locator<T> {
+ return this.#delegate;
+ }
+
+ override setTimeout(timeout: number): DelegatedLocator<T, U> {
+ const locator = super.setTimeout(timeout) as DelegatedLocator<T, U>;
+ locator.#delegate = this.#delegate.setTimeout(timeout);
+ return locator;
+ }
+
+ override setVisibility<ValueType extends Node, NodeType extends Node>(
+ this: DelegatedLocator<ValueType, NodeType>,
+ visibility: VisibilityOption
+ ): DelegatedLocator<ValueType, NodeType> {
+ const locator = super.setVisibility<NodeType>(
+ visibility
+ ) as DelegatedLocator<ValueType, NodeType>;
+ locator.#delegate = locator.#delegate.setVisibility<ValueType>(visibility);
+ return locator;
+ }
+
+ override setWaitForEnabled<ValueType extends Node, NodeType extends Node>(
+ this: DelegatedLocator<ValueType, NodeType>,
+ value: boolean
+ ): DelegatedLocator<ValueType, NodeType> {
+ const locator = super.setWaitForEnabled<NodeType>(
+ value
+ ) as DelegatedLocator<ValueType, NodeType>;
+ locator.#delegate = this.#delegate.setWaitForEnabled(value);
+ return locator;
+ }
+
+ override setEnsureElementIsInTheViewport<
+ ValueType extends Element,
+ ElementType extends Element,
+ >(
+ this: DelegatedLocator<ValueType, ElementType>,
+ value: boolean
+ ): DelegatedLocator<ValueType, ElementType> {
+ const locator = super.setEnsureElementIsInTheViewport<ElementType>(
+ value
+ ) as DelegatedLocator<ValueType, ElementType>;
+ locator.#delegate = this.#delegate.setEnsureElementIsInTheViewport(value);
+ return locator;
+ }
+
+ override setWaitForStableBoundingBox<
+ ValueType extends Element,
+ ElementType extends Element,
+ >(
+ this: DelegatedLocator<ValueType, ElementType>,
+ value: boolean
+ ): DelegatedLocator<ValueType, ElementType> {
+ const locator = super.setWaitForStableBoundingBox<ElementType>(
+ value
+ ) as DelegatedLocator<ValueType, ElementType>;
+ locator.#delegate = this.#delegate.setWaitForStableBoundingBox(value);
+ return locator;
+ }
+
+ abstract override _clone(): DelegatedLocator<T, U>;
+ abstract override _wait(): Observable<HandleFor<U>>;
+}
+
+/**
+ * @internal
+ */
+export class FilteredLocator<From, To extends From> extends DelegatedLocator<
+ From,
+ To
+> {
+ #predicate: HandlePredicate<From, To>;
+
+ constructor(base: Locator<From>, predicate: HandlePredicate<From, To>) {
+ super(base);
+ this.#predicate = predicate;
+ }
+
+ override _clone(): FilteredLocator<From, To> {
+ return new FilteredLocator(
+ this.delegate.clone(),
+ this.#predicate
+ ).copyOptions(this);
+ }
+
+ override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
+ return this.delegate._wait(options).pipe(
+ mergeMap(handle => {
+ return from(
+ Promise.resolve(this.#predicate(handle, options?.signal))
+ ).pipe(
+ filter(value => {
+ return value;
+ }),
+ map(() => {
+ // SAFETY: It passed the predicate, so this is correct.
+ return handle as HandleFor<To>;
+ })
+ );
+ }),
+ throwIfEmpty()
+ );
+ }
+}
+
+/**
+ * @public
+ */
+export type Mapper<From, To> = (value: From) => Awaitable<To>;
+/**
+ * @internal
+ */
+export type HandleMapper<From, To> = (
+ value: HandleFor<From>,
+ signal?: AbortSignal
+) => Awaitable<HandleFor<To>>;
+/**
+ * @internal
+ */
+export class MappedLocator<From, To> extends DelegatedLocator<From, To> {
+ #mapper: HandleMapper<From, To>;
+
+ constructor(base: Locator<From>, mapper: HandleMapper<From, To>) {
+ super(base);
+ this.#mapper = mapper;
+ }
+
+ override _clone(): MappedLocator<From, To> {
+ return new MappedLocator(this.delegate.clone(), this.#mapper).copyOptions(
+ this
+ );
+ }
+
+ override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
+ return this.delegate._wait(options).pipe(
+ mergeMap(handle => {
+ return from(Promise.resolve(this.#mapper(handle, options?.signal)));
+ })
+ );
+ }
+}
+
+/**
+ * @internal
+ */
+export type Action<T, U> = (
+ element: HandleFor<T>,
+ signal?: AbortSignal
+) => Observable<U>;
+/**
+ * @internal
+ */
+export class NodeLocator<T extends Node> extends Locator<T> {
+ static create<Selector extends string>(
+ pageOrFrame: Page | Frame,
+ selector: Selector
+ ): Locator<NodeFor<Selector>> {
+ return new NodeLocator<NodeFor<Selector>>(pageOrFrame, selector).setTimeout(
+ 'getDefaultTimeout' in pageOrFrame
+ ? pageOrFrame.getDefaultTimeout()
+ : pageOrFrame.page().getDefaultTimeout()
+ );
+ }
+
+ #pageOrFrame: Page | Frame;
+ #selector: string;
+
+ private constructor(pageOrFrame: Page | Frame, selector: string) {
+ super();
+
+ this.#pageOrFrame = pageOrFrame;
+ this.#selector = selector;
+ }
+
+ /**
+ * Waits for the element to become visible or hidden. visibility === 'visible'
+ * means that the element has a computed style, the visibility property other
+ * than 'hidden' or 'collapse' and non-empty bounding box. visibility ===
+ * 'hidden' means the opposite of that.
+ */
+ #waitForVisibilityIfNeeded = (handle: HandleFor<T>): Observable<never> => {
+ if (!this.visibility) {
+ return EMPTY;
+ }
+
+ return (() => {
+ switch (this.visibility) {
+ case 'hidden':
+ return defer(() => {
+ return from(handle.isHidden());
+ });
+ case 'visible':
+ return defer(() => {
+ return from(handle.isVisible());
+ });
+ }
+ })().pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
+ };
+
+ override _clone(): NodeLocator<T> {
+ return new NodeLocator<T>(this.#pageOrFrame, this.#selector).copyOptions(
+ this
+ );
+ }
+
+ override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
+ const signal = options?.signal;
+ return defer(() => {
+ return from(
+ this.#pageOrFrame.waitForSelector(this.#selector, {
+ visible: false,
+ timeout: this._timeout,
+ signal,
+ }) as Promise<HandleFor<T> | null>
+ );
+ }).pipe(
+ filter((value): value is NonNullable<typeof value> => {
+ return value !== null;
+ }),
+ throwIfEmpty(),
+ this.operators.conditions([this.#waitForVisibilityIfNeeded], signal)
+ );
+ }
+}
+
+/**
+ * @public
+ */
+export type AwaitedLocator<T> = T extends Locator<infer S> ? S : never;
+function checkLocatorArray<T extends readonly unknown[] | []>(
+ locators: T
+): ReadonlyArray<Locator<AwaitedLocator<T[number]>>> {
+ for (const locator of locators) {
+ if (!(locator instanceof Locator)) {
+ throw new Error('Unknown locator for race candidate');
+ }
+ }
+ return locators as ReadonlyArray<Locator<AwaitedLocator<T[number]>>>;
+}
+/**
+ * @internal
+ */
+export class RaceLocator<T> extends Locator<T> {
+ static create<T extends readonly unknown[]>(
+ locators: T
+ ): Locator<AwaitedLocator<T[number]>> {
+ const array = checkLocatorArray(locators);
+ return new RaceLocator(array);
+ }
+
+ #locators: ReadonlyArray<Locator<T>>;
+
+ constructor(locators: ReadonlyArray<Locator<T>>) {
+ super();
+ this.#locators = locators;
+ }
+
+ override _clone(): RaceLocator<T> {
+ return new RaceLocator<T>(
+ this.#locators.map(locator => {
+ return locator.clone();
+ })
+ ).copyOptions(this);
+ }
+
+ override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
+ return race(
+ ...this.#locators.map(locator => {
+ return locator._wait(options);
+ })
+ );
+ }
+}
+
+/**
+ * For observables coming from promises, a delay is needed, otherwise RxJS will
+ * never yield in a permanent failure for a promise.
+ *
+ * We also don't want RxJS to do promise operations to often, so we bump the
+ * delay up to 100ms.
+ *
+ * @internal
+ */
+export const RETRY_DELAY = 100;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts
new file mode 100644
index 0000000000..ace35a52b0
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts
@@ -0,0 +1,209 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js';
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
+
+import type {CDPEvents, CDPSession} from '../api/CDPSession.js';
+import type {Connection as CdpConnection} from '../cdp/Connection.js';
+import {debug} from '../common/Debug.js';
+import {TargetCloseError} from '../common/Errors.js';
+import type {Handler} from '../common/EventEmitter.js';
+
+import {BidiConnection} from './Connection.js';
+
+const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
+ debug(`bidi:${prefix}`)(args);
+};
+
+/**
+ * @internal
+ */
+export async function connectBidiOverCdp(
+ cdp: CdpConnection,
+ // TODO: replace with `BidiMapper.MapperOptions`, once it's exported in
+ // https://github.com/puppeteer/puppeteer/pull/11415.
+ options: {acceptInsecureCerts: boolean}
+): Promise<BidiConnection> {
+ const transportBiDi = new NoOpTransport();
+ const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
+ const pptrTransport = {
+ send(message: string): void {
+ // Forwards a BiDi command sent by Puppeteer to the input of the BidiServer.
+ transportBiDi.emitMessage(JSON.parse(message));
+ },
+ close(): void {
+ bidiServer.close();
+ cdpConnectionAdapter.close();
+ cdp.dispose();
+ },
+ onmessage(_message: string): void {
+ // The method is overridden by the Connection.
+ },
+ };
+ transportBiDi.on('bidiResponse', (message: object) => {
+ // Forwards a BiDi event sent by BidiServer to Puppeteer.
+ pptrTransport.onmessage(JSON.stringify(message));
+ });
+ const pptrBiDiConnection = new BidiConnection(cdp.url(), pptrTransport);
+ const bidiServer = await BidiMapper.BidiServer.createAndStart(
+ transportBiDi,
+ cdpConnectionAdapter,
+ // TODO: most likely need a little bit of refactoring
+ cdpConnectionAdapter.browserClient(),
+ '',
+ options,
+ undefined,
+ bidiServerLogger
+ );
+ return pptrBiDiConnection;
+}
+
+/**
+ * Manages CDPSessions for BidiServer.
+ * @internal
+ */
+class CdpConnectionAdapter {
+ #cdp: CdpConnection;
+ #adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>();
+ #browserCdpConnection: CDPClientAdapter<CdpConnection>;
+
+ constructor(cdp: CdpConnection) {
+ this.#cdp = cdp;
+ this.#browserCdpConnection = new CDPClientAdapter(cdp);
+ }
+
+ browserClient(): CDPClientAdapter<CdpConnection> {
+ return this.#browserCdpConnection;
+ }
+
+ getCdpClient(id: string) {
+ const session = this.#cdp.session(id);
+ if (!session) {
+ throw new Error(`Unknown CDP session with id ${id}`);
+ }
+ if (!this.#adapters.has(session)) {
+ const adapter = new CDPClientAdapter(
+ session,
+ id,
+ this.#browserCdpConnection
+ );
+ this.#adapters.set(session, adapter);
+ return adapter;
+ }
+ return this.#adapters.get(session)!;
+ }
+
+ close() {
+ this.#browserCdpConnection.close();
+ for (const adapter of this.#adapters.values()) {
+ adapter.close();
+ }
+ }
+}
+
+/**
+ * Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that
+ * BidiServer needs.
+ *
+ * @internal
+ */
+class CDPClientAdapter<T extends CDPSession | CdpConnection>
+ extends BidiMapper.EventEmitter<CDPEvents>
+ implements BidiMapper.CdpClient
+{
+ #closed = false;
+ #client: T;
+ sessionId: string | undefined = undefined;
+ #browserClient?: BidiMapper.CdpClient;
+
+ constructor(
+ client: T,
+ sessionId?: string,
+ browserClient?: BidiMapper.CdpClient
+ ) {
+ super();
+ this.#client = client;
+ this.sessionId = sessionId;
+ this.#browserClient = browserClient;
+ this.#client.on('*', this.#forwardMessage as Handler<any>);
+ }
+
+ browserClient(): BidiMapper.CdpClient {
+ return this.#browserClient!;
+ }
+
+ #forwardMessage = <T extends keyof CDPEvents>(
+ method: T,
+ event: CDPEvents[T]
+ ) => {
+ this.emit(method, event);
+ };
+
+ async sendCommand<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ ...params: ProtocolMapping.Commands[T]['paramsType']
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ if (this.#closed) {
+ return;
+ }
+ try {
+ return await this.#client.send(method, ...params);
+ } catch (err) {
+ if (this.#closed) {
+ return;
+ }
+ throw err;
+ }
+ }
+
+ close() {
+ this.#client.off('*', this.#forwardMessage as Handler<any>);
+ this.#closed = true;
+ }
+
+ isCloseError(error: unknown): boolean {
+ return error instanceof TargetCloseError;
+ }
+}
+
+/**
+ * This transport is given to the BiDi server instance and allows Puppeteer
+ * to send and receive commands to the BiDiServer.
+ * @internal
+ */
+class NoOpTransport
+ extends BidiMapper.EventEmitter<{
+ bidiResponse: Bidi.ChromiumBidi.Message;
+ }>
+ implements BidiMapper.BidiTransport
+{
+ #onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void =
+ async (_m: Bidi.ChromiumBidi.Command): Promise<void> => {
+ return;
+ };
+
+ emitMessage(message: Bidi.ChromiumBidi.Command) {
+ void this.#onMessage(message);
+ }
+
+ setOnMessage(
+ onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void
+ ): void {
+ this.#onMessage = onMessage;
+ }
+
+ async sendMessage(message: Bidi.ChromiumBidi.Message): Promise<void> {
+ this.emit('bidiResponse', message);
+ }
+
+ close() {
+ this.#onMessage = async (_m: Bidi.ChromiumBidi.Command): Promise<void> => {
+ return;
+ };
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts
new file mode 100644
index 0000000000..42979790c9
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts
@@ -0,0 +1,317 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ChildProcess} from 'child_process';
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {
+ Browser,
+ BrowserEvent,
+ type BrowserCloseCallback,
+ type BrowserContextOptions,
+ type DebugInfo,
+} from '../api/Browser.js';
+import {BrowserContextEvent} from '../api/BrowserContext.js';
+import type {Page} from '../api/Page.js';
+import type {Target} from '../api/Target.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import type {Handler} from '../common/EventEmitter.js';
+import {debugError} from '../common/util.js';
+import type {Viewport} from '../common/Viewport.js';
+
+import {BidiBrowserContext} from './BrowserContext.js';
+import {BrowsingContext, BrowsingContextEvent} from './BrowsingContext.js';
+import type {BidiConnection} from './Connection.js';
+import type {Browser as BrowserCore} from './core/Browser.js';
+import {Session} from './core/Session.js';
+import type {UserContext} from './core/UserContext.js';
+import {
+ BiDiBrowserTarget,
+ BiDiBrowsingContextTarget,
+ BiDiPageTarget,
+ type BidiTarget,
+} from './Target.js';
+
+/**
+ * @internal
+ */
+export interface BidiBrowserOptions {
+ process?: ChildProcess;
+ closeCallback?: BrowserCloseCallback;
+ connection: BidiConnection;
+ defaultViewport: Viewport | null;
+ ignoreHTTPSErrors?: boolean;
+}
+
+/**
+ * @internal
+ */
+export class BidiBrowser extends Browser {
+ readonly protocol = 'webDriverBiDi';
+
+ // TODO: Update generator to include fully module
+ static readonly subscribeModules: string[] = [
+ 'browsingContext',
+ 'network',
+ 'log',
+ 'script',
+ ];
+ static readonly subscribeCdpEvents: Bidi.Cdp.EventNames[] = [
+ // Coverage
+ 'cdp.Debugger.scriptParsed',
+ 'cdp.CSS.styleSheetAdded',
+ 'cdp.Runtime.executionContextsCleared',
+ // Tracing
+ 'cdp.Tracing.tracingComplete',
+ // TODO: subscribe to all CDP events in the future.
+ 'cdp.Network.requestWillBeSent',
+ 'cdp.Debugger.scriptParsed',
+ 'cdp.Page.screencastFrame',
+ ];
+
+ static async create(opts: BidiBrowserOptions): Promise<BidiBrowser> {
+ const session = await Session.from(opts.connection, {
+ alwaysMatch: {
+ acceptInsecureCerts: opts.ignoreHTTPSErrors,
+ webSocketUrl: true,
+ },
+ });
+
+ await session.subscribe(
+ session.capabilities.browserName.toLocaleLowerCase().includes('firefox')
+ ? BidiBrowser.subscribeModules
+ : [...BidiBrowser.subscribeModules, ...BidiBrowser.subscribeCdpEvents]
+ );
+
+ const browser = new BidiBrowser(session.browser, opts);
+ browser.#initialize();
+ await browser.#getTree();
+ return browser;
+ }
+
+ #process?: ChildProcess;
+ #closeCallback?: BrowserCloseCallback;
+ #browserCore: BrowserCore;
+ #defaultViewport: Viewport | null;
+ #targets = new Map<string, BidiTarget>();
+ #browserContexts = new WeakMap<UserContext, BidiBrowserContext>();
+ #browserTarget: BiDiBrowserTarget;
+
+ #connectionEventHandlers = new Map<
+ Bidi.BrowsingContextEvent['method'],
+ Handler<any>
+ >([
+ ['browsingContext.contextCreated', this.#onContextCreated.bind(this)],
+ ['browsingContext.contextDestroyed', this.#onContextDestroyed.bind(this)],
+ ['browsingContext.domContentLoaded', this.#onContextDomLoaded.bind(this)],
+ ['browsingContext.fragmentNavigated', this.#onContextNavigation.bind(this)],
+ ['browsingContext.navigationStarted', this.#onContextNavigation.bind(this)],
+ ]);
+
+ private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) {
+ super();
+ this.#process = opts.process;
+ this.#closeCallback = opts.closeCallback;
+ this.#browserCore = browserCore;
+ this.#defaultViewport = opts.defaultViewport;
+ this.#browserTarget = new BiDiBrowserTarget(this);
+ this.#createBrowserContext(this.#browserCore.defaultUserContext);
+ }
+
+ #initialize() {
+ this.#browserCore.once('disconnected', () => {
+ this.emit(BrowserEvent.Disconnected, undefined);
+ });
+ this.#process?.once('close', () => {
+ this.#browserCore.dispose('Browser process exited.', true);
+ this.connection.dispose();
+ });
+
+ for (const [eventName, handler] of this.#connectionEventHandlers) {
+ this.connection.on(eventName, handler);
+ }
+ }
+
+ get #browserName() {
+ return this.#browserCore.session.capabilities.browserName;
+ }
+ get #browserVersion() {
+ return this.#browserCore.session.capabilities.browserVersion;
+ }
+
+ override userAgent(): never {
+ throw new UnsupportedOperation();
+ }
+
+ #createBrowserContext(userContext: UserContext) {
+ const browserContext = new BidiBrowserContext(this, userContext, {
+ defaultViewport: this.#defaultViewport,
+ });
+ this.#browserContexts.set(userContext, browserContext);
+ return browserContext;
+ }
+
+ #onContextDomLoaded(event: Bidi.BrowsingContext.Info) {
+ const target = this.#targets.get(event.context);
+ if (target) {
+ this.emit(BrowserEvent.TargetChanged, target);
+ }
+ }
+
+ #onContextNavigation(event: Bidi.BrowsingContext.NavigationInfo) {
+ const target = this.#targets.get(event.context);
+ if (target) {
+ this.emit(BrowserEvent.TargetChanged, target);
+ target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
+ }
+ }
+
+ #onContextCreated(event: Bidi.BrowsingContext.ContextCreated['params']) {
+ const context = new BrowsingContext(
+ this.connection,
+ event,
+ this.#browserName
+ );
+ this.connection.registerBrowsingContexts(context);
+ // TODO: once more browsing context types are supported, this should be
+ // updated to support those. Currently, all top-level contexts are treated
+ // as pages.
+ const browserContext = this.browserContexts().at(-1);
+ if (!browserContext) {
+ throw new Error('Missing browser contexts');
+ }
+ const target = !context.parent
+ ? new BiDiPageTarget(browserContext, context)
+ : new BiDiBrowsingContextTarget(browserContext, context);
+ this.#targets.set(event.context, target);
+
+ this.emit(BrowserEvent.TargetCreated, target);
+ target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
+
+ if (context.parent) {
+ const topLevel = this.connection.getTopLevelContext(context.parent);
+ topLevel.emit(BrowsingContextEvent.Created, context);
+ }
+ }
+
+ async #getTree(): Promise<void> {
+ const {result} = await this.connection.send('browsingContext.getTree', {});
+ for (const context of result.contexts) {
+ this.#onContextCreated(context);
+ }
+ }
+
+ async #onContextDestroyed(
+ event: Bidi.BrowsingContext.ContextDestroyed['params']
+ ) {
+ const context = this.connection.getBrowsingContext(event.context);
+ const topLevelContext = this.connection.getTopLevelContext(event.context);
+ topLevelContext.emit(BrowsingContextEvent.Destroyed, context);
+ const target = this.#targets.get(event.context);
+ const page = await target?.page();
+ await page?.close().catch(debugError);
+ this.#targets.delete(event.context);
+ if (target) {
+ this.emit(BrowserEvent.TargetDestroyed, target);
+ target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
+ }
+ }
+
+ get connection(): BidiConnection {
+ // SAFETY: We only have one implementation.
+ return this.#browserCore.session.connection as BidiConnection;
+ }
+
+ override wsEndpoint(): string {
+ return this.connection.url;
+ }
+
+ override async close(): Promise<void> {
+ for (const [eventName, handler] of this.#connectionEventHandlers) {
+ this.connection.off(eventName, handler);
+ }
+ if (this.connection.closed) {
+ return;
+ }
+
+ try {
+ await this.#browserCore.close();
+ await this.#closeCallback?.call(null);
+ } catch (error) {
+ // Fail silently.
+ debugError(error);
+ } finally {
+ this.connection.dispose();
+ }
+ }
+
+ override get connected(): boolean {
+ return !this.#browserCore.disposed;
+ }
+
+ override process(): ChildProcess | null {
+ return this.#process ?? null;
+ }
+
+ override async createIncognitoBrowserContext(
+ _options?: BrowserContextOptions
+ ): Promise<BidiBrowserContext> {
+ const userContext = await this.#browserCore.createUserContext();
+ return this.#createBrowserContext(userContext);
+ }
+
+ override async version(): Promise<string> {
+ return `${this.#browserName}/${this.#browserVersion}`;
+ }
+
+ override browserContexts(): BidiBrowserContext[] {
+ return [...this.#browserCore.userContexts].map(context => {
+ return this.#browserContexts.get(context)!;
+ });
+ }
+
+ override defaultBrowserContext(): BidiBrowserContext {
+ return this.#browserContexts.get(this.#browserCore.defaultUserContext)!;
+ }
+
+ override newPage(): Promise<Page> {
+ return this.defaultBrowserContext().newPage();
+ }
+
+ override targets(): Target[] {
+ return [this.#browserTarget, ...Array.from(this.#targets.values())];
+ }
+
+ _getTargetById(id: string): BidiTarget {
+ const target = this.#targets.get(id);
+ if (!target) {
+ throw new Error('Target not found');
+ }
+ return target;
+ }
+
+ override target(): Target {
+ return this.#browserTarget;
+ }
+
+ override async disconnect(): Promise<void> {
+ try {
+ await this.#browserCore.session.end();
+ } catch (error) {
+ // Fail silently.
+ debugError(error);
+ } finally {
+ this.connection.dispose();
+ }
+ }
+
+ override get debugInfo(): DebugInfo {
+ return {
+ pendingProtocolErrors: this.connection.getPendingProtocolErrors(),
+ };
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserConnector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserConnector.ts
new file mode 100644
index 0000000000..f616e90561
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserConnector.ts
@@ -0,0 +1,123 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {BrowserCloseCallback} from '../api/Browser.js';
+import {Connection} from '../cdp/Connection.js';
+import type {ConnectionTransport} from '../common/ConnectionTransport.js';
+import type {
+ BrowserConnectOptions,
+ ConnectOptions,
+} from '../common/ConnectOptions.js';
+import {ProtocolError, UnsupportedOperation} from '../common/Errors.js';
+import {debugError, DEFAULT_VIEWPORT} from '../common/util.js';
+
+import type {BidiBrowser} from './Browser.js';
+import type {BidiConnection} from './Connection.js';
+
+/**
+ * Users should never call this directly; it's called when calling `puppeteer.connect`
+ * with `protocol: 'webDriverBiDi'`. This method attaches Puppeteer to an existing browser
+ * instance. First it tries to connect to the browser using pure BiDi. If the protocol is
+ * not supported, connects to the browser using BiDi over CDP.
+ *
+ * @internal
+ */
+export async function _connectToBiDiBrowser(
+ connectionTransport: ConnectionTransport,
+ url: string,
+ options: BrowserConnectOptions & ConnectOptions
+): Promise<BidiBrowser> {
+ const {ignoreHTTPSErrors = false, defaultViewport = DEFAULT_VIEWPORT} =
+ options;
+
+ const {bidiConnection, closeCallback} = await getBiDiConnection(
+ connectionTransport,
+ url,
+ options
+ );
+ const BiDi = await import(/* webpackIgnore: true */ './bidi.js');
+ const bidiBrowser = await BiDi.BidiBrowser.create({
+ connection: bidiConnection,
+ closeCallback,
+ process: undefined,
+ defaultViewport: defaultViewport,
+ ignoreHTTPSErrors: ignoreHTTPSErrors,
+ });
+ return bidiBrowser;
+}
+
+/**
+ * Returns a BiDiConnection established to the endpoint specified by the options and a
+ * callback closing the browser. Callback depends on whether the connection is pure BiDi
+ * or BiDi over CDP.
+ * The method tries to connect to the browser using pure BiDi protocol, and falls back
+ * to BiDi over CDP.
+ */
+async function getBiDiConnection(
+ connectionTransport: ConnectionTransport,
+ url: string,
+ options: BrowserConnectOptions
+): Promise<{
+ bidiConnection: BidiConnection;
+ closeCallback: BrowserCloseCallback;
+}> {
+ const BiDi = await import(/* webpackIgnore: true */ './bidi.js');
+ const {ignoreHTTPSErrors = false, slowMo = 0, protocolTimeout} = options;
+
+ // Try pure BiDi first.
+ const pureBidiConnection = new BiDi.BidiConnection(
+ url,
+ connectionTransport,
+ slowMo,
+ protocolTimeout
+ );
+ try {
+ const result = await pureBidiConnection.send('session.status', {});
+ if ('type' in result && result.type === 'success') {
+ // The `browserWSEndpoint` points to an endpoint supporting pure WebDriver BiDi.
+ return {
+ bidiConnection: pureBidiConnection,
+ closeCallback: async () => {
+ await pureBidiConnection.send('browser.close', {}).catch(debugError);
+ },
+ };
+ }
+ } catch (e) {
+ if (!(e instanceof ProtocolError)) {
+ // Unexpected exception not related to BiDi / CDP. Rethrow.
+ throw e;
+ }
+ }
+ // Unbind the connection to avoid memory leaks.
+ pureBidiConnection.unbind();
+
+ // Fall back to CDP over BiDi reusing the WS connection.
+ const cdpConnection = new Connection(
+ url,
+ connectionTransport,
+ slowMo,
+ protocolTimeout
+ );
+
+ const version = await cdpConnection.send('Browser.getVersion');
+ if (version.product.toLowerCase().includes('firefox')) {
+ throw new UnsupportedOperation(
+ 'Firefox is not supported in BiDi over CDP mode.'
+ );
+ }
+
+ // TODO: use other options too.
+ const bidiOverCdpConnection = await BiDi.connectBidiOverCdp(cdpConnection, {
+ acceptInsecureCerts: ignoreHTTPSErrors,
+ });
+ return {
+ bidiConnection: bidiOverCdpConnection,
+ closeCallback: async () => {
+ // In case of BiDi over CDP, we need to close browser via CDP.
+ await cdpConnection.send('Browser.close').catch(debugError);
+ },
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts
new file mode 100644
index 0000000000..feb5e9951d
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts
@@ -0,0 +1,145 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import type {WaitForTargetOptions} from '../api/Browser.js';
+import {BrowserContext} from '../api/BrowserContext.js';
+import type {Page} from '../api/Page.js';
+import type {Target} from '../api/Target.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import {debugError} from '../common/util.js';
+import type {Viewport} from '../common/Viewport.js';
+
+import type {BidiBrowser} from './Browser.js';
+import type {BidiConnection} from './Connection.js';
+import {UserContext} from './core/UserContext.js';
+import type {BidiPage} from './Page.js';
+
+/**
+ * @internal
+ */
+export interface BidiBrowserContextOptions {
+ defaultViewport: Viewport | null;
+}
+
+/**
+ * @internal
+ */
+export class BidiBrowserContext extends BrowserContext {
+ #browser: BidiBrowser;
+ #connection: BidiConnection;
+ #defaultViewport: Viewport | null;
+ #userContext: UserContext;
+
+ constructor(
+ browser: BidiBrowser,
+ userContext: UserContext,
+ options: BidiBrowserContextOptions
+ ) {
+ super();
+ this.#browser = browser;
+ this.#userContext = userContext;
+ this.#connection = this.#browser.connection;
+ this.#defaultViewport = options.defaultViewport;
+ }
+
+ override targets(): Target[] {
+ return this.#browser.targets().filter(target => {
+ return target.browserContext() === this;
+ });
+ }
+
+ override waitForTarget(
+ predicate: (x: Target) => boolean | Promise<boolean>,
+ options: WaitForTargetOptions = {}
+ ): Promise<Target> {
+ return this.#browser.waitForTarget(target => {
+ return target.browserContext() === this && predicate(target);
+ }, options);
+ }
+
+ get connection(): BidiConnection {
+ return this.#connection;
+ }
+
+ override async newPage(): Promise<Page> {
+ const {result} = await this.#connection.send('browsingContext.create', {
+ type: Bidi.BrowsingContext.CreateType.Tab,
+ });
+ const target = this.#browser._getTargetById(result.context);
+
+ // TODO: once BiDi has some concept matching BrowserContext, the newly
+ // created contexts should get automatically assigned to the right
+ // BrowserContext. For now, we assume that only explicitly created pages go
+ // to the current BrowserContext. Otherwise, the contexts get assigned to
+ // the default BrowserContext by the Browser.
+ target._setBrowserContext(this);
+
+ const page = await target.page();
+ if (!page) {
+ throw new Error('Page is not found');
+ }
+ if (this.#defaultViewport) {
+ try {
+ await page.setViewport(this.#defaultViewport);
+ } catch {
+ // No support for setViewport in Firefox.
+ }
+ }
+
+ return page;
+ }
+
+ override async close(): Promise<void> {
+ if (!this.isIncognito()) {
+ throw new Error('Default context cannot be closed!');
+ }
+
+ // TODO: Remove once we have adopted the new browsing contexts.
+ for (const target of this.targets()) {
+ const page = await target?.page();
+ try {
+ await page?.close();
+ } catch (error) {
+ debugError(error);
+ }
+ }
+
+ try {
+ await this.#userContext.remove();
+ } catch (error) {
+ debugError(error);
+ }
+ }
+
+ override browser(): BidiBrowser {
+ return this.#browser;
+ }
+
+ override async pages(): Promise<BidiPage[]> {
+ const results = await Promise.all(
+ [...this.targets()].map(t => {
+ return t.page();
+ })
+ );
+ return results.filter((p): p is BidiPage => {
+ return p !== null;
+ });
+ }
+
+ override isIncognito(): boolean {
+ return this.#userContext.id !== UserContext.DEFAULT;
+ }
+
+ override overridePermissions(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override clearPermissionOverrides(): never {
+ throw new UnsupportedOperation();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts
new file mode 100644
index 0000000000..0804628c06
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts
@@ -0,0 +1,187 @@
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
+
+import {CDPSession} from '../api/CDPSession.js';
+import type {Connection as CdpConnection} from '../cdp/Connection.js';
+import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
+import type {EventType} from '../common/EventEmitter.js';
+import {debugError} from '../common/util.js';
+import {Deferred} from '../util/Deferred.js';
+
+import type {BidiConnection} from './Connection.js';
+import {BidiRealm} from './Realm.js';
+
+/**
+ * @internal
+ */
+export const cdpSessions = new Map<string, CdpSessionWrapper>();
+
+/**
+ * @internal
+ */
+export class CdpSessionWrapper extends CDPSession {
+ #context: BrowsingContext;
+ #sessionId = Deferred.create<string>();
+ #detached = false;
+
+ constructor(context: BrowsingContext, sessionId?: string) {
+ super();
+ this.#context = context;
+ if (!this.#context.supportsCdp()) {
+ return;
+ }
+ if (sessionId) {
+ this.#sessionId.resolve(sessionId);
+ cdpSessions.set(sessionId, this);
+ } else {
+ context.connection
+ .send('cdp.getSession', {
+ context: context.id,
+ })
+ .then(session => {
+ this.#sessionId.resolve(session.result.session!);
+ cdpSessions.set(session.result.session!, this);
+ })
+ .catch(err => {
+ this.#sessionId.reject(err);
+ });
+ }
+ }
+
+ override connection(): CdpConnection | undefined {
+ return undefined;
+ }
+
+ override async send<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ ...paramArgs: ProtocolMapping.Commands[T]['paramsType']
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ if (!this.#context.supportsCdp()) {
+ throw new UnsupportedOperation(
+ 'CDP support is required for this feature. The current browser does not support CDP.'
+ );
+ }
+ if (this.#detached) {
+ throw new TargetCloseError(
+ `Protocol error (${method}): Session closed. Most likely the page has been closed.`
+ );
+ }
+ const session = await this.#sessionId.valueOrThrow();
+ const {result} = await this.#context.connection.send('cdp.sendCommand', {
+ method: method,
+ params: paramArgs[0],
+ session,
+ });
+ return result.result;
+ }
+
+ override async detach(): Promise<void> {
+ cdpSessions.delete(this.id());
+ if (!this.#detached && this.#context.supportsCdp()) {
+ await this.#context.cdpSession.send('Target.detachFromTarget', {
+ sessionId: this.id(),
+ });
+ }
+ this.#detached = true;
+ }
+
+ override id(): string {
+ const val = this.#sessionId.value();
+ return val instanceof Error || val === undefined ? '' : val;
+ }
+}
+
+/**
+ * Internal events that the BrowsingContext class emits.
+ *
+ * @internal
+ */
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace BrowsingContextEvent {
+ /**
+ * Emitted on the top-level context, when a descendant context is created.
+ */
+ export const Created = Symbol('BrowsingContext.created');
+ /**
+ * Emitted on the top-level context, when a descendant context or the
+ * top-level context itself is destroyed.
+ */
+ export const Destroyed = Symbol('BrowsingContext.destroyed');
+}
+
+/**
+ * @internal
+ */
+export interface BrowsingContextEvents extends Record<EventType, unknown> {
+ [BrowsingContextEvent.Created]: BrowsingContext;
+ [BrowsingContextEvent.Destroyed]: BrowsingContext;
+}
+
+/**
+ * @internal
+ */
+export class BrowsingContext extends BidiRealm {
+ #id: string;
+ #url: string;
+ #cdpSession: CDPSession;
+ #parent?: string | null;
+ #browserName = '';
+
+ constructor(
+ connection: BidiConnection,
+ info: Bidi.BrowsingContext.Info,
+ browserName: string
+ ) {
+ super(connection);
+ this.#id = info.context;
+ this.#url = info.url;
+ this.#parent = info.parent;
+ this.#browserName = browserName;
+ this.#cdpSession = new CdpSessionWrapper(this, undefined);
+
+ this.on('browsingContext.domContentLoaded', this.#updateUrl.bind(this));
+ this.on('browsingContext.fragmentNavigated', this.#updateUrl.bind(this));
+ this.on('browsingContext.load', this.#updateUrl.bind(this));
+ }
+
+ supportsCdp(): boolean {
+ return !this.#browserName.toLowerCase().includes('firefox');
+ }
+
+ #updateUrl(info: Bidi.BrowsingContext.NavigationInfo) {
+ this.#url = info.url;
+ }
+
+ createRealmForSandbox(): BidiRealm {
+ return new BidiRealm(this.connection);
+ }
+
+ get url(): string {
+ return this.#url;
+ }
+
+ get id(): string {
+ return this.#id;
+ }
+
+ get parent(): string | undefined | null {
+ return this.#parent;
+ }
+
+ get cdpSession(): CDPSession {
+ return this.#cdpSession;
+ }
+
+ async sendCdpCommand<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ ...paramArgs: ProtocolMapping.Commands[T]['paramsType']
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ return await this.#cdpSession.send(method, ...paramArgs);
+ }
+
+ dispose(): void {
+ this.removeAllListeners();
+ this.connection.unregisterBrowsingContexts(this.#id);
+ void this.#cdpSession.detach().catch(debugError);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.test.ts
new file mode 100644
index 0000000000..9f37e38661
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.test.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import type {ConnectionTransport} from '../common/ConnectionTransport.js';
+
+import {BidiConnection} from './Connection.js';
+
+describe('WebDriver BiDi Connection', () => {
+ class TestConnectionTransport implements ConnectionTransport {
+ sent: string[] = [];
+ closed = false;
+
+ send(message: string) {
+ this.sent.push(message);
+ }
+
+ close(): void {
+ this.closed = true;
+ }
+ }
+
+ it('should work', async () => {
+ const transport = new TestConnectionTransport();
+ const connection = new BidiConnection('ws://127.0.0.1', transport);
+ const responsePromise = connection.send('session.new', {
+ capabilities: {},
+ });
+ expect(transport.sent).toEqual([
+ `{"id":1,"method":"session.new","params":{"capabilities":{}}}`,
+ ]);
+ const id = JSON.parse(transport.sent[0]!).id;
+ const rawResponse = {
+ id,
+ type: 'success',
+ result: {ready: false, message: 'already connected'},
+ };
+ (transport as ConnectionTransport).onmessage?.(JSON.stringify(rawResponse));
+ const response = await responsePromise;
+ expect(response).toEqual(rawResponse);
+ connection.dispose();
+ expect(transport.closed).toBeTruthy();
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts
new file mode 100644
index 0000000000..bce952ba39
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts
@@ -0,0 +1,256 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {CallbackRegistry} from '../common/CallbackRegistry.js';
+import type {ConnectionTransport} from '../common/ConnectionTransport.js';
+import {debug} from '../common/Debug.js';
+import type {EventsWithWildcard} from '../common/EventEmitter.js';
+import {EventEmitter} from '../common/EventEmitter.js';
+import {debugError} from '../common/util.js';
+import {assert} from '../util/assert.js';
+
+import {cdpSessions, type BrowsingContext} from './BrowsingContext.js';
+import type {
+ BidiEvents,
+ Commands as BidiCommands,
+ Connection,
+} from './core/Connection.js';
+
+const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
+const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
+
+/**
+ * @internal
+ */
+export interface Commands extends BidiCommands {
+ 'cdp.sendCommand': {
+ params: Bidi.Cdp.SendCommandParameters;
+ returnType: Bidi.Cdp.SendCommandResult;
+ };
+ 'cdp.getSession': {
+ params: Bidi.Cdp.GetSessionParameters;
+ returnType: Bidi.Cdp.GetSessionResult;
+ };
+}
+
+/**
+ * @internal
+ */
+export class BidiConnection
+ extends EventEmitter<BidiEvents>
+ implements Connection
+{
+ #url: string;
+ #transport: ConnectionTransport;
+ #delay: number;
+ #timeout? = 0;
+ #closed = false;
+ #callbacks = new CallbackRegistry();
+ #browsingContexts = new Map<string, BrowsingContext>();
+ #emitters: Array<EventEmitter<any>> = [];
+
+ constructor(
+ url: string,
+ transport: ConnectionTransport,
+ delay = 0,
+ timeout?: number
+ ) {
+ super();
+ this.#url = url;
+ this.#delay = delay;
+ this.#timeout = timeout ?? 180_000;
+
+ this.#transport = transport;
+ this.#transport.onmessage = this.onMessage.bind(this);
+ this.#transport.onclose = this.unbind.bind(this);
+ }
+
+ get closed(): boolean {
+ return this.#closed;
+ }
+
+ get url(): string {
+ return this.#url;
+ }
+
+ pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
+ this.#emitters.push(emitter);
+ }
+
+ override emit<Key extends keyof EventsWithWildcard<BidiEvents>>(
+ type: Key,
+ event: EventsWithWildcard<BidiEvents>[Key]
+ ): boolean {
+ for (const emitter of this.#emitters) {
+ emitter.emit(type, event);
+ }
+ return super.emit(type, event);
+ }
+
+ send<T extends keyof Commands>(
+ method: T,
+ params: Commands[T]['params']
+ ): Promise<{result: Commands[T]['returnType']}> {
+ assert(!this.#closed, 'Protocol error: Connection closed.');
+
+ return this.#callbacks.create(method, this.#timeout, id => {
+ const stringifiedMessage = JSON.stringify({
+ id,
+ method,
+ params,
+ } as Bidi.Command);
+ debugProtocolSend(stringifiedMessage);
+ this.#transport.send(stringifiedMessage);
+ }) as Promise<{result: Commands[T]['returnType']}>;
+ }
+
+ /**
+ * @internal
+ */
+ protected async onMessage(message: string): Promise<void> {
+ if (this.#delay) {
+ await new Promise(f => {
+ return setTimeout(f, this.#delay);
+ });
+ }
+ debugProtocolReceive(message);
+ const object: Bidi.ChromiumBidi.Message = JSON.parse(message);
+ if ('type' in object) {
+ switch (object.type) {
+ case 'success':
+ this.#callbacks.resolve(object.id, object);
+ return;
+ case 'error':
+ if (object.id === null) {
+ break;
+ }
+ this.#callbacks.reject(
+ object.id,
+ createProtocolError(object),
+ object.message
+ );
+ return;
+ case 'event':
+ if (isCdpEvent(object)) {
+ cdpSessions
+ .get(object.params.session)
+ ?.emit(object.params.event, object.params.params);
+ return;
+ }
+ this.#maybeEmitOnContext(object);
+ // SAFETY: We know the method and parameter still match here.
+ this.emit(
+ object.method,
+ object.params as BidiEvents[keyof BidiEvents]
+ );
+ return;
+ }
+ }
+ // Even if the response in not in BiDi protocol format but `id` is provided, reject
+ // the callback. This can happen if the endpoint supports CDP instead of BiDi.
+ if ('id' in object) {
+ this.#callbacks.reject(
+ (object as {id: number}).id,
+ `Protocol Error. Message is not in BiDi protocol format: '${message}'`,
+ object.message
+ );
+ }
+ debugError(object);
+ }
+
+ #maybeEmitOnContext(event: Bidi.ChromiumBidi.Event) {
+ let context: BrowsingContext | undefined;
+ // Context specific events
+ if ('context' in event.params && event.params.context !== null) {
+ context = this.#browsingContexts.get(event.params.context);
+ // `log.entryAdded` specific context
+ } else if (
+ 'source' in event.params &&
+ event.params.source.context !== undefined
+ ) {
+ context = this.#browsingContexts.get(event.params.source.context);
+ }
+ context?.emit(event.method, event.params);
+ }
+
+ registerBrowsingContexts(context: BrowsingContext): void {
+ this.#browsingContexts.set(context.id, context);
+ }
+
+ getBrowsingContext(contextId: string): BrowsingContext {
+ const currentContext = this.#browsingContexts.get(contextId);
+ if (!currentContext) {
+ throw new Error(`BrowsingContext ${contextId} does not exist.`);
+ }
+ return currentContext;
+ }
+
+ getTopLevelContext(contextId: string): BrowsingContext {
+ let currentContext = this.#browsingContexts.get(contextId);
+ if (!currentContext) {
+ throw new Error(`BrowsingContext ${contextId} does not exist.`);
+ }
+ while (currentContext.parent) {
+ contextId = currentContext.parent;
+ currentContext = this.#browsingContexts.get(contextId);
+ if (!currentContext) {
+ throw new Error(`BrowsingContext ${contextId} does not exist.`);
+ }
+ }
+ return currentContext;
+ }
+
+ unregisterBrowsingContexts(id: string): void {
+ this.#browsingContexts.delete(id);
+ }
+
+ /**
+ * Unbinds the connection, but keeps the transport open. Useful when the transport will
+ * be reused by other connection e.g. with different protocol.
+ * @internal
+ */
+ unbind(): void {
+ if (this.#closed) {
+ return;
+ }
+ this.#closed = true;
+ // Both may still be invoked and produce errors
+ this.#transport.onmessage = () => {};
+ this.#transport.onclose = () => {};
+
+ this.#browsingContexts.clear();
+ this.#callbacks.clear();
+ }
+
+ /**
+ * Unbinds the connection and closes the transport.
+ */
+ dispose(): void {
+ this.unbind();
+ this.#transport.close();
+ }
+
+ getPendingProtocolErrors(): Error[] {
+ return this.#callbacks.getPendingProtocolErrors();
+ }
+}
+
+/**
+ * @internal
+ */
+function createProtocolError(object: Bidi.ErrorResponse): string {
+ let message = `${object.error} ${object.message}`;
+ if (object.stacktrace) {
+ message += ` ${object.stacktrace}`;
+ }
+ return message;
+}
+
+function isCdpEvent(event: Bidi.ChromiumBidi.Event): event is Bidi.Cdp.Event {
+ return event.method.startsWith('cdp.');
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts
new file mode 100644
index 0000000000..14b87d403b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts
@@ -0,0 +1,96 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {debugError} from '../common/util.js';
+
+/**
+ * @internal
+ */
+export class BidiDeserializer {
+ static deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
+ switch (value) {
+ case '-0':
+ return -0;
+ case 'NaN':
+ return NaN;
+ case 'Infinity':
+ return Infinity;
+ case '-Infinity':
+ return -Infinity;
+ default:
+ return value;
+ }
+ }
+
+ static deserializeLocalValue(result: Bidi.Script.RemoteValue): unknown {
+ switch (result.type) {
+ case 'array':
+ return result.value?.map(value => {
+ return BidiDeserializer.deserializeLocalValue(value);
+ });
+ case 'set':
+ return result.value?.reduce((acc: Set<unknown>, value) => {
+ return acc.add(BidiDeserializer.deserializeLocalValue(value));
+ }, new Set());
+ case 'object':
+ return result.value?.reduce((acc: Record<any, unknown>, tuple) => {
+ const {key, value} = BidiDeserializer.deserializeTuple(tuple);
+ acc[key as any] = value;
+ return acc;
+ }, {});
+ case 'map':
+ return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
+ const {key, value} = BidiDeserializer.deserializeTuple(tuple);
+ return acc.set(key, value);
+ }, new Map());
+ case 'promise':
+ return {};
+ case 'regexp':
+ return new RegExp(result.value.pattern, result.value.flags);
+ case 'date':
+ return new Date(result.value);
+ case 'undefined':
+ return undefined;
+ case 'null':
+ return null;
+ case 'number':
+ return BidiDeserializer.deserializeNumber(result.value);
+ case 'bigint':
+ return BigInt(result.value);
+ case 'boolean':
+ return Boolean(result.value);
+ case 'string':
+ return result.value;
+ }
+
+ debugError(`Deserialization of type ${result.type} not supported.`);
+ return undefined;
+ }
+
+ static deserializeTuple([serializedKey, serializedValue]: [
+ Bidi.Script.RemoteValue | string,
+ Bidi.Script.RemoteValue,
+ ]): {key: unknown; value: unknown} {
+ const key =
+ typeof serializedKey === 'string'
+ ? serializedKey
+ : BidiDeserializer.deserializeLocalValue(serializedKey);
+ const value = BidiDeserializer.deserializeLocalValue(serializedValue);
+
+ return {key, value};
+ }
+
+ static deserialize(result: Bidi.Script.RemoteValue): any {
+ if (!result) {
+ debugError('Service did not produce a result.');
+ return undefined;
+ }
+
+ return BidiDeserializer.deserializeLocalValue(result);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts
new file mode 100644
index 0000000000..ce22223461
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {Dialog} from '../api/Dialog.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+
+/**
+ * @internal
+ */
+export class BidiDialog extends Dialog {
+ #context: BrowsingContext;
+
+ /**
+ * @internal
+ */
+ constructor(
+ context: BrowsingContext,
+ type: Bidi.BrowsingContext.UserPromptOpenedParameters['type'],
+ message: string,
+ defaultValue?: string
+ ) {
+ super(type, message, defaultValue);
+ this.#context = context;
+ }
+
+ /**
+ * @internal
+ */
+ override async handle(options: {
+ accept: boolean;
+ text?: string;
+ }): Promise<void> {
+ await this.#context.connection.send('browsingContext.handleUserPrompt', {
+ context: this.#context.id,
+ accept: options.accept,
+ userText: options.text,
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts
new file mode 100644
index 0000000000..fd886e8c26
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {type AutofillData, ElementHandle} from '../api/ElementHandle.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import {throwIfDisposed} from '../util/decorators.js';
+
+import type {BidiFrame} from './Frame.js';
+import {BidiJSHandle} from './JSHandle.js';
+import type {BidiRealm} from './Realm.js';
+import type {Sandbox} from './Sandbox.js';
+
+/**
+ * @internal
+ */
+export class BidiElementHandle<
+ ElementType extends Node = Element,
+> extends ElementHandle<ElementType> {
+ declare handle: BidiJSHandle<ElementType>;
+
+ constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
+ super(new BidiJSHandle(sandbox, remoteValue));
+ }
+
+ override get realm(): Sandbox {
+ return this.handle.realm;
+ }
+
+ override get frame(): BidiFrame {
+ return this.realm.environment;
+ }
+
+ context(): BidiRealm {
+ return this.handle.context();
+ }
+
+ get isPrimitiveValue(): boolean {
+ return this.handle.isPrimitiveValue;
+ }
+
+ remoteValue(): Bidi.Script.RemoteValue {
+ return this.handle.remoteValue();
+ }
+
+ @throwIfDisposed()
+ override async autofill(data: AutofillData): Promise<void> {
+ const client = this.frame.client;
+ const nodeInfo = await client.send('DOM.describeNode', {
+ objectId: this.handle.id,
+ });
+ const fieldId = nodeInfo.node.backendNodeId;
+ const frameId = this.frame._id;
+ await client.send('Autofill.trigger', {
+ fieldId,
+ frameId,
+ card: data.creditCard,
+ });
+ }
+
+ override async contentFrame(
+ this: BidiElementHandle<HTMLIFrameElement>
+ ): Promise<BidiFrame>;
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ override async contentFrame(): Promise<BidiFrame | null> {
+ using handle = (await this.evaluateHandle(element => {
+ if (element instanceof HTMLIFrameElement) {
+ return element.contentWindow;
+ }
+ return;
+ })) as BidiJSHandle;
+ const value = handle.remoteValue();
+ if (value.type === 'window') {
+ return this.frame.page().frame(value.value.context);
+ }
+ return null;
+ }
+
+ override uploadFile(this: ElementHandle<HTMLInputElement>): never {
+ throw new UnsupportedOperation();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts
new file mode 100644
index 0000000000..de95695785
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {Viewport} from '../common/Viewport.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+
+/**
+ * @internal
+ */
+export class EmulationManager {
+ #browsingContext: BrowsingContext;
+
+ constructor(browsingContext: BrowsingContext) {
+ this.#browsingContext = browsingContext;
+ }
+
+ async emulateViewport(viewport: Viewport): Promise<void> {
+ await this.#browsingContext.connection.send('browsingContext.setViewport', {
+ context: this.#browsingContext.id,
+ viewport:
+ viewport.width && viewport.height
+ ? {
+ width: viewport.width,
+ height: viewport.height,
+ }
+ : null,
+ devicePixelRatio: viewport.deviceScaleFactor
+ ? viewport.deviceScaleFactor
+ : null,
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
new file mode 100644
index 0000000000..62c6b5e37e
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
@@ -0,0 +1,295 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import type {Awaitable, FlattenHandle} from '../common/types.js';
+import {debugError} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+import {interpolateFunction, stringifyFunction} from '../util/Function.js';
+
+import type {BidiConnection} from './Connection.js';
+import {BidiDeserializer} from './Deserializer.js';
+import type {BidiFrame} from './Frame.js';
+import {BidiSerializer} from './Serializer.js';
+
+type SendArgsChannel<Args> = (value: [id: number, args: Args]) => void;
+type SendResolveChannel<Ret> = (
+ value: [id: number, resolve: (ret: FlattenHandle<Awaited<Ret>>) => void]
+) => void;
+type SendRejectChannel = (
+ value: [id: number, reject: (error: unknown) => void]
+) => void;
+
+interface RemotePromiseCallbacks {
+ resolve: Deferred<Bidi.Script.RemoteValue>;
+ reject: Deferred<Bidi.Script.RemoteValue>;
+}
+
+/**
+ * @internal
+ */
+export class ExposeableFunction<Args extends unknown[], Ret> {
+ readonly #frame;
+
+ readonly name;
+ readonly #apply;
+
+ readonly #channels;
+ readonly #callerInfos = new Map<
+ string,
+ Map<number, RemotePromiseCallbacks>
+ >();
+
+ #preloadScriptId?: Bidi.Script.PreloadScript;
+
+ constructor(
+ frame: BidiFrame,
+ name: string,
+ apply: (...args: Args) => Awaitable<Ret>
+ ) {
+ this.#frame = frame;
+ this.name = name;
+ this.#apply = apply;
+
+ this.#channels = {
+ args: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_args`,
+ resolve: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_resolve`,
+ reject: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_reject`,
+ };
+ }
+
+ async expose(): Promise<void> {
+ const connection = this.#connection;
+ const channelArguments = this.#channelArguments;
+
+ // TODO(jrandolf): Implement cleanup with removePreloadScript.
+ connection.on(
+ Bidi.ChromiumBidi.Script.EventNames.Message,
+ this.#handleArgumentsMessage
+ );
+ connection.on(
+ Bidi.ChromiumBidi.Script.EventNames.Message,
+ this.#handleResolveMessage
+ );
+ connection.on(
+ Bidi.ChromiumBidi.Script.EventNames.Message,
+ this.#handleRejectMessage
+ );
+
+ const functionDeclaration = stringifyFunction(
+ interpolateFunction(
+ (
+ sendArgs: SendArgsChannel<Args>,
+ sendResolve: SendResolveChannel<Ret>,
+ sendReject: SendRejectChannel
+ ) => {
+ let id = 0;
+ Object.assign(globalThis, {
+ [PLACEHOLDER('name') as string]: function (...args: Args) {
+ return new Promise<FlattenHandle<Awaited<Ret>>>(
+ (resolve, reject) => {
+ sendArgs([id, args]);
+ sendResolve([id, resolve]);
+ sendReject([id, reject]);
+ ++id;
+ }
+ );
+ },
+ });
+ },
+ {name: JSON.stringify(this.name)}
+ )
+ );
+
+ const {result} = await connection.send('script.addPreloadScript', {
+ functionDeclaration,
+ arguments: channelArguments,
+ contexts: [this.#frame.page().mainFrame()._id],
+ });
+ this.#preloadScriptId = result.script;
+
+ await Promise.all(
+ this.#frame
+ .page()
+ .frames()
+ .map(async frame => {
+ return await connection.send('script.callFunction', {
+ functionDeclaration,
+ arguments: channelArguments,
+ awaitPromise: false,
+ target: frame.mainRealm().realm.target,
+ });
+ })
+ );
+ }
+
+ #handleArgumentsMessage = async (params: Bidi.Script.MessageParameters) => {
+ if (params.channel !== this.#channels.args) {
+ return;
+ }
+ const connection = this.#connection;
+ const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
+ const args = remoteValue.value?.[1];
+ assert(args);
+ try {
+ const result = await this.#apply(...BidiDeserializer.deserialize(args));
+ await connection.send('script.callFunction', {
+ functionDeclaration: stringifyFunction(([_, resolve]: any, result) => {
+ resolve(result);
+ }),
+ arguments: [
+ (await callbacks.resolve.valueOrThrow()) as Bidi.Script.LocalValue,
+ BidiSerializer.serializeRemoteValue(result),
+ ],
+ awaitPromise: false,
+ target: {
+ realm: params.source.realm,
+ },
+ });
+ } catch (error) {
+ try {
+ if (error instanceof Error) {
+ await connection.send('script.callFunction', {
+ functionDeclaration: stringifyFunction(
+ (
+ [_, reject]: [unknown, (error: Error) => void],
+ name: string,
+ message: string,
+ stack?: string
+ ) => {
+ const error = new Error(message);
+ error.name = name;
+ if (stack) {
+ error.stack = stack;
+ }
+ reject(error);
+ }
+ ),
+ arguments: [
+ (await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
+ BidiSerializer.serializeRemoteValue(error.name),
+ BidiSerializer.serializeRemoteValue(error.message),
+ BidiSerializer.serializeRemoteValue(error.stack),
+ ],
+ awaitPromise: false,
+ target: {
+ realm: params.source.realm,
+ },
+ });
+ } else {
+ await connection.send('script.callFunction', {
+ functionDeclaration: stringifyFunction(
+ (
+ [_, reject]: [unknown, (error: unknown) => void],
+ error: unknown
+ ) => {
+ reject(error);
+ }
+ ),
+ arguments: [
+ (await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
+ BidiSerializer.serializeRemoteValue(error),
+ ],
+ awaitPromise: false,
+ target: {
+ realm: params.source.realm,
+ },
+ });
+ }
+ } catch (error) {
+ debugError(error);
+ }
+ }
+ };
+
+ get #connection(): BidiConnection {
+ return this.#frame.context().connection;
+ }
+
+ get #channelArguments() {
+ return [
+ {
+ type: 'channel' as const,
+ value: {
+ channel: this.#channels.args,
+ ownership: Bidi.Script.ResultOwnership.Root,
+ },
+ },
+ {
+ type: 'channel' as const,
+ value: {
+ channel: this.#channels.resolve,
+ ownership: Bidi.Script.ResultOwnership.Root,
+ },
+ },
+ {
+ type: 'channel' as const,
+ value: {
+ channel: this.#channels.reject,
+ ownership: Bidi.Script.ResultOwnership.Root,
+ },
+ },
+ ];
+ }
+
+ #handleResolveMessage = (params: Bidi.Script.MessageParameters) => {
+ if (params.channel !== this.#channels.resolve) {
+ return;
+ }
+ const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
+ callbacks.resolve.resolve(remoteValue);
+ };
+
+ #handleRejectMessage = (params: Bidi.Script.MessageParameters) => {
+ if (params.channel !== this.#channels.reject) {
+ return;
+ }
+ const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
+ callbacks.reject.resolve(remoteValue);
+ };
+
+ #getCallbacksAndRemoteValue(params: Bidi.Script.MessageParameters) {
+ const {data, source} = params;
+ assert(data.type === 'array');
+ assert(data.value);
+
+ const callerIdRemote = data.value[0];
+ assert(callerIdRemote);
+ assert(callerIdRemote.type === 'number');
+ assert(typeof callerIdRemote.value === 'number');
+
+ let bindingMap = this.#callerInfos.get(source.realm);
+ if (!bindingMap) {
+ bindingMap = new Map();
+ this.#callerInfos.set(source.realm, bindingMap);
+ }
+
+ const callerId = callerIdRemote.value;
+ let callbacks = bindingMap.get(callerId);
+ if (!callbacks) {
+ callbacks = {
+ resolve: new Deferred(),
+ reject: new Deferred(),
+ };
+ bindingMap.set(callerId, callbacks);
+ }
+ return {callbacks, remoteValue: data};
+ }
+
+ [Symbol.dispose](): void {
+ void this[Symbol.asyncDispose]().catch(debugError);
+ }
+
+ async [Symbol.asyncDispose](): Promise<void> {
+ if (this.#preloadScriptId) {
+ await this.#connection.send('script.removePreloadScript', {
+ script: this.#preloadScriptId,
+ });
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts
new file mode 100644
index 0000000000..1638c2cbdf
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts
@@ -0,0 +1,313 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {
+ first,
+ firstValueFrom,
+ forkJoin,
+ from,
+ map,
+ merge,
+ raceWith,
+ zip,
+} from '../../third_party/rxjs/rxjs.js';
+import type {CDPSession} from '../api/CDPSession.js';
+import type {ElementHandle} from '../api/ElementHandle.js';
+import {
+ Frame,
+ throwIfDetached,
+ type GoToOptions,
+ type WaitForOptions,
+} from '../api/Frame.js';
+import type {WaitForSelectorOptions} from '../api/Page.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import type {TimeoutSettings} from '../common/TimeoutSettings.js';
+import type {Awaitable, NodeFor} from '../common/types.js';
+import {
+ fromEmitterEvent,
+ NETWORK_IDLE_TIME,
+ timeout,
+ UTILITY_WORLD_NAME,
+} from '../common/util.js';
+import {Deferred} from '../util/Deferred.js';
+import {disposeSymbol} from '../util/disposable.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+import {ExposeableFunction} from './ExposedFunction.js';
+import type {BidiHTTPResponse} from './HTTPResponse.js';
+import {
+ getBiDiLifecycleEvent,
+ getBiDiReadinessState,
+ rewriteNavigationError,
+} from './lifecycle.js';
+import type {BidiPage} from './Page.js';
+import {
+ MAIN_SANDBOX,
+ PUPPETEER_SANDBOX,
+ Sandbox,
+ type SandboxChart,
+} from './Sandbox.js';
+
+/**
+ * Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation
+ * @internal
+ */
+export class BidiFrame extends Frame {
+ #page: BidiPage;
+ #context: BrowsingContext;
+ #timeoutSettings: TimeoutSettings;
+ #abortDeferred = Deferred.create<never>();
+ #disposed = false;
+ sandboxes: SandboxChart;
+ override _id: string;
+
+ constructor(
+ page: BidiPage,
+ context: BrowsingContext,
+ timeoutSettings: TimeoutSettings,
+ parentId?: string | null
+ ) {
+ super();
+ this.#page = page;
+ this.#context = context;
+ this.#timeoutSettings = timeoutSettings;
+ this._id = this.#context.id;
+ this._parentId = parentId ?? undefined;
+
+ this.sandboxes = {
+ [MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings),
+ [PUPPETEER_SANDBOX]: new Sandbox(
+ UTILITY_WORLD_NAME,
+ this,
+ context.createRealmForSandbox(),
+ timeoutSettings
+ ),
+ };
+ }
+
+ override get client(): CDPSession {
+ return this.context().cdpSession;
+ }
+
+ override mainRealm(): Sandbox {
+ return this.sandboxes[MAIN_SANDBOX];
+ }
+
+ override isolatedRealm(): Sandbox {
+ return this.sandboxes[PUPPETEER_SANDBOX];
+ }
+
+ override page(): BidiPage {
+ return this.#page;
+ }
+
+ override isOOPFrame(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override url(): string {
+ return this.#context.url;
+ }
+
+ override parentFrame(): BidiFrame | null {
+ return this.#page.frame(this._parentId ?? '');
+ }
+
+ override childFrames(): BidiFrame[] {
+ return this.#page.childFrames(this.#context.id);
+ }
+
+ @throwIfDetached
+ override async goto(
+ url: string,
+ options: GoToOptions = {}
+ ): Promise<BidiHTTPResponse | null> {
+ const {
+ waitUntil = 'load',
+ timeout: ms = this.#timeoutSettings.navigationTimeout(),
+ } = options;
+
+ const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
+
+ const result$ = zip(
+ from(
+ this.#context.connection.send('browsingContext.navigate', {
+ context: this.#context.id,
+ url,
+ wait: readiness,
+ })
+ ),
+ ...(networkIdle !== null
+ ? [
+ this.#page.waitForNetworkIdle$({
+ timeout: ms,
+ concurrency: networkIdle === 'networkidle2' ? 2 : 0,
+ idleTime: NETWORK_IDLE_TIME,
+ }),
+ ]
+ : [])
+ ).pipe(
+ map(([{result}]) => {
+ return result;
+ }),
+ raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
+ rewriteNavigationError(url, ms)
+ );
+
+ const result = await firstValueFrom(result$);
+ return this.#page.getNavigationResponse(result.navigation);
+ }
+
+ @throwIfDetached
+ override async setContent(
+ html: string,
+ options: WaitForOptions = {}
+ ): Promise<void> {
+ const {
+ waitUntil = 'load',
+ timeout: ms = this.#timeoutSettings.navigationTimeout(),
+ } = options;
+
+ const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
+
+ const result$ = zip(
+ forkJoin([
+ fromEmitterEvent(this.#context, waitEvent).pipe(first()),
+ from(this.setFrameContent(html)),
+ ]).pipe(
+ map(() => {
+ return null;
+ })
+ ),
+ ...(networkIdle !== null
+ ? [
+ this.#page.waitForNetworkIdle$({
+ timeout: ms,
+ concurrency: networkIdle === 'networkidle2' ? 2 : 0,
+ idleTime: NETWORK_IDLE_TIME,
+ }),
+ ]
+ : [])
+ ).pipe(
+ raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
+ rewriteNavigationError('setContent', ms)
+ );
+
+ await firstValueFrom(result$);
+ }
+
+ context(): BrowsingContext {
+ return this.#context;
+ }
+
+ @throwIfDetached
+ override async waitForNavigation(
+ options: WaitForOptions = {}
+ ): Promise<BidiHTTPResponse | null> {
+ const {
+ waitUntil = 'load',
+ timeout: ms = this.#timeoutSettings.navigationTimeout(),
+ } = options;
+
+ const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
+
+ const navigation$ = merge(
+ forkJoin([
+ fromEmitterEvent(
+ this.#context,
+ Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted
+ ).pipe(first()),
+ fromEmitterEvent(this.#context, waitUntilEvent).pipe(first()),
+ ]),
+ fromEmitterEvent(
+ this.#context,
+ Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated
+ )
+ ).pipe(
+ map(result => {
+ if (Array.isArray(result)) {
+ return {result: result[1]};
+ }
+ return {result};
+ })
+ );
+
+ const result$ = zip(
+ navigation$,
+ ...(networkIdle !== null
+ ? [
+ this.#page.waitForNetworkIdle$({
+ timeout: ms,
+ concurrency: networkIdle === 'networkidle2' ? 2 : 0,
+ idleTime: NETWORK_IDLE_TIME,
+ }),
+ ]
+ : [])
+ ).pipe(
+ map(([{result}]) => {
+ return result;
+ }),
+ raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))
+ );
+
+ const result = await firstValueFrom(result$);
+ return this.#page.getNavigationResponse(result.navigation);
+ }
+
+ override waitForDevicePrompt(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override get detached(): boolean {
+ return this.#disposed;
+ }
+
+ [disposeSymbol](): void {
+ if (this.#disposed) {
+ return;
+ }
+ this.#disposed = true;
+ this.#abortDeferred.reject(new Error('Frame detached'));
+ this.#context.dispose();
+ this.sandboxes[MAIN_SANDBOX][disposeSymbol]();
+ this.sandboxes[PUPPETEER_SANDBOX][disposeSymbol]();
+ }
+
+ #exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>();
+ async exposeFunction<Args extends unknown[], Ret>(
+ name: string,
+ apply: (...args: Args) => Awaitable<Ret>
+ ): Promise<void> {
+ if (this.#exposedFunctions.has(name)) {
+ throw new Error(
+ `Failed to add page binding with name ${name}: globalThis['${name}'] already exists!`
+ );
+ }
+ const exposeable = new ExposeableFunction(this, name, apply);
+ this.#exposedFunctions.set(name, exposeable);
+ try {
+ await exposeable.expose();
+ } catch (error) {
+ this.#exposedFunctions.delete(name);
+ throw error;
+ }
+ }
+
+ override waitForSelector<Selector extends string>(
+ selector: Selector,
+ options?: WaitForSelectorOptions
+ ): Promise<ElementHandle<NodeFor<Selector>> | null> {
+ if (selector.startsWith('aria')) {
+ throw new UnsupportedOperation(
+ 'ARIA selector is not supported for BiDi!'
+ );
+ }
+
+ return super.waitForSelector(selector, options);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts
new file mode 100644
index 0000000000..57cb801b8c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts
@@ -0,0 +1,163 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import type {Frame} from '../api/Frame.js';
+import type {
+ ContinueRequestOverrides,
+ ResponseForRequest,
+} from '../api/HTTPRequest.js';
+import {HTTPRequest, type ResourceType} from '../api/HTTPRequest.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+
+import type {BidiHTTPResponse} from './HTTPResponse.js';
+
+/**
+ * @internal
+ */
+export class BidiHTTPRequest extends HTTPRequest {
+ override _response: BidiHTTPResponse | null = null;
+ override _redirectChain: BidiHTTPRequest[];
+ _navigationId: string | null;
+
+ #url: string;
+ #resourceType: ResourceType;
+
+ #method: string;
+ #postData?: string;
+ #headers: Record<string, string> = {};
+ #initiator: Bidi.Network.Initiator;
+ #frame: Frame | null;
+
+ constructor(
+ event: Bidi.Network.BeforeRequestSentParameters,
+ frame: Frame | null,
+ redirectChain: BidiHTTPRequest[] = []
+ ) {
+ super();
+
+ this.#url = event.request.url;
+ this.#resourceType = event.initiator.type.toLowerCase() as ResourceType;
+ this.#method = event.request.method;
+ this.#postData = undefined;
+ this.#initiator = event.initiator;
+ this.#frame = frame;
+
+ this._requestId = event.request.request;
+ this._redirectChain = redirectChain;
+ this._navigationId = event.navigation;
+
+ for (const header of event.request.headers) {
+ // TODO: How to handle Binary Headers
+ // https://w3c.github.io/webdriver-bidi/#type-network-Header
+ if (header.value.type === 'string') {
+ this.#headers[header.name.toLowerCase()] = header.value.value;
+ }
+ }
+ }
+
+ override get client(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override url(): string {
+ return this.#url;
+ }
+
+ override resourceType(): ResourceType {
+ return this.#resourceType;
+ }
+
+ override method(): string {
+ return this.#method;
+ }
+
+ override postData(): string | undefined {
+ return this.#postData;
+ }
+
+ override hasPostData(): boolean {
+ return this.#postData !== undefined;
+ }
+
+ override async fetchPostData(): Promise<string | undefined> {
+ return this.#postData;
+ }
+
+ override headers(): Record<string, string> {
+ return this.#headers;
+ }
+
+ override response(): BidiHTTPResponse | null {
+ return this._response;
+ }
+
+ override isNavigationRequest(): boolean {
+ return Boolean(this._navigationId);
+ }
+
+ override initiator(): Bidi.Network.Initiator {
+ return this.#initiator;
+ }
+
+ override redirectChain(): BidiHTTPRequest[] {
+ return this._redirectChain.slice();
+ }
+
+ override enqueueInterceptAction(
+ pendingHandler: () => void | PromiseLike<unknown>
+ ): void {
+ // Execute the handler when interception is not supported
+ void pendingHandler();
+ }
+
+ override frame(): Frame | null {
+ return this.#frame;
+ }
+
+ override continueRequestOverrides(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override continue(_overrides: ContinueRequestOverrides = {}): never {
+ throw new UnsupportedOperation();
+ }
+
+ override responseForRequest(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override abortErrorReason(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override interceptResolutionState(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override isInterceptResolutionHandled(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override finalizeInterceptions(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override abort(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override respond(
+ _response: Partial<ResponseForRequest>,
+ _priority?: number
+ ): never {
+ throw new UnsupportedOperation();
+ }
+
+ override failure(): never {
+ throw new UnsupportedOperation();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts
new file mode 100644
index 0000000000..ce28820a65
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts
@@ -0,0 +1,107 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type Protocol from 'devtools-protocol';
+
+import type {Frame} from '../api/Frame.js';
+import {
+ HTTPResponse as HTTPResponse,
+ type RemoteAddress,
+} from '../api/HTTPResponse.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+
+import type {BidiHTTPRequest} from './HTTPRequest.js';
+
+/**
+ * @internal
+ */
+export class BidiHTTPResponse extends HTTPResponse {
+ #request: BidiHTTPRequest;
+ #remoteAddress: RemoteAddress;
+ #status: number;
+ #statusText: string;
+ #url: string;
+ #fromCache: boolean;
+ #headers: Record<string, string> = {};
+ #timings: Record<string, string> | null;
+
+ constructor(
+ request: BidiHTTPRequest,
+ {response}: Bidi.Network.ResponseCompletedParameters
+ ) {
+ super();
+ this.#request = request;
+
+ this.#remoteAddress = {
+ ip: '',
+ port: -1,
+ };
+
+ this.#url = response.url;
+ this.#fromCache = response.fromCache;
+ this.#status = response.status;
+ this.#statusText = response.statusText;
+ // TODO: File and issue with BiDi spec
+ this.#timings = null;
+
+ // TODO: Removed once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
+ for (const header of response.headers || []) {
+ // TODO: How to handle Binary Headers
+ // https://w3c.github.io/webdriver-bidi/#type-network-Header
+ if (header.value.type === 'string') {
+ this.#headers[header.name.toLowerCase()] = header.value.value;
+ }
+ }
+ }
+
+ override remoteAddress(): RemoteAddress {
+ return this.#remoteAddress;
+ }
+
+ override url(): string {
+ return this.#url;
+ }
+
+ override status(): number {
+ return this.#status;
+ }
+
+ override statusText(): string {
+ return this.#statusText;
+ }
+
+ override headers(): Record<string, string> {
+ return this.#headers;
+ }
+
+ override request(): BidiHTTPRequest {
+ return this.#request;
+ }
+
+ override fromCache(): boolean {
+ return this.#fromCache;
+ }
+
+ override timing(): Protocol.Network.ResourceTiming | null {
+ return this.#timings as any;
+ }
+
+ override frame(): Frame | null {
+ return this.#request.frame();
+ }
+
+ override fromServiceWorker(): boolean {
+ return false;
+ }
+
+ override securityDetails(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override buffer(): never {
+ throw new UnsupportedOperation();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts
new file mode 100644
index 0000000000..5406556d64
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts
@@ -0,0 +1,732 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import type {Point} from '../api/ElementHandle.js';
+import {
+ Keyboard,
+ Mouse,
+ MouseButton,
+ Touchscreen,
+ type KeyDownOptions,
+ type KeyPressOptions,
+ type KeyboardTypeOptions,
+ type MouseClickOptions,
+ type MouseMoveOptions,
+ type MouseOptions,
+ type MouseWheelOptions,
+} from '../api/Input.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import type {KeyInput} from '../common/USKeyboardLayout.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+import type {BidiPage} from './Page.js';
+
+const enum InputId {
+ Mouse = '__puppeteer_mouse',
+ Keyboard = '__puppeteer_keyboard',
+ Wheel = '__puppeteer_wheel',
+ Finger = '__puppeteer_finger',
+}
+
+enum SourceActionsType {
+ None = 'none',
+ Key = 'key',
+ Pointer = 'pointer',
+ Wheel = 'wheel',
+}
+
+enum ActionType {
+ Pause = 'pause',
+ KeyDown = 'keyDown',
+ KeyUp = 'keyUp',
+ PointerUp = 'pointerUp',
+ PointerDown = 'pointerDown',
+ PointerMove = 'pointerMove',
+ Scroll = 'scroll',
+}
+
+const getBidiKeyValue = (key: KeyInput) => {
+ switch (key) {
+ case '\r':
+ case '\n':
+ key = 'Enter';
+ break;
+ }
+ // Measures the number of code points rather than UTF-16 code units.
+ if ([...key].length === 1) {
+ return key;
+ }
+ switch (key) {
+ case 'Cancel':
+ return '\uE001';
+ case 'Help':
+ return '\uE002';
+ case 'Backspace':
+ return '\uE003';
+ case 'Tab':
+ return '\uE004';
+ case 'Clear':
+ return '\uE005';
+ case 'Enter':
+ return '\uE007';
+ case 'Shift':
+ case 'ShiftLeft':
+ return '\uE008';
+ case 'Control':
+ case 'ControlLeft':
+ return '\uE009';
+ case 'Alt':
+ case 'AltLeft':
+ return '\uE00A';
+ case 'Pause':
+ return '\uE00B';
+ case 'Escape':
+ return '\uE00C';
+ case 'PageUp':
+ return '\uE00E';
+ case 'PageDown':
+ return '\uE00F';
+ case 'End':
+ return '\uE010';
+ case 'Home':
+ return '\uE011';
+ case 'ArrowLeft':
+ return '\uE012';
+ case 'ArrowUp':
+ return '\uE013';
+ case 'ArrowRight':
+ return '\uE014';
+ case 'ArrowDown':
+ return '\uE015';
+ case 'Insert':
+ return '\uE016';
+ case 'Delete':
+ return '\uE017';
+ case 'NumpadEqual':
+ return '\uE019';
+ case 'Numpad0':
+ return '\uE01A';
+ case 'Numpad1':
+ return '\uE01B';
+ case 'Numpad2':
+ return '\uE01C';
+ case 'Numpad3':
+ return '\uE01D';
+ case 'Numpad4':
+ return '\uE01E';
+ case 'Numpad5':
+ return '\uE01F';
+ case 'Numpad6':
+ return '\uE020';
+ case 'Numpad7':
+ return '\uE021';
+ case 'Numpad8':
+ return '\uE022';
+ case 'Numpad9':
+ return '\uE023';
+ case 'NumpadMultiply':
+ return '\uE024';
+ case 'NumpadAdd':
+ return '\uE025';
+ case 'NumpadSubtract':
+ return '\uE027';
+ case 'NumpadDecimal':
+ return '\uE028';
+ case 'NumpadDivide':
+ return '\uE029';
+ case 'F1':
+ return '\uE031';
+ case 'F2':
+ return '\uE032';
+ case 'F3':
+ return '\uE033';
+ case 'F4':
+ return '\uE034';
+ case 'F5':
+ return '\uE035';
+ case 'F6':
+ return '\uE036';
+ case 'F7':
+ return '\uE037';
+ case 'F8':
+ return '\uE038';
+ case 'F9':
+ return '\uE039';
+ case 'F10':
+ return '\uE03A';
+ case 'F11':
+ return '\uE03B';
+ case 'F12':
+ return '\uE03C';
+ case 'Meta':
+ case 'MetaLeft':
+ return '\uE03D';
+ case 'ShiftRight':
+ return '\uE050';
+ case 'ControlRight':
+ return '\uE051';
+ case 'AltRight':
+ return '\uE052';
+ case 'MetaRight':
+ return '\uE053';
+ case 'Digit0':
+ return '0';
+ case 'Digit1':
+ return '1';
+ case 'Digit2':
+ return '2';
+ case 'Digit3':
+ return '3';
+ case 'Digit4':
+ return '4';
+ case 'Digit5':
+ return '5';
+ case 'Digit6':
+ return '6';
+ case 'Digit7':
+ return '7';
+ case 'Digit8':
+ return '8';
+ case 'Digit9':
+ return '9';
+ case 'KeyA':
+ return 'a';
+ case 'KeyB':
+ return 'b';
+ case 'KeyC':
+ return 'c';
+ case 'KeyD':
+ return 'd';
+ case 'KeyE':
+ return 'e';
+ case 'KeyF':
+ return 'f';
+ case 'KeyG':
+ return 'g';
+ case 'KeyH':
+ return 'h';
+ case 'KeyI':
+ return 'i';
+ case 'KeyJ':
+ return 'j';
+ case 'KeyK':
+ return 'k';
+ case 'KeyL':
+ return 'l';
+ case 'KeyM':
+ return 'm';
+ case 'KeyN':
+ return 'n';
+ case 'KeyO':
+ return 'o';
+ case 'KeyP':
+ return 'p';
+ case 'KeyQ':
+ return 'q';
+ case 'KeyR':
+ return 'r';
+ case 'KeyS':
+ return 's';
+ case 'KeyT':
+ return 't';
+ case 'KeyU':
+ return 'u';
+ case 'KeyV':
+ return 'v';
+ case 'KeyW':
+ return 'w';
+ case 'KeyX':
+ return 'x';
+ case 'KeyY':
+ return 'y';
+ case 'KeyZ':
+ return 'z';
+ case 'Semicolon':
+ return ';';
+ case 'Equal':
+ return '=';
+ case 'Comma':
+ return ',';
+ case 'Minus':
+ return '-';
+ case 'Period':
+ return '.';
+ case 'Slash':
+ return '/';
+ case 'Backquote':
+ return '`';
+ case 'BracketLeft':
+ return '[';
+ case 'Backslash':
+ return '\\';
+ case 'BracketRight':
+ return ']';
+ case 'Quote':
+ return '"';
+ default:
+ throw new Error(`Unknown key: "${key}"`);
+ }
+};
+
+/**
+ * @internal
+ */
+export class BidiKeyboard extends Keyboard {
+ #page: BidiPage;
+
+ constructor(page: BidiPage) {
+ super();
+ this.#page = page;
+ }
+
+ override async down(
+ key: KeyInput,
+ _options?: Readonly<KeyDownOptions>
+ ): Promise<void> {
+ await this.#page.connection.send('input.performActions', {
+ context: this.#page.mainFrame()._id,
+ actions: [
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions: [
+ {
+ type: ActionType.KeyDown,
+ value: getBidiKeyValue(key),
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+ override async up(key: KeyInput): Promise<void> {
+ await this.#page.connection.send('input.performActions', {
+ context: this.#page.mainFrame()._id,
+ actions: [
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions: [
+ {
+ type: ActionType.KeyUp,
+ value: getBidiKeyValue(key),
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+ override async press(
+ key: KeyInput,
+ options: Readonly<KeyPressOptions> = {}
+ ): Promise<void> {
+ const {delay = 0} = options;
+ const actions: Bidi.Input.KeySourceAction[] = [
+ {
+ type: ActionType.KeyDown,
+ value: getBidiKeyValue(key),
+ },
+ ];
+ if (delay > 0) {
+ actions.push({
+ type: ActionType.Pause,
+ duration: delay,
+ });
+ }
+ actions.push({
+ type: ActionType.KeyUp,
+ value: getBidiKeyValue(key),
+ });
+ await this.#page.connection.send('input.performActions', {
+ context: this.#page.mainFrame()._id,
+ actions: [
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions,
+ },
+ ],
+ });
+ }
+
+ override async type(
+ text: string,
+ options: Readonly<KeyboardTypeOptions> = {}
+ ): Promise<void> {
+ const {delay = 0} = options;
+ // This spread separates the characters into code points rather than UTF-16
+ // code units.
+ const values = ([...text] as KeyInput[]).map(getBidiKeyValue);
+ const actions: Bidi.Input.KeySourceAction[] = [];
+ if (delay <= 0) {
+ for (const value of values) {
+ actions.push(
+ {
+ type: ActionType.KeyDown,
+ value,
+ },
+ {
+ type: ActionType.KeyUp,
+ value,
+ }
+ );
+ }
+ } else {
+ for (const value of values) {
+ actions.push(
+ {
+ type: ActionType.KeyDown,
+ value,
+ },
+ {
+ type: ActionType.Pause,
+ duration: delay,
+ },
+ {
+ type: ActionType.KeyUp,
+ value,
+ }
+ );
+ }
+ }
+ await this.#page.connection.send('input.performActions', {
+ context: this.#page.mainFrame()._id,
+ actions: [
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions,
+ },
+ ],
+ });
+ }
+
+ override async sendCharacter(char: string): Promise<void> {
+ // Measures the number of code points rather than UTF-16 code units.
+ if ([...char].length > 1) {
+ throw new Error('Cannot send more than 1 character.');
+ }
+ const frame = await this.#page.focusedFrame();
+ await frame.isolatedRealm().evaluate(async char => {
+ document.execCommand('insertText', false, char);
+ }, char);
+ }
+}
+
+/**
+ * @internal
+ */
+export interface BidiMouseClickOptions extends MouseClickOptions {
+ origin?: Bidi.Input.Origin;
+}
+
+/**
+ * @internal
+ */
+export interface BidiMouseMoveOptions extends MouseMoveOptions {
+ origin?: Bidi.Input.Origin;
+}
+
+/**
+ * @internal
+ */
+export interface BidiTouchMoveOptions {
+ origin?: Bidi.Input.Origin;
+}
+
+const getBidiButton = (button: MouseButton) => {
+ switch (button) {
+ case MouseButton.Left:
+ return 0;
+ case MouseButton.Middle:
+ return 1;
+ case MouseButton.Right:
+ return 2;
+ case MouseButton.Back:
+ return 3;
+ case MouseButton.Forward:
+ return 4;
+ }
+};
+
+/**
+ * @internal
+ */
+export class BidiMouse extends Mouse {
+ #context: BrowsingContext;
+ #lastMovePoint: Point = {x: 0, y: 0};
+
+ constructor(context: BrowsingContext) {
+ super();
+ this.#context = context;
+ }
+
+ override async reset(): Promise<void> {
+ this.#lastMovePoint = {x: 0, y: 0};
+ await this.#context.connection.send('input.releaseActions', {
+ context: this.#context.id,
+ });
+ }
+
+ override async move(
+ x: number,
+ y: number,
+ options: Readonly<BidiMouseMoveOptions> = {}
+ ): Promise<void> {
+ const from = this.#lastMovePoint;
+ const to = {
+ x: Math.round(x),
+ y: Math.round(y),
+ };
+ const actions: Bidi.Input.PointerSourceAction[] = [];
+ const steps = options.steps ?? 0;
+ for (let i = 0; i < steps; ++i) {
+ actions.push({
+ type: ActionType.PointerMove,
+ x: from.x + (to.x - from.x) * (i / steps),
+ y: from.y + (to.y - from.y) * (i / steps),
+ origin: options.origin,
+ });
+ }
+ actions.push({
+ type: ActionType.PointerMove,
+ ...to,
+ origin: options.origin,
+ });
+ // https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
+ this.#lastMovePoint = to;
+ await this.#context.connection.send('input.performActions', {
+ context: this.#context.id,
+ actions: [
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions,
+ },
+ ],
+ });
+ }
+
+ override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
+ await this.#context.connection.send('input.performActions', {
+ context: this.#context.id,
+ actions: [
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions: [
+ {
+ type: ActionType.PointerDown,
+ button: getBidiButton(options.button ?? MouseButton.Left),
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+ override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
+ await this.#context.connection.send('input.performActions', {
+ context: this.#context.id,
+ actions: [
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions: [
+ {
+ type: ActionType.PointerUp,
+ button: getBidiButton(options.button ?? MouseButton.Left),
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+ override async click(
+ x: number,
+ y: number,
+ options: Readonly<BidiMouseClickOptions> = {}
+ ): Promise<void> {
+ const actions: Bidi.Input.PointerSourceAction[] = [
+ {
+ type: ActionType.PointerMove,
+ x: Math.round(x),
+ y: Math.round(y),
+ origin: options.origin,
+ },
+ ];
+ const pointerDownAction = {
+ type: ActionType.PointerDown,
+ button: getBidiButton(options.button ?? MouseButton.Left),
+ } as const;
+ const pointerUpAction = {
+ type: ActionType.PointerUp,
+ button: pointerDownAction.button,
+ } as const;
+ for (let i = 1; i < (options.count ?? 1); ++i) {
+ actions.push(pointerDownAction, pointerUpAction);
+ }
+ actions.push(pointerDownAction);
+ if (options.delay) {
+ actions.push({
+ type: ActionType.Pause,
+ duration: options.delay,
+ });
+ }
+ actions.push(pointerUpAction);
+ await this.#context.connection.send('input.performActions', {
+ context: this.#context.id,
+ actions: [
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions,
+ },
+ ],
+ });
+ }
+
+ override async wheel(
+ options: Readonly<MouseWheelOptions> = {}
+ ): Promise<void> {
+ await this.#context.connection.send('input.performActions', {
+ context: this.#context.id,
+ actions: [
+ {
+ type: SourceActionsType.Wheel,
+ id: InputId.Wheel,
+ actions: [
+ {
+ type: ActionType.Scroll,
+ ...(this.#lastMovePoint ?? {
+ x: 0,
+ y: 0,
+ }),
+ deltaX: options.deltaX ?? 0,
+ deltaY: options.deltaY ?? 0,
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+ override drag(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override dragOver(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override dragEnter(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override drop(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override dragAndDrop(): never {
+ throw new UnsupportedOperation();
+ }
+}
+
+/**
+ * @internal
+ */
+export class BidiTouchscreen extends Touchscreen {
+ #context: BrowsingContext;
+
+ constructor(context: BrowsingContext) {
+ super();
+ this.#context = context;
+ }
+
+ override async touchStart(
+ x: number,
+ y: number,
+ options: BidiTouchMoveOptions = {}
+ ): Promise<void> {
+ await this.#context.connection.send('input.performActions', {
+ context: this.#context.id,
+ actions: [
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
+ },
+ actions: [
+ {
+ type: ActionType.PointerMove,
+ x: Math.round(x),
+ y: Math.round(y),
+ origin: options.origin,
+ },
+ {
+ type: ActionType.PointerDown,
+ button: 0,
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+ override async touchMove(
+ x: number,
+ y: number,
+ options: BidiTouchMoveOptions = {}
+ ): Promise<void> {
+ await this.#context.connection.send('input.performActions', {
+ context: this.#context.id,
+ actions: [
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
+ },
+ actions: [
+ {
+ type: ActionType.PointerMove,
+ x: Math.round(x),
+ y: Math.round(y),
+ origin: options.origin,
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+ override async touchEnd(): Promise<void> {
+ await this.#context.connection.send('input.performActions', {
+ context: this.#context.id,
+ actions: [
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
+ },
+ actions: [
+ {
+ type: ActionType.PointerUp,
+ button: 0,
+ },
+ ],
+ },
+ ],
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts
new file mode 100644
index 0000000000..7104601553
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import {JSHandle} from '../api/JSHandle.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+
+import {BidiDeserializer} from './Deserializer.js';
+import type {BidiRealm} from './Realm.js';
+import type {Sandbox} from './Sandbox.js';
+import {releaseReference} from './util.js';
+
+/**
+ * @internal
+ */
+export class BidiJSHandle<T = unknown> extends JSHandle<T> {
+ #disposed = false;
+ readonly #sandbox: Sandbox;
+ readonly #remoteValue: Bidi.Script.RemoteValue;
+
+ constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
+ super();
+ this.#sandbox = sandbox;
+ this.#remoteValue = remoteValue;
+ }
+
+ context(): BidiRealm {
+ return this.realm.environment.context();
+ }
+
+ override get realm(): Sandbox {
+ return this.#sandbox;
+ }
+
+ override get disposed(): boolean {
+ return this.#disposed;
+ }
+
+ override async jsonValue(): Promise<T> {
+ return await this.evaluate(value => {
+ return value;
+ });
+ }
+
+ override asElement(): ElementHandle<Node> | null {
+ return null;
+ }
+
+ override async dispose(): Promise<void> {
+ if (this.#disposed) {
+ return;
+ }
+ this.#disposed = true;
+ if ('handle' in this.#remoteValue) {
+ await releaseReference(
+ this.context(),
+ this.#remoteValue as Bidi.Script.RemoteReference
+ );
+ }
+ }
+
+ get isPrimitiveValue(): boolean {
+ switch (this.#remoteValue.type) {
+ case 'string':
+ case 'number':
+ case 'bigint':
+ case 'boolean':
+ case 'undefined':
+ case 'null':
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ override toString(): string {
+ if (this.isPrimitiveValue) {
+ return 'JSHandle:' + BidiDeserializer.deserialize(this.#remoteValue);
+ }
+
+ return 'JSHandle@' + this.#remoteValue.type;
+ }
+
+ override get id(): string | undefined {
+ return 'handle' in this.#remoteValue ? this.#remoteValue.handle : undefined;
+ }
+
+ remoteValue(): Bidi.Script.RemoteValue {
+ return this.#remoteValue;
+ }
+
+ override remoteObject(): never {
+ throw new UnsupportedOperation('Not available in WebDriver BiDi');
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts
new file mode 100644
index 0000000000..2caaf0ad50
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts
@@ -0,0 +1,155 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
+import {
+ NetworkManagerEvent,
+ type NetworkManagerEvents,
+} from '../common/NetworkManagerEvents.js';
+import {DisposableStack} from '../util/disposable.js';
+
+import type {BidiConnection} from './Connection.js';
+import type {BidiFrame} from './Frame.js';
+import {BidiHTTPRequest} from './HTTPRequest.js';
+import {BidiHTTPResponse} from './HTTPResponse.js';
+import type {BidiPage} from './Page.js';
+
+/**
+ * @internal
+ */
+export class BidiNetworkManager extends EventEmitter<NetworkManagerEvents> {
+ #connection: BidiConnection;
+ #page: BidiPage;
+ #subscriptions = new DisposableStack();
+
+ #requestMap = new Map<string, BidiHTTPRequest>();
+ #navigationMap = new Map<string, BidiHTTPResponse>();
+
+ constructor(connection: BidiConnection, page: BidiPage) {
+ super();
+ this.#connection = connection;
+ this.#page = page;
+
+ // TODO: Subscribe to the Frame individually
+ this.#subscriptions.use(
+ new EventSubscription(
+ this.#connection,
+ 'network.beforeRequestSent',
+ this.#onBeforeRequestSent.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ this.#connection,
+ 'network.responseStarted',
+ this.#onResponseStarted.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ this.#connection,
+ 'network.responseCompleted',
+ this.#onResponseCompleted.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ this.#connection,
+ 'network.fetchError',
+ this.#onFetchError.bind(this)
+ )
+ );
+ }
+
+ #onBeforeRequestSent(event: Bidi.Network.BeforeRequestSentParameters): void {
+ const frame = this.#page.frame(event.context ?? '');
+ if (!frame) {
+ return;
+ }
+ const request = this.#requestMap.get(event.request.request);
+ let upsertRequest: BidiHTTPRequest;
+ if (request) {
+ request._redirectChain.push(request);
+ upsertRequest = new BidiHTTPRequest(event, frame, request._redirectChain);
+ } else {
+ upsertRequest = new BidiHTTPRequest(event, frame, []);
+ }
+ this.#requestMap.set(event.request.request, upsertRequest);
+ this.emit(NetworkManagerEvent.Request, upsertRequest);
+ }
+
+ #onResponseStarted(_event: Bidi.Network.ResponseStartedParameters) {}
+
+ #onResponseCompleted(event: Bidi.Network.ResponseCompletedParameters): void {
+ const request = this.#requestMap.get(event.request.request);
+ if (!request) {
+ return;
+ }
+ const response = new BidiHTTPResponse(request, event);
+ request._response = response;
+ if (event.navigation) {
+ this.#navigationMap.set(event.navigation, response);
+ }
+ if (response.fromCache()) {
+ this.emit(NetworkManagerEvent.RequestServedFromCache, request);
+ }
+ this.emit(NetworkManagerEvent.Response, response);
+ this.emit(NetworkManagerEvent.RequestFinished, request);
+ }
+
+ #onFetchError(event: Bidi.Network.FetchErrorParameters) {
+ const request = this.#requestMap.get(event.request.request);
+ if (!request) {
+ return;
+ }
+ request._failureText = event.errorText;
+ this.emit(NetworkManagerEvent.RequestFailed, request);
+ this.#requestMap.delete(event.request.request);
+ }
+
+ getNavigationResponse(navigationId?: string | null): BidiHTTPResponse | null {
+ if (!navigationId) {
+ return null;
+ }
+ const response = this.#navigationMap.get(navigationId);
+
+ return response ?? null;
+ }
+
+ inFlightRequestsCount(): number {
+ let inFlightRequestCounter = 0;
+ for (const request of this.#requestMap.values()) {
+ if (!request.response() || request._failureText) {
+ inFlightRequestCounter++;
+ }
+ }
+
+ return inFlightRequestCounter;
+ }
+
+ clearMapAfterFrameDispose(frame: BidiFrame): void {
+ for (const [id, request] of this.#requestMap.entries()) {
+ if (request.frame() === frame) {
+ this.#requestMap.delete(id);
+ }
+ }
+
+ for (const [id, response] of this.#navigationMap.entries()) {
+ if (response.frame() === frame) {
+ this.#navigationMap.delete(id);
+ }
+ }
+ }
+
+ dispose(): void {
+ this.removeAllListeners();
+ this.#requestMap.clear();
+ this.#navigationMap.clear();
+ this.#subscriptions.dispose();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
new file mode 100644
index 0000000000..053d23b63a
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
@@ -0,0 +1,913 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Readable} from 'stream';
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type Protocol from 'devtools-protocol';
+
+import {
+ firstValueFrom,
+ from,
+ map,
+ raceWith,
+ zip,
+} from '../../third_party/rxjs/rxjs.js';
+import type {CDPSession} from '../api/CDPSession.js';
+import type {BoundingBox} from '../api/ElementHandle.js';
+import type {WaitForOptions} from '../api/Frame.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+import {
+ Page,
+ PageEvent,
+ type GeolocationOptions,
+ type MediaFeature,
+ type NewDocumentScriptEvaluation,
+ type ScreenshotOptions,
+} from '../api/Page.js';
+import {Accessibility} from '../cdp/Accessibility.js';
+import {Coverage} from '../cdp/Coverage.js';
+import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js';
+import {FrameTree} from '../cdp/FrameTree.js';
+import {Tracing} from '../cdp/Tracing.js';
+import {
+ ConsoleMessage,
+ type ConsoleMessageLocation,
+} from '../common/ConsoleMessage.js';
+import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
+import type {Handler} from '../common/EventEmitter.js';
+import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
+import type {PDFOptions} from '../common/PDFOptions.js';
+import type {Awaitable} from '../common/types.js';
+import {
+ debugError,
+ evaluationString,
+ NETWORK_IDLE_TIME,
+ parsePDFOptions,
+ timeout,
+ validateDialogType,
+} from '../common/util.js';
+import type {Viewport} from '../common/Viewport.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+import {disposeSymbol} from '../util/disposable.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import type {BidiBrowser} from './Browser.js';
+import type {BidiBrowserContext} from './BrowserContext.js';
+import {
+ BrowsingContextEvent,
+ CdpSessionWrapper,
+ type BrowsingContext,
+} from './BrowsingContext.js';
+import type {BidiConnection} from './Connection.js';
+import {BidiDeserializer} from './Deserializer.js';
+import {BidiDialog} from './Dialog.js';
+import {BidiElementHandle} from './ElementHandle.js';
+import {EmulationManager} from './EmulationManager.js';
+import {BidiFrame} from './Frame.js';
+import type {BidiHTTPRequest} from './HTTPRequest.js';
+import type {BidiHTTPResponse} from './HTTPResponse.js';
+import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
+import type {BidiJSHandle} from './JSHandle.js';
+import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js';
+import {BidiNetworkManager} from './NetworkManager.js';
+import {createBidiHandle} from './Realm.js';
+import type {BiDiPageTarget} from './Target.js';
+
+/**
+ * @internal
+ */
+export class BidiPage extends Page {
+ #accessibility: Accessibility;
+ #connection: BidiConnection;
+ #frameTree = new FrameTree<BidiFrame>();
+ #networkManager: BidiNetworkManager;
+ #viewport: Viewport | null = null;
+ #closedDeferred = Deferred.create<never, TargetCloseError>();
+ #subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([
+ ['log.entryAdded', this.#onLogEntryAdded.bind(this)],
+ ['browsingContext.load', this.#onFrameLoaded.bind(this)],
+ [
+ 'browsingContext.fragmentNavigated',
+ this.#onFrameFragmentNavigated.bind(this),
+ ],
+ [
+ 'browsingContext.domContentLoaded',
+ this.#onFrameDOMContentLoaded.bind(this),
+ ],
+ ['browsingContext.userPromptOpened', this.#onDialog.bind(this)],
+ ]);
+ readonly #networkManagerEvents = [
+ [
+ NetworkManagerEvent.Request,
+ (request: BidiHTTPRequest) => {
+ this.emit(PageEvent.Request, request);
+ },
+ ],
+ [
+ NetworkManagerEvent.RequestServedFromCache,
+ (request: BidiHTTPRequest) => {
+ this.emit(PageEvent.RequestServedFromCache, request);
+ },
+ ],
+ [
+ NetworkManagerEvent.RequestFailed,
+ (request: BidiHTTPRequest) => {
+ this.emit(PageEvent.RequestFailed, request);
+ },
+ ],
+ [
+ NetworkManagerEvent.RequestFinished,
+ (request: BidiHTTPRequest) => {
+ this.emit(PageEvent.RequestFinished, request);
+ },
+ ],
+ [
+ NetworkManagerEvent.Response,
+ (response: BidiHTTPResponse) => {
+ this.emit(PageEvent.Response, response);
+ },
+ ],
+ ] as const;
+
+ readonly #browsingContextEvents = new Map<symbol, Handler<any>>([
+ [BrowsingContextEvent.Created, this.#onContextCreated.bind(this)],
+ [BrowsingContextEvent.Destroyed, this.#onContextDestroyed.bind(this)],
+ ]);
+ #tracing: Tracing;
+ #coverage: Coverage;
+ #cdpEmulationManager: CdpEmulationManager;
+ #emulationManager: EmulationManager;
+ #mouse: BidiMouse;
+ #touchscreen: BidiTouchscreen;
+ #keyboard: BidiKeyboard;
+ #browsingContext: BrowsingContext;
+ #browserContext: BidiBrowserContext;
+ #target: BiDiPageTarget;
+
+ _client(): CDPSession {
+ return this.mainFrame().context().cdpSession;
+ }
+
+ constructor(
+ browsingContext: BrowsingContext,
+ browserContext: BidiBrowserContext,
+ target: BiDiPageTarget
+ ) {
+ super();
+ this.#browsingContext = browsingContext;
+ this.#browserContext = browserContext;
+ this.#target = target;
+ this.#connection = browsingContext.connection;
+
+ for (const [event, subscriber] of this.#browsingContextEvents) {
+ this.#browsingContext.on(event, subscriber);
+ }
+
+ this.#networkManager = new BidiNetworkManager(this.#connection, this);
+
+ for (const [event, subscriber] of this.#subscribedEvents) {
+ this.#connection.on(event, subscriber);
+ }
+
+ for (const [event, subscriber] of this.#networkManagerEvents) {
+ // TODO: remove any
+ this.#networkManager.on(event, subscriber as any);
+ }
+
+ const frame = new BidiFrame(
+ this,
+ this.#browsingContext,
+ this._timeoutSettings,
+ this.#browsingContext.parent
+ );
+ this.#frameTree.addFrame(frame);
+ this.emit(PageEvent.FrameAttached, frame);
+
+ // TODO: https://github.com/w3c/webdriver-bidi/issues/443
+ this.#accessibility = new Accessibility(
+ this.mainFrame().context().cdpSession
+ );
+ this.#tracing = new Tracing(this.mainFrame().context().cdpSession);
+ this.#coverage = new Coverage(this.mainFrame().context().cdpSession);
+ this.#cdpEmulationManager = new CdpEmulationManager(
+ this.mainFrame().context().cdpSession
+ );
+ this.#emulationManager = new EmulationManager(browsingContext);
+ this.#mouse = new BidiMouse(this.mainFrame().context());
+ this.#touchscreen = new BidiTouchscreen(this.mainFrame().context());
+ this.#keyboard = new BidiKeyboard(this);
+ }
+
+ /**
+ * @internal
+ */
+ get connection(): BidiConnection {
+ return this.#connection;
+ }
+
+ override async setUserAgent(
+ userAgent: string,
+ userAgentMetadata?: Protocol.Emulation.UserAgentMetadata | undefined
+ ): Promise<void> {
+ // TODO: handle CDP-specific cases such as mprach.
+ await this._client().send('Network.setUserAgentOverride', {
+ userAgent: userAgent,
+ userAgentMetadata: userAgentMetadata,
+ });
+ }
+
+ override async setBypassCSP(enabled: boolean): Promise<void> {
+ // TODO: handle CDP-specific cases such as mprach.
+ await this._client().send('Page.setBypassCSP', {enabled});
+ }
+
+ override async queryObjects<Prototype>(
+ prototypeHandle: BidiJSHandle<Prototype>
+ ): Promise<BidiJSHandle<Prototype[]>> {
+ assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
+ assert(
+ prototypeHandle.id,
+ 'Prototype JSHandle must not be referencing primitive value'
+ );
+ const response = await this.mainFrame().client.send(
+ 'Runtime.queryObjects',
+ {
+ prototypeObjectId: prototypeHandle.id,
+ }
+ );
+ return createBidiHandle(this.mainFrame().mainRealm(), {
+ type: 'array',
+ handle: response.objects.objectId,
+ }) as BidiJSHandle<Prototype[]>;
+ }
+
+ _setBrowserContext(browserContext: BidiBrowserContext): void {
+ this.#browserContext = browserContext;
+ }
+
+ override get accessibility(): Accessibility {
+ return this.#accessibility;
+ }
+
+ override get tracing(): Tracing {
+ return this.#tracing;
+ }
+
+ override get coverage(): Coverage {
+ return this.#coverage;
+ }
+
+ override get mouse(): BidiMouse {
+ return this.#mouse;
+ }
+
+ override get touchscreen(): BidiTouchscreen {
+ return this.#touchscreen;
+ }
+
+ override get keyboard(): BidiKeyboard {
+ return this.#keyboard;
+ }
+
+ override browser(): BidiBrowser {
+ return this.browserContext().browser();
+ }
+
+ override browserContext(): BidiBrowserContext {
+ return this.#browserContext;
+ }
+
+ override mainFrame(): BidiFrame {
+ const mainFrame = this.#frameTree.getMainFrame();
+ assert(mainFrame, 'Requesting main frame too early!');
+ return mainFrame;
+ }
+
+ /**
+ * @internal
+ */
+ async focusedFrame(): Promise<BidiFrame> {
+ using frame = await this.mainFrame()
+ .isolatedRealm()
+ .evaluateHandle(() => {
+ let frame: HTMLIFrameElement | undefined;
+ let win: Window | null = window;
+ while (win?.document.activeElement instanceof HTMLIFrameElement) {
+ frame = win.document.activeElement;
+ win = frame.contentWindow;
+ }
+ return frame;
+ });
+ if (!(frame instanceof BidiElementHandle)) {
+ return this.mainFrame();
+ }
+ return await frame.contentFrame();
+ }
+
+ override frames(): BidiFrame[] {
+ return Array.from(this.#frameTree.frames());
+ }
+
+ frame(frameId?: string): BidiFrame | null {
+ return this.#frameTree.getById(frameId ?? '') || null;
+ }
+
+ childFrames(frameId: string): BidiFrame[] {
+ return this.#frameTree.childFrames(frameId);
+ }
+
+ #onFrameLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
+ const frame = this.frame(info.context);
+ if (frame && this.mainFrame() === frame) {
+ this.emit(PageEvent.Load, undefined);
+ }
+ }
+
+ #onFrameFragmentNavigated(info: Bidi.BrowsingContext.NavigationInfo): void {
+ const frame = this.frame(info.context);
+ if (frame) {
+ this.emit(PageEvent.FrameNavigated, frame);
+ }
+ }
+
+ #onFrameDOMContentLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
+ const frame = this.frame(info.context);
+ if (frame) {
+ frame._hasStartedLoading = true;
+ if (this.mainFrame() === frame) {
+ this.emit(PageEvent.DOMContentLoaded, undefined);
+ }
+ this.emit(PageEvent.FrameNavigated, frame);
+ }
+ }
+
+ #onContextCreated(context: BrowsingContext): void {
+ if (
+ !this.frame(context.id) &&
+ (this.frame(context.parent ?? '') || !this.#frameTree.getMainFrame())
+ ) {
+ const frame = new BidiFrame(
+ this,
+ context,
+ this._timeoutSettings,
+ context.parent
+ );
+ this.#frameTree.addFrame(frame);
+ if (frame !== this.mainFrame()) {
+ this.emit(PageEvent.FrameAttached, frame);
+ }
+ }
+ }
+
+ #onContextDestroyed(context: BrowsingContext): void {
+ const frame = this.frame(context.id);
+
+ if (frame) {
+ if (frame === this.mainFrame()) {
+ this.emit(PageEvent.Close, undefined);
+ }
+ this.#removeFramesRecursively(frame);
+ }
+ }
+
+ #removeFramesRecursively(frame: BidiFrame): void {
+ for (const child of frame.childFrames()) {
+ this.#removeFramesRecursively(child);
+ }
+ frame[disposeSymbol]();
+ this.#networkManager.clearMapAfterFrameDispose(frame);
+ this.#frameTree.removeFrame(frame);
+ this.emit(PageEvent.FrameDetached, frame);
+ }
+
+ #onLogEntryAdded(event: Bidi.Log.Entry): void {
+ const frame = this.frame(event.source.context);
+ if (!frame) {
+ return;
+ }
+ if (isConsoleLogEntry(event)) {
+ const args = event.args.map(arg => {
+ return createBidiHandle(frame.mainRealm(), arg);
+ });
+
+ const text = args
+ .reduce((value, arg) => {
+ const parsedValue = arg.isPrimitiveValue
+ ? BidiDeserializer.deserialize(arg.remoteValue())
+ : arg.toString();
+ return `${value} ${parsedValue}`;
+ }, '')
+ .slice(1);
+
+ this.emit(
+ PageEvent.Console,
+ new ConsoleMessage(
+ event.method as any,
+ text,
+ args,
+ getStackTraceLocations(event.stackTrace)
+ )
+ );
+ } else if (isJavaScriptLogEntry(event)) {
+ const error = new Error(event.text ?? '');
+
+ const messageHeight = error.message.split('\n').length;
+ const messageLines = error.stack!.split('\n').splice(0, messageHeight);
+
+ const stackLines = [];
+ if (event.stackTrace) {
+ for (const frame of event.stackTrace.callFrames) {
+ // Note we need to add `1` because the values are 0-indexed.
+ stackLines.push(
+ ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
+ frame.lineNumber + 1
+ }:${frame.columnNumber + 1})`
+ );
+ if (stackLines.length >= Error.stackTraceLimit) {
+ break;
+ }
+ }
+ }
+
+ error.stack = [...messageLines, ...stackLines].join('\n');
+ this.emit(PageEvent.PageError, error);
+ } else {
+ debugError(
+ `Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"`
+ );
+ }
+ }
+
+ #onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void {
+ const frame = this.frame(event.context);
+ if (!frame) {
+ return;
+ }
+ const type = validateDialogType(event.type);
+
+ const dialog = new BidiDialog(
+ frame.context(),
+ type,
+ event.message,
+ event.defaultValue
+ );
+ this.emit(PageEvent.Dialog, dialog);
+ }
+
+ getNavigationResponse(id?: string | null): BidiHTTPResponse | null {
+ return this.#networkManager.getNavigationResponse(id);
+ }
+
+ override isClosed(): boolean {
+ return this.#closedDeferred.finished();
+ }
+
+ override async close(options?: {runBeforeUnload?: boolean}): Promise<void> {
+ if (this.#closedDeferred.finished()) {
+ return;
+ }
+
+ this.#closedDeferred.reject(new TargetCloseError('Page closed!'));
+ this.#networkManager.dispose();
+
+ await this.#connection.send('browsingContext.close', {
+ context: this.mainFrame()._id,
+ promptUnload: options?.runBeforeUnload ?? false,
+ });
+
+ this.emit(PageEvent.Close, undefined);
+ this.removeAllListeners();
+ }
+
+ override async reload(
+ options: WaitForOptions = {}
+ ): Promise<BidiHTTPResponse | null> {
+ const {
+ waitUntil = 'load',
+ timeout: ms = this._timeoutSettings.navigationTimeout(),
+ } = options;
+
+ const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
+
+ const result$ = zip(
+ from(
+ this.#connection.send('browsingContext.reload', {
+ context: this.mainFrame()._id,
+ wait: readiness,
+ })
+ ),
+ ...(networkIdle !== null
+ ? [
+ this.waitForNetworkIdle$({
+ timeout: ms,
+ concurrency: networkIdle === 'networkidle2' ? 2 : 0,
+ idleTime: NETWORK_IDLE_TIME,
+ }),
+ ]
+ : [])
+ ).pipe(
+ map(([{result}]) => {
+ return result;
+ }),
+ raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())),
+ rewriteNavigationError(this.url(), ms)
+ );
+
+ const result = await firstValueFrom(result$);
+ return this.getNavigationResponse(result.navigation);
+ }
+
+ override setDefaultNavigationTimeout(timeout: number): void {
+ this._timeoutSettings.setDefaultNavigationTimeout(timeout);
+ }
+
+ override setDefaultTimeout(timeout: number): void {
+ this._timeoutSettings.setDefaultTimeout(timeout);
+ }
+
+ override getDefaultTimeout(): number {
+ return this._timeoutSettings.timeout();
+ }
+
+ override isJavaScriptEnabled(): boolean {
+ return this.#cdpEmulationManager.javascriptEnabled;
+ }
+
+ override async setGeolocation(options: GeolocationOptions): Promise<void> {
+ return await this.#cdpEmulationManager.setGeolocation(options);
+ }
+
+ override async setJavaScriptEnabled(enabled: boolean): Promise<void> {
+ return await this.#cdpEmulationManager.setJavaScriptEnabled(enabled);
+ }
+
+ override async emulateMediaType(type?: string): Promise<void> {
+ return await this.#cdpEmulationManager.emulateMediaType(type);
+ }
+
+ override async emulateCPUThrottling(factor: number | null): Promise<void> {
+ return await this.#cdpEmulationManager.emulateCPUThrottling(factor);
+ }
+
+ override async emulateMediaFeatures(
+ features?: MediaFeature[]
+ ): Promise<void> {
+ return await this.#cdpEmulationManager.emulateMediaFeatures(features);
+ }
+
+ override async emulateTimezone(timezoneId?: string): Promise<void> {
+ return await this.#cdpEmulationManager.emulateTimezone(timezoneId);
+ }
+
+ override async emulateIdleState(overrides?: {
+ isUserActive: boolean;
+ isScreenUnlocked: boolean;
+ }): Promise<void> {
+ return await this.#cdpEmulationManager.emulateIdleState(overrides);
+ }
+
+ override async emulateVisionDeficiency(
+ type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
+ ): Promise<void> {
+ return await this.#cdpEmulationManager.emulateVisionDeficiency(type);
+ }
+
+ override async setViewport(viewport: Viewport): Promise<void> {
+ if (!this.#browsingContext.supportsCdp()) {
+ await this.#emulationManager.emulateViewport(viewport);
+ this.#viewport = viewport;
+ return;
+ }
+ const needsReload =
+ await this.#cdpEmulationManager.emulateViewport(viewport);
+ this.#viewport = viewport;
+ if (needsReload) {
+ await this.reload();
+ }
+ }
+
+ override viewport(): Viewport | null {
+ return this.#viewport;
+ }
+
+ override async pdf(options: PDFOptions = {}): Promise<Buffer> {
+ const {timeout: ms = this._timeoutSettings.timeout(), path = undefined} =
+ options;
+ const {
+ printBackground: background,
+ margin,
+ landscape,
+ width,
+ height,
+ pageRanges: ranges,
+ scale,
+ preferCSSPageSize,
+ } = parsePDFOptions(options, 'cm');
+ const pageRanges = ranges ? ranges.split(', ') : [];
+ const {result} = await firstValueFrom(
+ from(
+ this.#connection.send('browsingContext.print', {
+ context: this.mainFrame()._id,
+ background,
+ margin,
+ orientation: landscape ? 'landscape' : 'portrait',
+ page: {
+ width,
+ height,
+ },
+ pageRanges,
+ scale,
+ shrinkToFit: !preferCSSPageSize,
+ })
+ ).pipe(raceWith(timeout(ms)))
+ );
+
+ const buffer = Buffer.from(result.data, 'base64');
+
+ await this._maybeWriteBufferToFile(path, buffer);
+
+ return buffer;
+ }
+
+ override async createPDFStream(
+ options?: PDFOptions | undefined
+ ): Promise<Readable> {
+ const buffer = await this.pdf(options);
+ try {
+ const {Readable} = await import('stream');
+ return Readable.from(buffer);
+ } catch (error) {
+ if (error instanceof TypeError) {
+ throw new Error(
+ 'Can only pass a file path in a Node-like environment.'
+ );
+ }
+ throw error;
+ }
+ }
+
+ override async _screenshot(
+ options: Readonly<ScreenshotOptions>
+ ): Promise<string> {
+ const {clip, type, captureBeyondViewport, quality} = options;
+ if (options.omitBackground !== undefined && options.omitBackground) {
+ throw new UnsupportedOperation(`BiDi does not support 'omitBackground'.`);
+ }
+ if (options.optimizeForSpeed !== undefined && options.optimizeForSpeed) {
+ throw new UnsupportedOperation(
+ `BiDi does not support 'optimizeForSpeed'.`
+ );
+ }
+ if (options.fromSurface !== undefined && !options.fromSurface) {
+ throw new UnsupportedOperation(`BiDi does not support 'fromSurface'.`);
+ }
+ if (clip !== undefined && clip.scale !== undefined && clip.scale !== 1) {
+ throw new UnsupportedOperation(
+ `BiDi does not support 'scale' in 'clip'.`
+ );
+ }
+
+ let box: BoundingBox | undefined;
+ if (clip) {
+ if (captureBeyondViewport) {
+ box = clip;
+ } else {
+ // The clip is always with respect to the document coordinates, so we
+ // need to convert this to viewport coordinates when we aren't capturing
+ // beyond the viewport.
+ const [pageLeft, pageTop] = await this.evaluate(() => {
+ if (!window.visualViewport) {
+ throw new Error('window.visualViewport is not supported.');
+ }
+ return [
+ window.visualViewport.pageLeft,
+ window.visualViewport.pageTop,
+ ] as const;
+ });
+ box = {
+ ...clip,
+ x: clip.x - pageLeft,
+ y: clip.y - pageTop,
+ };
+ }
+ }
+
+ const {
+ result: {data},
+ } = await this.#connection.send('browsingContext.captureScreenshot', {
+ context: this.mainFrame()._id,
+ origin: captureBeyondViewport ? 'document' : 'viewport',
+ format: {
+ type: `image/${type}`,
+ ...(quality !== undefined ? {quality: quality / 100} : {}),
+ },
+ ...(box ? {clip: {type: 'box', ...box}} : {}),
+ });
+ return data;
+ }
+
+ override async createCDPSession(): Promise<CDPSession> {
+ const {sessionId} = await this.mainFrame()
+ .context()
+ .cdpSession.send('Target.attachToTarget', {
+ targetId: this.mainFrame()._id,
+ flatten: true,
+ });
+ return new CdpSessionWrapper(this.mainFrame().context(), sessionId);
+ }
+
+ override async bringToFront(): Promise<void> {
+ await this.#connection.send('browsingContext.activate', {
+ context: this.mainFrame()._id,
+ });
+ }
+
+ override async evaluateOnNewDocument<
+ Params extends unknown[],
+ Func extends (...args: Params) => unknown = (...args: Params) => unknown,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<NewDocumentScriptEvaluation> {
+ const expression = evaluationExpression(pageFunction, ...args);
+ const {result} = await this.#connection.send('script.addPreloadScript', {
+ functionDeclaration: expression,
+ contexts: [this.mainFrame()._id],
+ });
+
+ return {identifier: result.script};
+ }
+
+ override async removeScriptToEvaluateOnNewDocument(
+ id: string
+ ): Promise<void> {
+ await this.#connection.send('script.removePreloadScript', {
+ script: id,
+ });
+ }
+
+ override async exposeFunction<Args extends unknown[], Ret>(
+ name: string,
+ pptrFunction:
+ | ((...args: Args) => Awaitable<Ret>)
+ | {default: (...args: Args) => Awaitable<Ret>}
+ ): Promise<void> {
+ return await this.mainFrame().exposeFunction(
+ name,
+ 'default' in pptrFunction ? pptrFunction.default : pptrFunction
+ );
+ }
+
+ override isDragInterceptionEnabled(): boolean {
+ return false;
+ }
+
+ override async setCacheEnabled(enabled?: boolean): Promise<void> {
+ // TODO: handle CDP-specific cases such as mprach.
+ await this._client().send('Network.setCacheDisabled', {
+ cacheDisabled: !enabled,
+ });
+ }
+
+ override isServiceWorkerBypassed(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override target(): BiDiPageTarget {
+ return this.#target;
+ }
+
+ override waitForFileChooser(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override workers(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override setRequestInterception(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override setDragInterception(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override setBypassServiceWorker(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override setOfflineMode(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override emulateNetworkConditions(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override cookies(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override setCookie(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override deleteCookie(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override removeExposedFunction(): never {
+ // TODO: Quick win?
+ throw new UnsupportedOperation();
+ }
+
+ override authenticate(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override setExtraHTTPHeaders(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override metrics(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override async goBack(
+ options: WaitForOptions = {}
+ ): Promise<HTTPResponse | null> {
+ return await this.#go(-1, options);
+ }
+
+ override async goForward(
+ options: WaitForOptions = {}
+ ): Promise<HTTPResponse | null> {
+ return await this.#go(+1, options);
+ }
+
+ async #go(
+ delta: number,
+ options: WaitForOptions
+ ): Promise<HTTPResponse | null> {
+ try {
+ const result = await Promise.all([
+ this.waitForNavigation(options),
+ this.#connection.send('browsingContext.traverseHistory', {
+ delta,
+ context: this.mainFrame()._id,
+ }),
+ ]);
+ return result[0];
+ } catch (err) {
+ // TODO: waitForNavigation should be cancelled if an error happens.
+ if (isErrorLike(err)) {
+ if (err.message.includes('no such history entry')) {
+ return null;
+ }
+ }
+ throw err;
+ }
+ }
+
+ override waitForDevicePrompt(): never {
+ throw new UnsupportedOperation();
+ }
+}
+
+function isConsoleLogEntry(
+ event: Bidi.Log.Entry
+): event is Bidi.Log.ConsoleLogEntry {
+ return event.type === 'console';
+}
+
+function isJavaScriptLogEntry(
+ event: Bidi.Log.Entry
+): event is Bidi.Log.JavascriptLogEntry {
+ return event.type === 'javascript';
+}
+
+function getStackTraceLocations(
+ stackTrace?: Bidi.Script.StackTrace
+): ConsoleMessageLocation[] {
+ const stackTraceLocations: ConsoleMessageLocation[] = [];
+ if (stackTrace) {
+ for (const callFrame of stackTrace.callFrames) {
+ stackTraceLocations.push({
+ url: callFrame.url,
+ lineNumber: callFrame.lineNumber,
+ columnNumber: callFrame.columnNumber,
+ });
+ }
+ }
+ return stackTraceLocations;
+}
+
+function evaluationExpression(fun: Function | string, ...args: unknown[]) {
+ return `() => {${evaluationString(fun, ...args)}}`;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts
new file mode 100644
index 0000000000..84f13bc703
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts
@@ -0,0 +1,228 @@
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter, type EventType} from '../common/EventEmitter.js';
+import {scriptInjector} from '../common/ScriptInjector.js';
+import type {EvaluateFunc, HandleFor} from '../common/types.js';
+import {
+ PuppeteerURL,
+ SOURCE_URL_REGEX,
+ getSourcePuppeteerURLIfAvailable,
+ getSourceUrlComment,
+ isString,
+} from '../common/util.js';
+import type PuppeteerUtil from '../injected/injected.js';
+import {disposeSymbol} from '../util/disposable.js';
+import {stringifyFunction} from '../util/Function.js';
+
+import type {BidiConnection} from './Connection.js';
+import {BidiDeserializer} from './Deserializer.js';
+import {BidiElementHandle} from './ElementHandle.js';
+import {BidiJSHandle} from './JSHandle.js';
+import type {Sandbox} from './Sandbox.js';
+import {BidiSerializer} from './Serializer.js';
+import {createEvaluationError} from './util.js';
+
+/**
+ * @internal
+ */
+export class BidiRealm extends EventEmitter<Record<EventType, any>> {
+ readonly connection: BidiConnection;
+
+ #id!: string;
+ #sandbox!: Sandbox;
+
+ constructor(connection: BidiConnection) {
+ super();
+ this.connection = connection;
+ }
+
+ get target(): Bidi.Script.Target {
+ return {
+ context: this.#sandbox.environment._id,
+ sandbox: this.#sandbox.name,
+ };
+ }
+
+ handleRealmDestroyed = async (
+ params: Bidi.Script.RealmDestroyed['params']
+ ): Promise<void> => {
+ if (params.realm === this.#id) {
+ // Note: The Realm is destroyed, so in theory the handle should be as
+ // well.
+ this.internalPuppeteerUtil = undefined;
+ this.#sandbox.environment.clearDocumentHandle();
+ }
+ };
+
+ handleRealmCreated = (params: Bidi.Script.RealmCreated['params']): void => {
+ if (
+ params.type === 'window' &&
+ params.context === this.#sandbox.environment._id &&
+ params.sandbox === this.#sandbox.name
+ ) {
+ this.#id = params.realm;
+ void this.#sandbox.taskManager.rerunAll();
+ }
+ };
+
+ setSandbox(sandbox: Sandbox): void {
+ this.#sandbox = sandbox;
+ this.connection.on(
+ Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
+ this.handleRealmCreated
+ );
+ this.connection.on(
+ Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
+ this.handleRealmDestroyed
+ );
+ }
+
+ protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>;
+ get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerUtil>> {
+ const promise = Promise.resolve() as Promise<unknown>;
+ scriptInjector.inject(script => {
+ if (this.internalPuppeteerUtil) {
+ void this.internalPuppeteerUtil.then(handle => {
+ void handle.dispose();
+ });
+ }
+ this.internalPuppeteerUtil = promise.then(() => {
+ return this.evaluateHandle(script) as Promise<
+ BidiJSHandle<PuppeteerUtil>
+ >;
+ });
+ }, !this.internalPuppeteerUtil);
+ return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>;
+ }
+
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ return await this.#evaluate(false, pageFunction, ...args);
+ }
+
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ return await this.#evaluate(true, pageFunction, ...args);
+ }
+
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ returnByValue: true,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>>;
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ returnByValue: false,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ returnByValue: boolean,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
+ const sourceUrlComment = getSourceUrlComment(
+ getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
+ PuppeteerURL.INTERNAL_URL
+ );
+
+ const sandbox = this.#sandbox;
+
+ let responsePromise;
+ const resultOwnership = returnByValue
+ ? Bidi.Script.ResultOwnership.None
+ : Bidi.Script.ResultOwnership.Root;
+ const serializationOptions: Bidi.Script.SerializationOptions = returnByValue
+ ? {}
+ : {
+ maxObjectDepth: 0,
+ maxDomDepth: 0,
+ };
+ if (isString(pageFunction)) {
+ const expression = SOURCE_URL_REGEX.test(pageFunction)
+ ? pageFunction
+ : `${pageFunction}\n${sourceUrlComment}\n`;
+
+ responsePromise = this.connection.send('script.evaluate', {
+ expression,
+ target: this.target,
+ resultOwnership,
+ awaitPromise: true,
+ userActivation: true,
+ serializationOptions,
+ });
+ } else {
+ let functionDeclaration = stringifyFunction(pageFunction);
+ functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
+ ? functionDeclaration
+ : `${functionDeclaration}\n${sourceUrlComment}\n`;
+ responsePromise = this.connection.send('script.callFunction', {
+ functionDeclaration,
+ arguments: args.length
+ ? await Promise.all(
+ args.map(arg => {
+ return BidiSerializer.serialize(sandbox, arg);
+ })
+ )
+ : [],
+ target: this.target,
+ resultOwnership,
+ awaitPromise: true,
+ userActivation: true,
+ serializationOptions,
+ });
+ }
+
+ const {result} = await responsePromise;
+
+ if ('type' in result && result.type === 'exception') {
+ throw createEvaluationError(result.exceptionDetails);
+ }
+
+ return returnByValue
+ ? BidiDeserializer.deserialize(result.result)
+ : createBidiHandle(sandbox, result.result);
+ }
+
+ [disposeSymbol](): void {
+ this.connection.off(
+ Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
+ this.handleRealmCreated
+ );
+ this.connection.off(
+ Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
+ this.handleRealmDestroyed
+ );
+ }
+}
+
+/**
+ * @internal
+ */
+export function createBidiHandle(
+ sandbox: Sandbox,
+ result: Bidi.Script.RemoteValue
+): BidiJSHandle<unknown> | BidiElementHandle<Node> {
+ if (result.type === 'node' || result.type === 'window') {
+ return new BidiElementHandle(sandbox, result);
+ }
+ return new BidiJSHandle(sandbox, result);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts
new file mode 100644
index 0000000000..4411b3dbcd
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts
@@ -0,0 +1,123 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JSHandle} from '../api/JSHandle.js';
+import {Realm} from '../api/Realm.js';
+import type {TimeoutSettings} from '../common/TimeoutSettings.js';
+import type {EvaluateFunc, HandleFor} from '../common/types.js';
+import {withSourcePuppeteerURLIfNone} from '../common/util.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+import {BidiElementHandle} from './ElementHandle.js';
+import type {BidiFrame} from './Frame.js';
+import type {BidiRealm as BidiRealm} from './Realm.js';
+/**
+ * A unique key for {@link SandboxChart} to denote the default world.
+ * Realms are automatically created in the default sandbox.
+ *
+ * @internal
+ */
+export const MAIN_SANDBOX = Symbol('mainSandbox');
+/**
+ * A unique key for {@link SandboxChart} to denote the puppeteer sandbox.
+ * This world contains all puppeteer-internal bindings/code.
+ *
+ * @internal
+ */
+export const PUPPETEER_SANDBOX = Symbol('puppeteerSandbox');
+
+/**
+ * @internal
+ */
+export interface SandboxChart {
+ [key: string]: Sandbox;
+ [MAIN_SANDBOX]: Sandbox;
+ [PUPPETEER_SANDBOX]: Sandbox;
+}
+
+/**
+ * @internal
+ */
+export class Sandbox extends Realm {
+ readonly name: string | undefined;
+ readonly realm: BidiRealm;
+ #frame: BidiFrame;
+
+ constructor(
+ name: string | undefined,
+ frame: BidiFrame,
+ // TODO: We should split the Realm and BrowsingContext
+ realm: BidiRealm | BrowsingContext,
+ timeoutSettings: TimeoutSettings
+ ) {
+ super(timeoutSettings);
+ this.name = name;
+ this.realm = realm;
+ this.#frame = frame;
+ this.realm.setSandbox(this);
+ }
+
+ override get environment(): BidiFrame {
+ return this.#frame;
+ }
+
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluateHandle.name,
+ pageFunction
+ );
+ return await this.realm.evaluateHandle(pageFunction, ...args);
+ }
+
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluate.name,
+ pageFunction
+ );
+ return await this.realm.evaluate(pageFunction, ...args);
+ }
+
+ async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
+ return (await this.evaluateHandle(node => {
+ return node;
+ }, handle)) as unknown as T;
+ }
+
+ async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
+ if (handle.realm === this) {
+ return handle;
+ }
+ const transferredHandle = await this.evaluateHandle(node => {
+ return node;
+ }, handle);
+ await handle.dispose();
+ return transferredHandle as unknown as T;
+ }
+
+ override async adoptBackendNode(
+ backendNodeId?: number
+ ): Promise<JSHandle<Node>> {
+ const {object} = await this.environment.client.send('DOM.resolveNode', {
+ backendNodeId: backendNodeId,
+ });
+ return new BidiElementHandle(this, {
+ handle: object.objectId,
+ type: 'node',
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts
new file mode 100644
index 0000000000..c147ec9281
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts
@@ -0,0 +1,164 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {LazyArg} from '../common/LazyArg.js';
+import {isDate, isPlainObject, isRegExp} from '../common/util.js';
+
+import {BidiElementHandle} from './ElementHandle.js';
+import {BidiJSHandle} from './JSHandle.js';
+import type {Sandbox} from './Sandbox.js';
+
+/**
+ * @internal
+ */
+class UnserializableError extends Error {}
+
+/**
+ * @internal
+ */
+export class BidiSerializer {
+ static serializeNumber(arg: number): Bidi.Script.LocalValue {
+ let value: Bidi.Script.SpecialNumber | number;
+ if (Object.is(arg, -0)) {
+ value = '-0';
+ } else if (Object.is(arg, Infinity)) {
+ value = 'Infinity';
+ } else if (Object.is(arg, -Infinity)) {
+ value = '-Infinity';
+ } else if (Object.is(arg, NaN)) {
+ value = 'NaN';
+ } else {
+ value = arg;
+ }
+ return {
+ type: 'number',
+ value,
+ };
+ }
+
+ static serializeObject(arg: object | null): Bidi.Script.LocalValue {
+ if (arg === null) {
+ return {
+ type: 'null',
+ };
+ } else if (Array.isArray(arg)) {
+ const parsedArray = arg.map(subArg => {
+ return BidiSerializer.serializeRemoteValue(subArg);
+ });
+
+ return {
+ type: 'array',
+ value: parsedArray,
+ };
+ } else if (isPlainObject(arg)) {
+ try {
+ JSON.stringify(arg);
+ } catch (error) {
+ if (
+ error instanceof TypeError &&
+ error.message.startsWith('Converting circular structure to JSON')
+ ) {
+ error.message += ' Recursive objects are not allowed.';
+ }
+ throw error;
+ }
+
+ const parsedObject: Bidi.Script.MappingLocalValue = [];
+ for (const key in arg) {
+ parsedObject.push([
+ BidiSerializer.serializeRemoteValue(key),
+ BidiSerializer.serializeRemoteValue(arg[key]),
+ ]);
+ }
+
+ return {
+ type: 'object',
+ value: parsedObject,
+ };
+ } else if (isRegExp(arg)) {
+ return {
+ type: 'regexp',
+ value: {
+ pattern: arg.source,
+ flags: arg.flags,
+ },
+ };
+ } else if (isDate(arg)) {
+ return {
+ type: 'date',
+ value: arg.toISOString(),
+ };
+ }
+
+ throw new UnserializableError(
+ 'Custom object sterilization not possible. Use plain objects instead.'
+ );
+ }
+
+ static serializeRemoteValue(arg: unknown): Bidi.Script.LocalValue {
+ switch (typeof arg) {
+ case 'symbol':
+ case 'function':
+ throw new UnserializableError(`Unable to serializable ${typeof arg}`);
+ case 'object':
+ return BidiSerializer.serializeObject(arg);
+
+ case 'undefined':
+ return {
+ type: 'undefined',
+ };
+ case 'number':
+ return BidiSerializer.serializeNumber(arg);
+ case 'bigint':
+ return {
+ type: 'bigint',
+ value: arg.toString(),
+ };
+ case 'string':
+ return {
+ type: 'string',
+ value: arg,
+ };
+ case 'boolean':
+ return {
+ type: 'boolean',
+ value: arg,
+ };
+ }
+ }
+
+ static async serialize(
+ sandbox: Sandbox,
+ arg: unknown
+ ): Promise<Bidi.Script.LocalValue> {
+ if (arg instanceof LazyArg) {
+ arg = await arg.get(sandbox.realm);
+ }
+ // eslint-disable-next-line rulesdir/use-using -- We want this to continue living.
+ const objectHandle =
+ arg && (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle)
+ ? arg
+ : null;
+ if (objectHandle) {
+ if (
+ objectHandle.realm.environment.context() !==
+ sandbox.environment.context()
+ ) {
+ throw new Error(
+ 'JSHandles can be evaluated only in the context they were created!'
+ );
+ }
+ if (objectHandle.disposed) {
+ throw new Error('JSHandle is disposed!');
+ }
+ return objectHandle.remoteValue() as Bidi.Script.RemoteReference;
+ }
+
+ return BidiSerializer.serializeRemoteValue(arg);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts
new file mode 100644
index 0000000000..fb01c34638
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts
@@ -0,0 +1,151 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {Page} from '../api/Page.js';
+import {Target, TargetType} from '../api/Target.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+
+import type {BidiBrowser} from './Browser.js';
+import type {BidiBrowserContext} from './BrowserContext.js';
+import {type BrowsingContext, CdpSessionWrapper} from './BrowsingContext.js';
+import {BidiPage} from './Page.js';
+
+/**
+ * @internal
+ */
+export abstract class BidiTarget extends Target {
+ protected _browserContext: BidiBrowserContext;
+
+ constructor(browserContext: BidiBrowserContext) {
+ super();
+ this._browserContext = browserContext;
+ }
+
+ _setBrowserContext(browserContext: BidiBrowserContext): void {
+ this._browserContext = browserContext;
+ }
+
+ override asPage(): Promise<Page> {
+ throw new UnsupportedOperation();
+ }
+
+ override browser(): BidiBrowser {
+ return this._browserContext.browser();
+ }
+
+ override browserContext(): BidiBrowserContext {
+ return this._browserContext;
+ }
+
+ override opener(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override createCDPSession(): Promise<CDPSession> {
+ throw new UnsupportedOperation();
+ }
+}
+
+/**
+ * @internal
+ */
+export class BiDiBrowserTarget extends Target {
+ #browser: BidiBrowser;
+
+ constructor(browser: BidiBrowser) {
+ super();
+ this.#browser = browser;
+ }
+
+ override url(): string {
+ return '';
+ }
+
+ override type(): TargetType {
+ return TargetType.BROWSER;
+ }
+
+ override asPage(): Promise<Page> {
+ throw new UnsupportedOperation();
+ }
+
+ override browser(): BidiBrowser {
+ return this.#browser;
+ }
+
+ override browserContext(): BidiBrowserContext {
+ return this.#browser.defaultBrowserContext();
+ }
+
+ override opener(): never {
+ throw new UnsupportedOperation();
+ }
+
+ override createCDPSession(): Promise<CDPSession> {
+ throw new UnsupportedOperation();
+ }
+}
+
+/**
+ * @internal
+ */
+export class BiDiBrowsingContextTarget extends BidiTarget {
+ protected _browsingContext: BrowsingContext;
+
+ constructor(
+ browserContext: BidiBrowserContext,
+ browsingContext: BrowsingContext
+ ) {
+ super(browserContext);
+
+ this._browsingContext = browsingContext;
+ }
+
+ override url(): string {
+ return this._browsingContext.url;
+ }
+
+ override async createCDPSession(): Promise<CDPSession> {
+ const {sessionId} = await this._browsingContext.cdpSession.send(
+ 'Target.attachToTarget',
+ {
+ targetId: this._browsingContext.id,
+ flatten: true,
+ }
+ );
+ return new CdpSessionWrapper(this._browsingContext, sessionId);
+ }
+
+ override type(): TargetType {
+ return TargetType.PAGE;
+ }
+}
+
+/**
+ * @internal
+ */
+export class BiDiPageTarget extends BiDiBrowsingContextTarget {
+ #page: BidiPage;
+
+ constructor(
+ browserContext: BidiBrowserContext,
+ browsingContext: BrowsingContext
+ ) {
+ super(browserContext, browsingContext);
+
+ this.#page = new BidiPage(browsingContext, browserContext, this);
+ }
+
+ override async page(): Promise<BidiPage> {
+ return this.#page;
+ }
+
+ override _setBrowserContext(browserContext: BidiBrowserContext): void {
+ super._setBrowserContext(browserContext);
+ this.#page._setBrowserContext(browserContext);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts
new file mode 100644
index 0000000000..373d6d999c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './BidiOverCdp.js';
+export * from './Browser.js';
+export * from './BrowserContext.js';
+export * from './BrowsingContext.js';
+export * from './Connection.js';
+export * from './ElementHandle.js';
+export * from './Frame.js';
+export * from './HTTPRequest.js';
+export * from './HTTPResponse.js';
+export * from './Input.js';
+export * from './JSHandle.js';
+export * from './NetworkManager.js';
+export * from './Page.js';
+export * from './Realm.js';
+export * from './Sandbox.js';
+export * from './Target.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts
new file mode 100644
index 0000000000..7c4a8ed01c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts
@@ -0,0 +1,225 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
+import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+import type {SharedWorkerRealm} from './Realm.js';
+import type {Session} from './Session.js';
+import {UserContext} from './UserContext.js';
+
+/**
+ * @internal
+ */
+export type AddPreloadScriptOptions = Omit<
+ Bidi.Script.AddPreloadScriptParameters,
+ 'functionDeclaration' | 'contexts'
+> & {
+ contexts?: [BrowsingContext, ...BrowsingContext[]];
+};
+
+/**
+ * @internal
+ */
+export class Browser extends EventEmitter<{
+ /** Emitted before the browser closes. */
+ closed: {
+ /** The reason for closing the browser. */
+ reason: string;
+ };
+ /** Emitted after the browser disconnects. */
+ disconnected: {
+ /** The reason for disconnecting the browser. */
+ reason: string;
+ };
+ /** Emitted when a shared worker is created. */
+ sharedworker: {
+ /** The realm of the shared worker. */
+ realm: SharedWorkerRealm;
+ };
+}> {
+ static async from(session: Session): Promise<Browser> {
+ const browser = new Browser(session);
+ await browser.#initialize();
+ return browser;
+ }
+
+ // keep-sorted start
+ #closed = false;
+ #reason: string | undefined;
+ readonly #disposables = new DisposableStack();
+ readonly #userContexts = new Map<string, UserContext>();
+ readonly session: Session;
+ // keep-sorted end
+
+ private constructor(session: Session) {
+ super();
+ // keep-sorted start
+ this.session = session;
+ // keep-sorted end
+
+ this.#userContexts.set(
+ UserContext.DEFAULT,
+ UserContext.create(this, UserContext.DEFAULT)
+ );
+ }
+
+ async #initialize() {
+ const sessionEmitter = this.#disposables.use(
+ new EventEmitter(this.session)
+ );
+ sessionEmitter.once('ended', ({reason}) => {
+ this.dispose(reason);
+ });
+
+ sessionEmitter.on('script.realmCreated', info => {
+ if (info.type === 'shared-worker') {
+ // TODO: Create a SharedWorkerRealm.
+ }
+ });
+
+ await this.#syncBrowsingContexts();
+ }
+
+ async #syncBrowsingContexts() {
+ // In case contexts are created or destroyed during `getTree`, we use this
+ // set to detect them.
+ const contextIds = new Set<string>();
+ let contexts: Bidi.BrowsingContext.Info[];
+
+ {
+ using sessionEmitter = new EventEmitter(this.session);
+ sessionEmitter.on('browsingContext.contextCreated', info => {
+ contextIds.add(info.context);
+ });
+ sessionEmitter.on('browsingContext.contextDestroyed', info => {
+ contextIds.delete(info.context);
+ });
+ const {result} = await this.session.send('browsingContext.getTree', {});
+ contexts = result.contexts;
+ }
+
+ // Simulating events so contexts are created naturally.
+ for (const info of contexts) {
+ if (contextIds.has(info.context)) {
+ this.session.emit('browsingContext.contextCreated', info);
+ }
+ if (info.children) {
+ contexts.push(...info.children);
+ }
+ }
+ }
+
+ // keep-sorted start block=yes
+ get closed(): boolean {
+ return this.#closed;
+ }
+ get defaultUserContext(): UserContext {
+ // SAFETY: A UserContext is always created for the default context.
+ return this.#userContexts.get(UserContext.DEFAULT)!;
+ }
+ get disconnected(): boolean {
+ return this.#reason !== undefined;
+ }
+ get disposed(): boolean {
+ return this.disconnected;
+ }
+ get userContexts(): Iterable<UserContext> {
+ return this.#userContexts.values();
+ }
+ // keep-sorted end
+
+ @inertIfDisposed
+ dispose(reason?: string, closed = false): void {
+ this.#closed = closed;
+ this.#reason = reason;
+ this[disposeSymbol]();
+ }
+
+ @throwIfDisposed<Browser>(browser => {
+ // SAFETY: By definition of `disposed`, `#reason` is defined.
+ return browser.#reason!;
+ })
+ async close(): Promise<void> {
+ try {
+ await this.session.send('browser.close', {});
+ } finally {
+ this.dispose('Browser already closed.', true);
+ }
+ }
+
+ @throwIfDisposed<Browser>(browser => {
+ // SAFETY: By definition of `disposed`, `#reason` is defined.
+ return browser.#reason!;
+ })
+ async addPreloadScript(
+ functionDeclaration: string,
+ options: AddPreloadScriptOptions = {}
+ ): Promise<string> {
+ const {
+ result: {script},
+ } = await this.session.send('script.addPreloadScript', {
+ functionDeclaration,
+ ...options,
+ contexts: options.contexts?.map(context => {
+ return context.id;
+ }) as [string, ...string[]],
+ });
+ return script;
+ }
+
+ @throwIfDisposed<Browser>(browser => {
+ // SAFETY: By definition of `disposed`, `#reason` is defined.
+ return browser.#reason!;
+ })
+ async removePreloadScript(script: string): Promise<void> {
+ await this.session.send('script.removePreloadScript', {
+ script,
+ });
+ }
+
+ static userContextId = 0;
+ @throwIfDisposed<Browser>(browser => {
+ // SAFETY: By definition of `disposed`, `#reason` is defined.
+ return browser.#reason!;
+ })
+ async createUserContext(): Promise<UserContext> {
+ // TODO: implement incognito context https://github.com/w3c/webdriver-bidi/issues/289.
+ // TODO: Call `createUserContext` once available.
+ // Generating a monotonically increasing context id.
+ const context = `${++Browser.userContextId}`;
+
+ const userContext = UserContext.create(this, context);
+ this.#userContexts.set(userContext.id, userContext);
+
+ const userContextEmitter = this.#disposables.use(
+ new EventEmitter(userContext)
+ );
+ userContextEmitter.once('closed', () => {
+ userContextEmitter.removeAllListeners();
+
+ this.#userContexts.delete(context);
+ });
+
+ return userContext;
+ }
+
+ [disposeSymbol](): void {
+ this.#reason ??=
+ 'Browser was disconnected, probably because the session ended.';
+ if (this.closed) {
+ this.emit('closed', {reason: this.#reason});
+ }
+ this.emit('disconnected', {reason: this.#reason});
+
+ this.#disposables.dispose();
+ super[disposeSymbol]();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts
new file mode 100644
index 0000000000..9bec2a506c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts
@@ -0,0 +1,475 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
+import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+
+import type {AddPreloadScriptOptions} from './Browser.js';
+import {Navigation} from './Navigation.js';
+import {WindowRealm} from './Realm.js';
+import {Request} from './Request.js';
+import type {UserContext} from './UserContext.js';
+import {UserPrompt} from './UserPrompt.js';
+
+/**
+ * @internal
+ */
+export type CaptureScreenshotOptions = Omit<
+ Bidi.BrowsingContext.CaptureScreenshotParameters,
+ 'context'
+>;
+
+/**
+ * @internal
+ */
+export type ReloadOptions = Omit<
+ Bidi.BrowsingContext.ReloadParameters,
+ 'context'
+>;
+
+/**
+ * @internal
+ */
+export type PrintOptions = Omit<
+ Bidi.BrowsingContext.PrintParameters,
+ 'context'
+>;
+
+/**
+ * @internal
+ */
+export type HandleUserPromptOptions = Omit<
+ Bidi.BrowsingContext.HandleUserPromptParameters,
+ 'context'
+>;
+
+/**
+ * @internal
+ */
+export type SetViewportOptions = Omit<
+ Bidi.BrowsingContext.SetViewportParameters,
+ 'context'
+>;
+
+/**
+ * @internal
+ */
+export class BrowsingContext extends EventEmitter<{
+ /** Emitted when this context is closed. */
+ closed: {
+ /** The reason the browsing context was closed */
+ reason: string;
+ };
+ /** Emitted when a child browsing context is created. */
+ browsingcontext: {
+ /** The newly created child browsing context. */
+ browsingContext: BrowsingContext;
+ };
+ /** Emitted whenever a navigation occurs. */
+ navigation: {
+ /** The navigation that occurred. */
+ navigation: Navigation;
+ };
+ /** Emitted whenever a request is made. */
+ request: {
+ /** The request that was made. */
+ request: Request;
+ };
+ /** Emitted whenever a log entry is added. */
+ log: {
+ /** Entry added to the log. */
+ entry: Bidi.Log.Entry;
+ };
+ /** Emitted whenever a prompt is opened. */
+ userprompt: {
+ /** The prompt that was opened. */
+ userPrompt: UserPrompt;
+ };
+ /** Emitted whenever the frame emits `DOMContentLoaded` */
+ DOMContentLoaded: void;
+ /** Emitted whenever the frame emits `load` */
+ load: void;
+}> {
+ static from(
+ userContext: UserContext,
+ parent: BrowsingContext | undefined,
+ id: string,
+ url: string
+ ): BrowsingContext {
+ const browsingContext = new BrowsingContext(userContext, parent, id, url);
+ browsingContext.#initialize();
+ return browsingContext;
+ }
+
+ // keep-sorted start
+ #navigation: Navigation | undefined;
+ #reason?: string;
+ #url: string;
+ readonly #children = new Map<string, BrowsingContext>();
+ readonly #disposables = new DisposableStack();
+ readonly #realms = new Map<string, WindowRealm>();
+ readonly #requests = new Map<string, Request>();
+ readonly defaultRealm: WindowRealm;
+ readonly id: string;
+ readonly parent: BrowsingContext | undefined;
+ readonly userContext: UserContext;
+ // keep-sorted end
+
+ private constructor(
+ context: UserContext,
+ parent: BrowsingContext | undefined,
+ id: string,
+ url: string
+ ) {
+ super();
+ // keep-sorted start
+ this.#url = url;
+ this.id = id;
+ this.parent = parent;
+ this.userContext = context;
+ // keep-sorted end
+
+ this.defaultRealm = WindowRealm.from(this);
+ }
+
+ #initialize() {
+ const userContextEmitter = this.#disposables.use(
+ new EventEmitter(this.userContext)
+ );
+ userContextEmitter.once('closed', ({reason}) => {
+ this.dispose(`Browsing context already closed: ${reason}`);
+ });
+
+ const sessionEmitter = this.#disposables.use(
+ new EventEmitter(this.#session)
+ );
+ sessionEmitter.on('browsingContext.contextCreated', info => {
+ if (info.parent !== this.id) {
+ return;
+ }
+
+ const browsingContext = BrowsingContext.from(
+ this.userContext,
+ this,
+ info.context,
+ info.url
+ );
+ this.#children.set(info.context, browsingContext);
+
+ const browsingContextEmitter = this.#disposables.use(
+ new EventEmitter(browsingContext)
+ );
+ browsingContextEmitter.once('closed', () => {
+ browsingContextEmitter.removeAllListeners();
+
+ this.#children.delete(browsingContext.id);
+ });
+
+ this.emit('browsingcontext', {browsingContext});
+ });
+ sessionEmitter.on('browsingContext.contextDestroyed', info => {
+ if (info.context !== this.id) {
+ return;
+ }
+ this.dispose('Browsing context already closed.');
+ });
+
+ sessionEmitter.on('browsingContext.domContentLoaded', info => {
+ if (info.context !== this.id) {
+ return;
+ }
+ this.#url = info.url;
+ this.emit('DOMContentLoaded', undefined);
+ });
+
+ sessionEmitter.on('browsingContext.load', info => {
+ if (info.context !== this.id) {
+ return;
+ }
+ this.#url = info.url;
+ this.emit('load', undefined);
+ });
+
+ sessionEmitter.on('browsingContext.navigationStarted', info => {
+ if (info.context !== this.id) {
+ return;
+ }
+ this.#url = info.url;
+
+ this.#requests.clear();
+
+ // Note the navigation ID is null for this event.
+ this.#navigation = Navigation.from(this);
+
+ const navigationEmitter = this.#disposables.use(
+ new EventEmitter(this.#navigation)
+ );
+ for (const eventName of ['fragment', 'failed', 'aborted'] as const) {
+ navigationEmitter.once(eventName, ({url}) => {
+ navigationEmitter[disposeSymbol]();
+
+ this.#url = url;
+ });
+ }
+
+ this.emit('navigation', {navigation: this.#navigation});
+ });
+ sessionEmitter.on('network.beforeRequestSent', event => {
+ if (event.context !== this.id) {
+ return;
+ }
+ if (this.#requests.has(event.request.request)) {
+ return;
+ }
+
+ const request = Request.from(this, event);
+ this.#requests.set(request.id, request);
+ this.emit('request', {request});
+ });
+
+ sessionEmitter.on('log.entryAdded', entry => {
+ if (entry.source.context !== this.id) {
+ return;
+ }
+
+ this.emit('log', {entry});
+ });
+
+ sessionEmitter.on('browsingContext.userPromptOpened', info => {
+ if (info.context !== this.id) {
+ return;
+ }
+
+ const userPrompt = UserPrompt.from(this, info);
+ this.emit('userprompt', {userPrompt});
+ });
+ }
+
+ // keep-sorted start block=yes
+ get #session() {
+ return this.userContext.browser.session;
+ }
+ get children(): Iterable<BrowsingContext> {
+ return this.#children.values();
+ }
+ get closed(): boolean {
+ return this.#reason !== undefined;
+ }
+ get disposed(): boolean {
+ return this.closed;
+ }
+ get realms(): Iterable<WindowRealm> {
+ return this.#realms.values();
+ }
+ get top(): BrowsingContext {
+ let context = this as BrowsingContext;
+ for (let {parent} = context; parent; {parent} = context) {
+ context = parent;
+ }
+ return context;
+ }
+ get url(): string {
+ return this.#url;
+ }
+ // keep-sorted end
+
+ @inertIfDisposed
+ private dispose(reason?: string): void {
+ this.#reason = reason;
+ this[disposeSymbol]();
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async activate(): Promise<void> {
+ await this.#session.send('browsingContext.activate', {
+ context: this.id,
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async captureScreenshot(
+ options: CaptureScreenshotOptions = {}
+ ): Promise<string> {
+ const {
+ result: {data},
+ } = await this.#session.send('browsingContext.captureScreenshot', {
+ context: this.id,
+ ...options,
+ });
+ return data;
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async close(promptUnload?: boolean): Promise<void> {
+ await Promise.all(
+ [...this.#children.values()].map(async child => {
+ await child.close(promptUnload);
+ })
+ );
+ await this.#session.send('browsingContext.close', {
+ context: this.id,
+ promptUnload,
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async traverseHistory(delta: number): Promise<void> {
+ await this.#session.send('browsingContext.traverseHistory', {
+ context: this.id,
+ delta,
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async navigate(
+ url: string,
+ wait?: Bidi.BrowsingContext.ReadinessState
+ ): Promise<Navigation> {
+ await this.#session.send('browsingContext.navigate', {
+ context: this.id,
+ url,
+ wait,
+ });
+ return await new Promise(resolve => {
+ this.once('navigation', ({navigation}) => {
+ resolve(navigation);
+ });
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async reload(options: ReloadOptions = {}): Promise<Navigation> {
+ await this.#session.send('browsingContext.reload', {
+ context: this.id,
+ ...options,
+ });
+ return await new Promise(resolve => {
+ this.once('navigation', ({navigation}) => {
+ resolve(navigation);
+ });
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async print(options: PrintOptions = {}): Promise<string> {
+ const {
+ result: {data},
+ } = await this.#session.send('browsingContext.print', {
+ context: this.id,
+ ...options,
+ });
+ return data;
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async handleUserPrompt(options: HandleUserPromptOptions = {}): Promise<void> {
+ await this.#session.send('browsingContext.handleUserPrompt', {
+ context: this.id,
+ ...options,
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async setViewport(options: SetViewportOptions = {}): Promise<void> {
+ await this.#session.send('browsingContext.setViewport', {
+ context: this.id,
+ ...options,
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async performActions(actions: Bidi.Input.SourceActions[]): Promise<void> {
+ await this.#session.send('input.performActions', {
+ context: this.id,
+ actions,
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async releaseActions(): Promise<void> {
+ await this.#session.send('input.releaseActions', {
+ context: this.id,
+ });
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ createWindowRealm(sandbox: string): WindowRealm {
+ return WindowRealm.from(this, sandbox);
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async addPreloadScript(
+ functionDeclaration: string,
+ options: AddPreloadScriptOptions = {}
+ ): Promise<string> {
+ return await this.userContext.browser.addPreloadScript(
+ functionDeclaration,
+ {
+ ...options,
+ contexts: [this, ...(options.contexts ?? [])],
+ }
+ );
+ }
+
+ @throwIfDisposed<BrowsingContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async removePreloadScript(script: string): Promise<void> {
+ await this.userContext.browser.removePreloadScript(script);
+ }
+
+ [disposeSymbol](): void {
+ this.#reason ??=
+ 'Browsing context already closed, probably because the user context closed.';
+ this.emit('closed', {reason: this.#reason});
+
+ this.#disposables.dispose();
+ super[disposeSymbol]();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts
new file mode 100644
index 0000000000..b9de14372b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts
@@ -0,0 +1,139 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import type {EventEmitter} from '../../common/EventEmitter.js';
+
+/**
+ * @internal
+ */
+export interface Commands {
+ 'script.evaluate': {
+ params: Bidi.Script.EvaluateParameters;
+ returnType: Bidi.Script.EvaluateResult;
+ };
+ 'script.callFunction': {
+ params: Bidi.Script.CallFunctionParameters;
+ returnType: Bidi.Script.EvaluateResult;
+ };
+ 'script.disown': {
+ params: Bidi.Script.DisownParameters;
+ returnType: Bidi.EmptyResult;
+ };
+ 'script.addPreloadScript': {
+ params: Bidi.Script.AddPreloadScriptParameters;
+ returnType: Bidi.Script.AddPreloadScriptResult;
+ };
+ 'script.removePreloadScript': {
+ params: Bidi.Script.RemovePreloadScriptParameters;
+ returnType: Bidi.EmptyResult;
+ };
+
+ 'browser.close': {
+ params: Bidi.EmptyParams;
+ returnType: Bidi.EmptyResult;
+ };
+
+ 'browsingContext.activate': {
+ params: Bidi.BrowsingContext.ActivateParameters;
+ returnType: Bidi.EmptyResult;
+ };
+ 'browsingContext.create': {
+ params: Bidi.BrowsingContext.CreateParameters;
+ returnType: Bidi.BrowsingContext.CreateResult;
+ };
+ 'browsingContext.close': {
+ params: Bidi.BrowsingContext.CloseParameters;
+ returnType: Bidi.EmptyResult;
+ };
+ 'browsingContext.getTree': {
+ params: Bidi.BrowsingContext.GetTreeParameters;
+ returnType: Bidi.BrowsingContext.GetTreeResult;
+ };
+ 'browsingContext.navigate': {
+ params: Bidi.BrowsingContext.NavigateParameters;
+ returnType: Bidi.BrowsingContext.NavigateResult;
+ };
+ 'browsingContext.reload': {
+ params: Bidi.BrowsingContext.ReloadParameters;
+ returnType: Bidi.BrowsingContext.NavigateResult;
+ };
+ 'browsingContext.print': {
+ params: Bidi.BrowsingContext.PrintParameters;
+ returnType: Bidi.BrowsingContext.PrintResult;
+ };
+ 'browsingContext.captureScreenshot': {
+ params: Bidi.BrowsingContext.CaptureScreenshotParameters;
+ returnType: Bidi.BrowsingContext.CaptureScreenshotResult;
+ };
+ 'browsingContext.handleUserPrompt': {
+ params: Bidi.BrowsingContext.HandleUserPromptParameters;
+ returnType: Bidi.EmptyResult;
+ };
+ 'browsingContext.setViewport': {
+ params: Bidi.BrowsingContext.SetViewportParameters;
+ returnType: Bidi.EmptyResult;
+ };
+ 'browsingContext.traverseHistory': {
+ params: Bidi.BrowsingContext.TraverseHistoryParameters;
+ returnType: Bidi.EmptyResult;
+ };
+
+ 'input.performActions': {
+ params: Bidi.Input.PerformActionsParameters;
+ returnType: Bidi.EmptyResult;
+ };
+ 'input.releaseActions': {
+ params: Bidi.Input.ReleaseActionsParameters;
+ returnType: Bidi.EmptyResult;
+ };
+
+ 'session.end': {
+ params: Bidi.EmptyParams;
+ returnType: Bidi.EmptyResult;
+ };
+ 'session.new': {
+ params: Bidi.Session.NewParameters;
+ returnType: Bidi.Session.NewResult;
+ };
+ 'session.status': {
+ params: object;
+ returnType: Bidi.Session.StatusResult;
+ };
+ 'session.subscribe': {
+ params: Bidi.Session.SubscriptionRequest;
+ returnType: Bidi.EmptyResult;
+ };
+ 'session.unsubscribe': {
+ params: Bidi.Session.SubscriptionRequest;
+ returnType: Bidi.EmptyResult;
+ };
+}
+
+/**
+ * @internal
+ */
+export type BidiEvents = {
+ [K in Bidi.ChromiumBidi.Event['method']]: Extract<
+ Bidi.ChromiumBidi.Event,
+ {method: K}
+ >['params'];
+};
+
+/**
+ * @internal
+ */
+export interface Connection<Events extends BidiEvents = BidiEvents>
+ extends EventEmitter<Events> {
+ send<T extends keyof Commands>(
+ method: T,
+ params: Commands[T]['params']
+ ): Promise<{result: Commands[T]['returnType']}>;
+
+ // This will pipe events into the provided emitter.
+ pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts
new file mode 100644
index 0000000000..a7efbfeb2c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts
@@ -0,0 +1,144 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {inertIfDisposed} from '../../util/decorators.js';
+import {Deferred} from '../../util/Deferred.js';
+import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+import type {Request} from './Request.js';
+
+/**
+ * @internal
+ */
+export interface NavigationInfo {
+ url: string;
+ timestamp: Date;
+}
+
+/**
+ * @internal
+ */
+export class Navigation extends EventEmitter<{
+ /** Emitted when navigation has a request associated with it. */
+ request: Request;
+ /** Emitted when fragment navigation occurred. */
+ fragment: NavigationInfo;
+ /** Emitted when navigation failed. */
+ failed: NavigationInfo;
+ /** Emitted when navigation was aborted. */
+ aborted: NavigationInfo;
+}> {
+ static from(context: BrowsingContext): Navigation {
+ const navigation = new Navigation(context);
+ navigation.#initialize();
+ return navigation;
+ }
+
+ // keep-sorted start
+ #request: Request | undefined;
+ readonly #browsingContext: BrowsingContext;
+ readonly #disposables = new DisposableStack();
+ readonly #id = new Deferred<string>();
+ // keep-sorted end
+
+ private constructor(context: BrowsingContext) {
+ super();
+ // keep-sorted start
+ this.#browsingContext = context;
+ // keep-sorted end
+ }
+
+ #initialize() {
+ const browsingContextEmitter = this.#disposables.use(
+ new EventEmitter(this.#browsingContext)
+ );
+ browsingContextEmitter.once('closed', () => {
+ this.emit('failed', {
+ url: this.#browsingContext.url,
+ timestamp: new Date(),
+ });
+ this.dispose();
+ });
+
+ this.#browsingContext.on('request', ({request}) => {
+ if (request.navigation === this.#id.value()) {
+ this.#request = request;
+ this.emit('request', request);
+ }
+ });
+
+ const sessionEmitter = this.#disposables.use(
+ new EventEmitter(this.#session)
+ );
+ // To get the navigation ID if any.
+ for (const eventName of [
+ 'browsingContext.domContentLoaded',
+ 'browsingContext.load',
+ ] as const) {
+ sessionEmitter.on(eventName, info => {
+ if (info.context !== this.#browsingContext.id) {
+ return;
+ }
+ if (!info.navigation) {
+ return;
+ }
+ if (!this.#id.resolved()) {
+ this.#id.resolve(info.navigation);
+ }
+ });
+ }
+
+ for (const [eventName, event] of [
+ ['browsingContext.fragmentNavigated', 'fragment'],
+ ['browsingContext.navigationFailed', 'failed'],
+ ['browsingContext.navigationAborted', 'aborted'],
+ ] as const) {
+ sessionEmitter.on(eventName, info => {
+ if (info.context !== this.#browsingContext.id) {
+ return;
+ }
+ if (!info.navigation) {
+ return;
+ }
+ if (!this.#id.resolved()) {
+ this.#id.resolve(info.navigation);
+ }
+ if (this.#id.value() !== info.navigation) {
+ return;
+ }
+ this.emit(event, {
+ url: info.url,
+ timestamp: new Date(info.timestamp),
+ });
+ this.dispose();
+ });
+ }
+ }
+
+ // keep-sorted start block=yes
+ get #session() {
+ return this.#browsingContext.userContext.browser.session;
+ }
+ get disposed(): boolean {
+ return this.#disposables.disposed;
+ }
+ get request(): Request | undefined {
+ return this.#request;
+ }
+ // keep-sorted end
+
+ @inertIfDisposed
+ private dispose(): void {
+ this[disposeSymbol]();
+ }
+
+ [disposeSymbol](): void {
+ this.#disposables.dispose();
+ super[disposeSymbol]();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts
new file mode 100644
index 0000000000..d9bbbede50
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts
@@ -0,0 +1,351 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
+import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+import type {Session} from './Session.js';
+
+/**
+ * @internal
+ */
+export type CallFunctionOptions = Omit<
+ Bidi.Script.CallFunctionParameters,
+ 'functionDeclaration' | 'awaitPromise' | 'target'
+>;
+
+/**
+ * @internal
+ */
+export type EvaluateOptions = Omit<
+ Bidi.Script.EvaluateParameters,
+ 'expression' | 'awaitPromise' | 'target'
+>;
+
+/**
+ * @internal
+ */
+export abstract class Realm extends EventEmitter<{
+ /** Emitted when the realm is destroyed. */
+ destroyed: {reason: string};
+ /** Emitted when a dedicated worker is created in the realm. */
+ worker: DedicatedWorkerRealm;
+ /** Emitted when a shared worker is created in the realm. */
+ sharedworker: SharedWorkerRealm;
+}> {
+ // keep-sorted start
+ #reason?: string;
+ protected readonly disposables = new DisposableStack();
+ readonly id: string;
+ readonly origin: string;
+ // keep-sorted end
+
+ protected constructor(id: string, origin: string) {
+ super();
+ // keep-sorted start
+ this.id = id;
+ this.origin = origin;
+ // keep-sorted end
+ }
+
+ protected initialize(): void {
+ const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
+ sessionEmitter.on('script.realmDestroyed', info => {
+ if (info.realm !== this.id) {
+ return;
+ }
+ this.dispose('Realm already destroyed.');
+ });
+ }
+
+ // keep-sorted start block=yes
+ get disposed(): boolean {
+ return this.#reason !== undefined;
+ }
+ protected abstract get session(): Session;
+ protected get target(): Bidi.Script.Target {
+ return {realm: this.id};
+ }
+ // keep-sorted end
+
+ @inertIfDisposed
+ protected dispose(reason?: string): void {
+ this.#reason = reason;
+ this[disposeSymbol]();
+ }
+
+ @throwIfDisposed<Realm>(realm => {
+ // SAFETY: Disposal implies this exists.
+ return realm.#reason!;
+ })
+ async disown(handles: string[]): Promise<void> {
+ await this.session.send('script.disown', {
+ target: this.target,
+ handles,
+ });
+ }
+
+ @throwIfDisposed<Realm>(realm => {
+ // SAFETY: Disposal implies this exists.
+ return realm.#reason!;
+ })
+ async callFunction(
+ functionDeclaration: string,
+ awaitPromise: boolean,
+ options: CallFunctionOptions = {}
+ ): Promise<Bidi.Script.EvaluateResult> {
+ const {result} = await this.session.send('script.callFunction', {
+ functionDeclaration,
+ awaitPromise,
+ target: this.target,
+ ...options,
+ });
+ return result;
+ }
+
+ @throwIfDisposed<Realm>(realm => {
+ // SAFETY: Disposal implies this exists.
+ return realm.#reason!;
+ })
+ async evaluate(
+ expression: string,
+ awaitPromise: boolean,
+ options: EvaluateOptions = {}
+ ): Promise<Bidi.Script.EvaluateResult> {
+ const {result} = await this.session.send('script.evaluate', {
+ expression,
+ awaitPromise,
+ target: this.target,
+ ...options,
+ });
+ return result;
+ }
+
+ [disposeSymbol](): void {
+ this.#reason ??=
+ 'Realm already destroyed, probably because all associated browsing contexts closed.';
+ this.emit('destroyed', {reason: this.#reason});
+
+ this.disposables.dispose();
+ super[disposeSymbol]();
+ }
+}
+
+/**
+ * @internal
+ */
+export class WindowRealm extends Realm {
+ static from(context: BrowsingContext, sandbox?: string): WindowRealm {
+ const realm = new WindowRealm(context, sandbox);
+ realm.initialize();
+ return realm;
+ }
+
+ // keep-sorted start
+ readonly browsingContext: BrowsingContext;
+ readonly sandbox?: string;
+ // keep-sorted end
+
+ readonly #workers: {
+ dedicated: Map<string, DedicatedWorkerRealm>;
+ shared: Map<string, SharedWorkerRealm>;
+ } = {
+ dedicated: new Map(),
+ shared: new Map(),
+ };
+
+ private constructor(context: BrowsingContext, sandbox?: string) {
+ super('', '');
+ // keep-sorted start
+ this.browsingContext = context;
+ this.sandbox = sandbox;
+ // keep-sorted end
+ }
+
+ override initialize(): void {
+ super.initialize();
+
+ const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
+ sessionEmitter.on('script.realmCreated', info => {
+ if (info.type !== 'window') {
+ return;
+ }
+ (this as any).id = info.realm;
+ (this as any).origin = info.origin;
+ });
+ sessionEmitter.on('script.realmCreated', info => {
+ if (info.type !== 'dedicated-worker') {
+ return;
+ }
+ if (!info.owners.includes(this.id)) {
+ return;
+ }
+
+ const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
+ this.#workers.dedicated.set(realm.id, realm);
+
+ const realmEmitter = this.disposables.use(new EventEmitter(realm));
+ realmEmitter.once('destroyed', () => {
+ realmEmitter.removeAllListeners();
+ this.#workers.dedicated.delete(realm.id);
+ });
+
+ this.emit('worker', realm);
+ });
+
+ this.browsingContext.userContext.browser.on('sharedworker', ({realm}) => {
+ if (!realm.owners.has(this)) {
+ return;
+ }
+
+ this.#workers.shared.set(realm.id, realm);
+
+ const realmEmitter = this.disposables.use(new EventEmitter(realm));
+ realmEmitter.once('destroyed', () => {
+ realmEmitter.removeAllListeners();
+ this.#workers.shared.delete(realm.id);
+ });
+
+ this.emit('sharedworker', realm);
+ });
+ }
+
+ override get session(): Session {
+ return this.browsingContext.userContext.browser.session;
+ }
+
+ override get target(): Bidi.Script.Target {
+ return {context: this.browsingContext.id, sandbox: this.sandbox};
+ }
+}
+
+/**
+ * @internal
+ */
+export type DedicatedWorkerOwnerRealm =
+ | DedicatedWorkerRealm
+ | SharedWorkerRealm
+ | WindowRealm;
+
+/**
+ * @internal
+ */
+export class DedicatedWorkerRealm extends Realm {
+ static from(
+ owner: DedicatedWorkerOwnerRealm,
+ id: string,
+ origin: string
+ ): DedicatedWorkerRealm {
+ const realm = new DedicatedWorkerRealm(owner, id, origin);
+ realm.initialize();
+ return realm;
+ }
+
+ // keep-sorted start
+ readonly #workers = new Map<string, DedicatedWorkerRealm>();
+ readonly owners: Set<DedicatedWorkerOwnerRealm>;
+ // keep-sorted end
+
+ private constructor(
+ owner: DedicatedWorkerOwnerRealm,
+ id: string,
+ origin: string
+ ) {
+ super(id, origin);
+ this.owners = new Set([owner]);
+ }
+
+ override initialize(): void {
+ super.initialize();
+
+ const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
+ sessionEmitter.on('script.realmCreated', info => {
+ if (info.type !== 'dedicated-worker') {
+ return;
+ }
+ if (!info.owners.includes(this.id)) {
+ return;
+ }
+
+ const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
+ this.#workers.set(realm.id, realm);
+
+ const realmEmitter = this.disposables.use(new EventEmitter(realm));
+ realmEmitter.once('destroyed', () => {
+ this.#workers.delete(realm.id);
+ });
+
+ this.emit('worker', realm);
+ });
+ }
+
+ override get session(): Session {
+ // SAFETY: At least one owner will exist.
+ return this.owners.values().next().value.session;
+ }
+}
+
+/**
+ * @internal
+ */
+export class SharedWorkerRealm extends Realm {
+ static from(
+ owners: [WindowRealm, ...WindowRealm[]],
+ id: string,
+ origin: string
+ ): SharedWorkerRealm {
+ const realm = new SharedWorkerRealm(owners, id, origin);
+ realm.initialize();
+ return realm;
+ }
+
+ // keep-sorted start
+ readonly #workers = new Map<string, DedicatedWorkerRealm>();
+ readonly owners: Set<WindowRealm>;
+ // keep-sorted end
+
+ private constructor(
+ owners: [WindowRealm, ...WindowRealm[]],
+ id: string,
+ origin: string
+ ) {
+ super(id, origin);
+ this.owners = new Set(owners);
+ }
+
+ override initialize(): void {
+ super.initialize();
+
+ const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
+ sessionEmitter.on('script.realmCreated', info => {
+ if (info.type !== 'dedicated-worker') {
+ return;
+ }
+ if (!info.owners.includes(this.id)) {
+ return;
+ }
+
+ const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
+ this.#workers.set(realm.id, realm);
+
+ const realmEmitter = this.disposables.use(new EventEmitter(realm));
+ realmEmitter.once('destroyed', () => {
+ this.#workers.delete(realm.id);
+ });
+
+ this.emit('worker', realm);
+ });
+ }
+
+ override get session(): Session {
+ // SAFETY: At least one owner will exist.
+ return this.owners.values().next().value.session;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts
new file mode 100644
index 0000000000..2a445f7d87
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts
@@ -0,0 +1,148 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {inertIfDisposed} from '../../util/decorators.js';
+import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+
+/**
+ * @internal
+ */
+export class Request extends EventEmitter<{
+ /** Emitted when the request is redirected. */
+ redirect: Request;
+ /** Emitted when the request succeeds. */
+ success: Bidi.Network.ResponseData;
+ /** Emitted when the request fails. */
+ error: string;
+}> {
+ static from(
+ browsingContext: BrowsingContext,
+ event: Bidi.Network.BeforeRequestSentParameters
+ ): Request {
+ const request = new Request(browsingContext, event);
+ request.#initialize();
+ return request;
+ }
+
+ // keep-sorted start
+ #error?: string;
+ #redirect?: Request;
+ #response?: Bidi.Network.ResponseData;
+ readonly #browsingContext: BrowsingContext;
+ readonly #disposables = new DisposableStack();
+ readonly #event: Bidi.Network.BeforeRequestSentParameters;
+ // keep-sorted end
+
+ private constructor(
+ browsingContext: BrowsingContext,
+ event: Bidi.Network.BeforeRequestSentParameters
+ ) {
+ super();
+ // keep-sorted start
+ this.#browsingContext = browsingContext;
+ this.#event = event;
+ // keep-sorted end
+ }
+
+ #initialize() {
+ const browsingContextEmitter = this.#disposables.use(
+ new EventEmitter(this.#browsingContext)
+ );
+ browsingContextEmitter.once('closed', ({reason}) => {
+ this.#error = reason;
+ this.emit('error', this.#error);
+ this.dispose();
+ });
+
+ const sessionEmitter = this.#disposables.use(
+ new EventEmitter(this.#session)
+ );
+ sessionEmitter.on('network.beforeRequestSent', event => {
+ if (event.context !== this.#browsingContext.id) {
+ return;
+ }
+ if (event.request.request !== this.id) {
+ return;
+ }
+ this.#redirect = Request.from(this.#browsingContext, event);
+ this.emit('redirect', this.#redirect);
+ this.dispose();
+ });
+ sessionEmitter.on('network.fetchError', event => {
+ if (event.context !== this.#browsingContext.id) {
+ return;
+ }
+ if (event.request.request !== this.id) {
+ return;
+ }
+ this.#error = event.errorText;
+ this.emit('error', this.#error);
+ this.dispose();
+ });
+ sessionEmitter.on('network.responseCompleted', event => {
+ if (event.context !== this.#browsingContext.id) {
+ return;
+ }
+ if (event.request.request !== this.id) {
+ return;
+ }
+ this.#response = event.response;
+ this.emit('success', this.#response);
+ this.dispose();
+ });
+ }
+
+ // keep-sorted start block=yes
+ get #session() {
+ return this.#browsingContext.userContext.browser.session;
+ }
+ get disposed(): boolean {
+ return this.#disposables.disposed;
+ }
+ get error(): string | undefined {
+ return this.#error;
+ }
+ get headers(): Bidi.Network.Header[] {
+ return this.#event.request.headers;
+ }
+ get id(): string {
+ return this.#event.request.request;
+ }
+ get initiator(): Bidi.Network.Initiator {
+ return this.#event.initiator;
+ }
+ get method(): string {
+ return this.#event.request.method;
+ }
+ get navigation(): string | undefined {
+ return this.#event.navigation ?? undefined;
+ }
+ get redirect(): Request | undefined {
+ return this.redirect;
+ }
+ get response(): Bidi.Network.ResponseData | undefined {
+ return this.#response;
+ }
+ get url(): string {
+ return this.#event.request.url;
+ }
+ // keep-sorted end
+
+ @inertIfDisposed
+ private dispose(): void {
+ this[disposeSymbol]();
+ }
+
+ [disposeSymbol](): void {
+ this.#disposables.dispose();
+ super[disposeSymbol]();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts
new file mode 100644
index 0000000000..b6e28061f1
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts
@@ -0,0 +1,180 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {debugError} from '../../common/util.js';
+import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
+import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+
+import {Browser} from './Browser.js';
+import type {BidiEvents, Commands, Connection} from './Connection.js';
+
+// TODO: Once Chrome supports session.status properly, uncomment this block.
+// const MAX_RETRIES = 5;
+
+/**
+ * @internal
+ */
+export class Session
+ extends EventEmitter<BidiEvents & {ended: {reason: string}}>
+ implements Connection<BidiEvents & {ended: {reason: string}}>
+{
+ static async from(
+ connection: Connection,
+ capabilities: Bidi.Session.CapabilitiesRequest
+ ): Promise<Session> {
+ // Wait until the session is ready.
+ //
+ // TODO: Once Chrome supports session.status properly, uncomment this block
+ // and remove `getBiDiConnection` in BrowserConnector.
+
+ // let status = {message: '', ready: false};
+ // for (let i = 0; i < MAX_RETRIES; ++i) {
+ // status = (await connection.send('session.status', {})).result;
+ // if (status.ready) {
+ // break;
+ // }
+ // // Backoff a little bit each time.
+ // await new Promise(resolve => {
+ // return setTimeout(resolve, (1 << i) * 100);
+ // });
+ // }
+ // if (!status.ready) {
+ // throw new Error(status.message);
+ // }
+
+ let result;
+ try {
+ result = (
+ await connection.send('session.new', {
+ capabilities,
+ })
+ ).result;
+ } catch (err) {
+ // Chrome does not support session.new.
+ debugError(err);
+ result = {
+ sessionId: '',
+ capabilities: {
+ acceptInsecureCerts: false,
+ browserName: '',
+ browserVersion: '',
+ platformName: '',
+ setWindowRect: false,
+ webSocketUrl: '',
+ },
+ };
+ }
+
+ const session = new Session(connection, result);
+ await session.#initialize();
+ return session;
+ }
+
+ // keep-sorted start
+ #reason: string | undefined;
+ readonly #disposables = new DisposableStack();
+ readonly #info: Bidi.Session.NewResult;
+ readonly browser!: Browser;
+ readonly connection: Connection;
+ // keep-sorted end
+
+ private constructor(connection: Connection, info: Bidi.Session.NewResult) {
+ super();
+ // keep-sorted start
+ this.#info = info;
+ this.connection = connection;
+ // keep-sorted end
+ }
+
+ async #initialize(): Promise<void> {
+ this.connection.pipeTo(this);
+
+ // SAFETY: We use `any` to allow assignment of the readonly property.
+ (this as any).browser = await Browser.from(this);
+
+ const browserEmitter = this.#disposables.use(this.browser);
+ browserEmitter.once('closed', ({reason}) => {
+ this.dispose(reason);
+ });
+ }
+
+ // keep-sorted start block=yes
+ get capabilities(): Bidi.Session.NewResult['capabilities'] {
+ return this.#info.capabilities;
+ }
+ get disposed(): boolean {
+ return this.ended;
+ }
+ get ended(): boolean {
+ return this.#reason !== undefined;
+ }
+ get id(): string {
+ return this.#info.sessionId;
+ }
+ // keep-sorted end
+
+ @inertIfDisposed
+ private dispose(reason?: string): void {
+ this.#reason = reason;
+ this[disposeSymbol]();
+ }
+
+ pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
+ this.connection.pipeTo(emitter);
+ }
+
+ /**
+ * Currently, there is a 1:1 relationship between the session and the
+ * session. In the future, we might support multiple sessions and in that
+ * case we always needs to make sure that the session for the right session
+ * object is used, so we implement this method here, although it's not defined
+ * in the spec.
+ */
+ @throwIfDisposed<Session>(session => {
+ // SAFETY: By definition of `disposed`, `#reason` is defined.
+ return session.#reason!;
+ })
+ async send<T extends keyof Commands>(
+ method: T,
+ params: Commands[T]['params']
+ ): Promise<{result: Commands[T]['returnType']}> {
+ return await this.connection.send(method, params);
+ }
+
+ @throwIfDisposed<Session>(session => {
+ // SAFETY: By definition of `disposed`, `#reason` is defined.
+ return session.#reason!;
+ })
+ async subscribe(events: string[]): Promise<void> {
+ await this.send('session.subscribe', {
+ events,
+ });
+ }
+
+ @throwIfDisposed<Session>(session => {
+ // SAFETY: By definition of `disposed`, `#reason` is defined.
+ return session.#reason!;
+ })
+ async end(): Promise<void> {
+ try {
+ await this.send('session.end', {});
+ } finally {
+ this.dispose(`Session already ended.`);
+ }
+ }
+
+ [disposeSymbol](): void {
+ this.#reason ??=
+ 'Session already destroyed, probably because the connection broke.';
+ this.emit('ended', {reason: this.#reason});
+
+ this.#disposables.dispose();
+ super[disposeSymbol]();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts
new file mode 100644
index 0000000000..01ee5c7649
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts
@@ -0,0 +1,178 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {assert} from '../../util/assert.js';
+import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
+import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+
+import type {Browser} from './Browser.js';
+import {BrowsingContext} from './BrowsingContext.js';
+
+/**
+ * @internal
+ */
+export type CreateBrowsingContextOptions = Omit<
+ Bidi.BrowsingContext.CreateParameters,
+ 'type' | 'referenceContext'
+> & {
+ referenceContext?: BrowsingContext;
+};
+
+/**
+ * @internal
+ */
+export class UserContext extends EventEmitter<{
+ /**
+ * Emitted when a new browsing context is created.
+ */
+ browsingcontext: {
+ /** The new browsing context. */
+ browsingContext: BrowsingContext;
+ };
+ /**
+ * Emitted when the user context is closed.
+ */
+ closed: {
+ /** The reason the user context was closed. */
+ reason: string;
+ };
+}> {
+ static DEFAULT = 'default';
+
+ static create(browser: Browser, id: string): UserContext {
+ const context = new UserContext(browser, id);
+ context.#initialize();
+ return context;
+ }
+
+ // keep-sorted start
+ #reason?: string;
+ // Note these are only top-level contexts.
+ readonly #browsingContexts = new Map<string, BrowsingContext>();
+ readonly #disposables = new DisposableStack();
+ readonly #id: string;
+ readonly browser: Browser;
+ // keep-sorted end
+
+ private constructor(browser: Browser, id: string) {
+ super();
+ // keep-sorted start
+ this.#id = id;
+ this.browser = browser;
+ // keep-sorted end
+ }
+
+ #initialize() {
+ const browserEmitter = this.#disposables.use(
+ new EventEmitter(this.browser)
+ );
+ browserEmitter.once('closed', ({reason}) => {
+ this.dispose(`User context already closed: ${reason}`);
+ });
+
+ const sessionEmitter = this.#disposables.use(
+ new EventEmitter(this.#session)
+ );
+ sessionEmitter.on('browsingContext.contextCreated', info => {
+ if (info.parent) {
+ return;
+ }
+
+ const browsingContext = BrowsingContext.from(
+ this,
+ undefined,
+ info.context,
+ info.url
+ );
+ this.#browsingContexts.set(browsingContext.id, browsingContext);
+
+ const browsingContextEmitter = this.#disposables.use(
+ new EventEmitter(browsingContext)
+ );
+ browsingContextEmitter.on('closed', () => {
+ browsingContextEmitter.removeAllListeners();
+
+ this.#browsingContexts.delete(browsingContext.id);
+ });
+
+ this.emit('browsingcontext', {browsingContext});
+ });
+ }
+
+ // keep-sorted start block=yes
+ get #session() {
+ return this.browser.session;
+ }
+ get browsingContexts(): Iterable<BrowsingContext> {
+ return this.#browsingContexts.values();
+ }
+ get closed(): boolean {
+ return this.#reason !== undefined;
+ }
+ get disposed(): boolean {
+ return this.closed;
+ }
+ get id(): string {
+ return this.#id;
+ }
+ // keep-sorted end
+
+ @inertIfDisposed
+ private dispose(reason?: string): void {
+ this.#reason = reason;
+ this[disposeSymbol]();
+ }
+
+ @throwIfDisposed<UserContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async createBrowsingContext(
+ type: Bidi.BrowsingContext.CreateType,
+ options: CreateBrowsingContextOptions = {}
+ ): Promise<BrowsingContext> {
+ const {
+ result: {context: contextId},
+ } = await this.#session.send('browsingContext.create', {
+ type,
+ ...options,
+ referenceContext: options.referenceContext?.id,
+ });
+
+ const browsingContext = this.#browsingContexts.get(contextId);
+ assert(
+ browsingContext,
+ 'The WebDriver BiDi implementation is failing to create a browsing context correctly.'
+ );
+
+ // We use an array to avoid the promise from being awaited.
+ return browsingContext;
+ }
+
+ @throwIfDisposed<UserContext>(context => {
+ // SAFETY: Disposal implies this exists.
+ return context.#reason!;
+ })
+ async remove(): Promise<void> {
+ try {
+ // TODO: Call `removeUserContext` once available.
+ } finally {
+ this.dispose('User context already closed.');
+ }
+ }
+
+ [disposeSymbol](): void {
+ this.#reason ??=
+ 'User context already closed, probably because the browser disconnected/closed.';
+ this.emit('closed', {reason: this.#reason});
+
+ this.#disposables.dispose();
+ super[disposeSymbol]();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserPrompt.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserPrompt.ts
new file mode 100644
index 0000000000..073233bed0
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserPrompt.ts
@@ -0,0 +1,137 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
+import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
+
+import type {BrowsingContext} from './BrowsingContext.js';
+
+/**
+ * @internal
+ */
+export type HandleOptions = Omit<
+ Bidi.BrowsingContext.HandleUserPromptParameters,
+ 'context'
+>;
+
+/**
+ * @internal
+ */
+export type UserPromptResult = Omit<
+ Bidi.BrowsingContext.UserPromptClosedParameters,
+ 'context'
+>;
+
+/**
+ * @internal
+ */
+export class UserPrompt extends EventEmitter<{
+ /** Emitted when the user prompt is handled. */
+ handled: UserPromptResult;
+ /** Emitted when the user prompt is closed. */
+ closed: {
+ /** The reason the user prompt was closed. */
+ reason: string;
+ };
+}> {
+ static from(
+ browsingContext: BrowsingContext,
+ info: Bidi.BrowsingContext.UserPromptOpenedParameters
+ ): UserPrompt {
+ const userPrompt = new UserPrompt(browsingContext, info);
+ userPrompt.#initialize();
+ return userPrompt;
+ }
+
+ // keep-sorted start
+ #reason?: string;
+ #result?: UserPromptResult;
+ readonly #disposables = new DisposableStack();
+ readonly browsingContext: BrowsingContext;
+ readonly info: Bidi.BrowsingContext.UserPromptOpenedParameters;
+ // keep-sorted end
+
+ private constructor(
+ context: BrowsingContext,
+ info: Bidi.BrowsingContext.UserPromptOpenedParameters
+ ) {
+ super();
+ // keep-sorted start
+ this.browsingContext = context;
+ this.info = info;
+ // keep-sorted end
+ }
+
+ #initialize() {
+ const browserContextEmitter = this.#disposables.use(
+ new EventEmitter(this.browsingContext)
+ );
+ browserContextEmitter.once('closed', ({reason}) => {
+ this.dispose(`User prompt already closed: ${reason}`);
+ });
+
+ const sessionEmitter = this.#disposables.use(
+ new EventEmitter(this.#session)
+ );
+ sessionEmitter.on('browsingContext.userPromptClosed', parameters => {
+ if (parameters.context !== this.browsingContext.id) {
+ return;
+ }
+ this.#result = parameters;
+ this.emit('handled', parameters);
+ this.dispose('User prompt already handled.');
+ });
+ }
+
+ // keep-sorted start block=yes
+ get #session() {
+ return this.browsingContext.userContext.browser.session;
+ }
+ get closed(): boolean {
+ return this.#reason !== undefined;
+ }
+ get disposed(): boolean {
+ return this.closed;
+ }
+ get handled(): boolean {
+ return this.#result !== undefined;
+ }
+ get result(): UserPromptResult | undefined {
+ return this.#result;
+ }
+ // keep-sorted end
+
+ @inertIfDisposed
+ private dispose(reason?: string): void {
+ this.#reason = reason;
+ this[disposeSymbol]();
+ }
+
+ @throwIfDisposed<UserPrompt>(prompt => {
+ // SAFETY: Disposal implies this exists.
+ return prompt.#reason!;
+ })
+ async handle(options: HandleOptions = {}): Promise<UserPromptResult> {
+ await this.#session.send('browsingContext.handleUserPrompt', {
+ ...options,
+ context: this.info.context,
+ });
+ // SAFETY: `handled` is triggered before the above promise resolved.
+ return this.#result!;
+ }
+
+ [disposeSymbol](): void {
+ this.#reason ??=
+ 'User prompt already closed, probably because the associated browsing context was destroyed.';
+ this.emit('closed', {reason: this.#reason});
+
+ this.#disposables.dispose();
+ super[disposeSymbol]();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/core.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/core.ts
new file mode 100644
index 0000000000..203281614b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/core.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './Browser.js';
+export * from './BrowsingContext.js';
+export * from './Connection.js';
+export * from './Navigation.js';
+export * from './Realm.js';
+export * from './Request.js';
+export * from './Session.js';
+export * from './UserContext.js';
+export * from './UserPrompt.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts
new file mode 100644
index 0000000000..73b86cba9c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts
@@ -0,0 +1,119 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import type {
+ ObservableInput,
+ ObservedValueOf,
+ OperatorFunction,
+} from '../../third_party/rxjs/rxjs.js';
+import {catchError} from '../../third_party/rxjs/rxjs.js';
+import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
+import {ProtocolError, TimeoutError} from '../common/Errors.js';
+
+/**
+ * @internal
+ */
+export type BiDiNetworkIdle = Extract<
+ PuppeteerLifeCycleEvent,
+ 'networkidle0' | 'networkidle2'
+> | null;
+
+/**
+ * @internal
+ */
+export function getBiDiLifeCycles(
+ event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
+): [
+ Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'>,
+ BiDiNetworkIdle,
+] {
+ if (Array.isArray(event)) {
+ const pageLifeCycle = event.some(lifeCycle => {
+ return lifeCycle !== 'domcontentloaded';
+ })
+ ? 'load'
+ : 'domcontentloaded';
+
+ const networkLifeCycle = event.reduce((acc, lifeCycle) => {
+ if (lifeCycle === 'networkidle0') {
+ return lifeCycle;
+ } else if (acc !== 'networkidle0' && lifeCycle === 'networkidle2') {
+ return lifeCycle;
+ }
+ return acc;
+ }, null as BiDiNetworkIdle);
+
+ return [pageLifeCycle, networkLifeCycle];
+ }
+
+ if (event === 'networkidle0' || event === 'networkidle2') {
+ return ['load', event];
+ }
+
+ return [event, null];
+}
+
+/**
+ * @internal
+ */
+export const lifeCycleToReadinessState = new Map<
+ PuppeteerLifeCycleEvent,
+ Bidi.BrowsingContext.ReadinessState
+>([
+ ['load', Bidi.BrowsingContext.ReadinessState.Complete],
+ ['domcontentloaded', Bidi.BrowsingContext.ReadinessState.Interactive],
+]);
+
+export function getBiDiReadinessState(
+ event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
+): [Bidi.BrowsingContext.ReadinessState, BiDiNetworkIdle] {
+ const lifeCycles = getBiDiLifeCycles(event);
+ const readiness = lifeCycleToReadinessState.get(lifeCycles[0])!;
+ return [readiness, lifeCycles[1]];
+}
+
+/**
+ * @internal
+ */
+export const lifeCycleToSubscribedEvent = new Map<
+ PuppeteerLifeCycleEvent,
+ 'browsingContext.load' | 'browsingContext.domContentLoaded'
+>([
+ ['load', 'browsingContext.load'],
+ ['domcontentloaded', 'browsingContext.domContentLoaded'],
+]);
+
+/**
+ * @internal
+ */
+export function getBiDiLifecycleEvent(
+ event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
+): [
+ 'browsingContext.load' | 'browsingContext.domContentLoaded',
+ BiDiNetworkIdle,
+] {
+ const lifeCycles = getBiDiLifeCycles(event);
+ const bidiEvent = lifeCycleToSubscribedEvent.get(lifeCycles[0])!;
+ return [bidiEvent, lifeCycles[1]];
+}
+
+/**
+ * @internal
+ */
+export function rewriteNavigationError<T, R extends ObservableInput<T>>(
+ message: string,
+ ms: number
+): OperatorFunction<T, T | ObservedValueOf<R>> {
+ return catchError<T, R>(error => {
+ if (error instanceof ProtocolError) {
+ error.message += ` at ${message}`;
+ } else if (error instanceof TimeoutError) {
+ error.message = `Navigation timeout of ${ms} ms exceeded`;
+ }
+ throw error;
+ });
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts
new file mode 100644
index 0000000000..41e88e26c2
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {PuppeteerURL, debugError} from '../common/util.js';
+
+import {BidiDeserializer} from './Deserializer.js';
+import type {BidiRealm} from './Realm.js';
+
+/**
+ * @internal
+ */
+export async function releaseReference(
+ client: BidiRealm,
+ remoteReference: Bidi.Script.RemoteReference
+): Promise<void> {
+ if (!remoteReference.handle) {
+ return;
+ }
+ await client.connection
+ .send('script.disown', {
+ target: client.target,
+ handles: [remoteReference.handle],
+ })
+ .catch(error => {
+ // Exceptions might happen in case of a page been navigated or closed.
+ // Swallow these since they are harmless and we don't leak anything in this case.
+ debugError(error);
+ });
+}
+
+/**
+ * @internal
+ */
+export function createEvaluationError(
+ details: Bidi.Script.ExceptionDetails
+): unknown {
+ if (details.exception.type !== 'error') {
+ return BidiDeserializer.deserialize(details.exception);
+ }
+ const [name = '', ...parts] = details.text.split(': ');
+ const message = parts.join(': ');
+ const error = new Error(message);
+ error.name = name;
+
+ // The first line is this function which we ignore.
+ const stackLines = [];
+ if (details.stackTrace && stackLines.length < Error.stackTraceLimit) {
+ for (const frame of details.stackTrace.callFrames.reverse()) {
+ if (
+ PuppeteerURL.isPuppeteerURL(frame.url) &&
+ frame.url !== PuppeteerURL.INTERNAL_URL
+ ) {
+ const url = PuppeteerURL.parse(frame.url);
+ stackLines.unshift(
+ ` at ${frame.functionName || url.functionName} (${
+ url.functionName
+ } at ${url.siteString}, <anonymous>:${frame.lineNumber}:${
+ frame.columnNumber
+ })`
+ );
+ } else {
+ stackLines.push(
+ ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
+ frame.lineNumber
+ }:${frame.columnNumber})`
+ );
+ }
+ if (stackLines.length >= Error.stackTraceLimit) {
+ break;
+ }
+ }
+ }
+
+ error.stack = [details.text, ...stackLines].join('\n');
+ return error;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Accessibility.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Accessibility.ts
new file mode 100644
index 0000000000..d0279e3dda
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Accessibility.ts
@@ -0,0 +1,579 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {ElementHandle} from '../api/ElementHandle.js';
+
+/**
+ * Represents a Node and the properties of it that are relevant to Accessibility.
+ * @public
+ */
+export interface SerializedAXNode {
+ /**
+ * The {@link https://www.w3.org/TR/wai-aria/#usage_intro | role} of the node.
+ */
+ role: string;
+ /**
+ * A human readable name for the node.
+ */
+ name?: string;
+ /**
+ * The current value of the node.
+ */
+ value?: string | number;
+ /**
+ * An additional human readable description of the node.
+ */
+ description?: string;
+ /**
+ * Any keyboard shortcuts associated with this node.
+ */
+ keyshortcuts?: string;
+ /**
+ * A human readable alternative to the role.
+ */
+ roledescription?: string;
+ /**
+ * A description of the current value.
+ */
+ valuetext?: string;
+ disabled?: boolean;
+ expanded?: boolean;
+ focused?: boolean;
+ modal?: boolean;
+ multiline?: boolean;
+ /**
+ * Whether more than one child can be selected.
+ */
+ multiselectable?: boolean;
+ readonly?: boolean;
+ required?: boolean;
+ selected?: boolean;
+ /**
+ * Whether the checkbox is checked, or in a
+ * {@link https://www.w3.org/TR/wai-aria-practices/examples/checkbox/checkbox-2/checkbox-2.html | mixed state}.
+ */
+ checked?: boolean | 'mixed';
+ /**
+ * Whether the node is checked or in a mixed state.
+ */
+ pressed?: boolean | 'mixed';
+ /**
+ * The level of a heading.
+ */
+ level?: number;
+ valuemin?: number;
+ valuemax?: number;
+ autocomplete?: string;
+ haspopup?: string;
+ /**
+ * Whether and in what way this node's value is invalid.
+ */
+ invalid?: string;
+ orientation?: string;
+ /**
+ * Children of this node, if there are any.
+ */
+ children?: SerializedAXNode[];
+}
+
+/**
+ * @public
+ */
+export interface SnapshotOptions {
+ /**
+ * Prune uninteresting nodes from the tree.
+ * @defaultValue `true`
+ */
+ interestingOnly?: boolean;
+ /**
+ * Root node to get the accessibility tree for
+ * @defaultValue The root node of the entire page.
+ */
+ root?: ElementHandle<Node>;
+}
+
+/**
+ * The Accessibility class provides methods for inspecting the browser's
+ * accessibility tree. The accessibility tree is used by assistive technology
+ * such as {@link https://en.wikipedia.org/wiki/Screen_reader | screen readers} or
+ * {@link https://en.wikipedia.org/wiki/Switch_access | switches}.
+ *
+ * @remarks
+ *
+ * Accessibility is a very platform-specific thing. On different platforms,
+ * there are different screen readers that might have wildly different output.
+ *
+ * Blink - Chrome's rendering engine - has a concept of "accessibility tree",
+ * which is then translated into different platform-specific APIs. Accessibility
+ * namespace gives users access to the Blink Accessibility Tree.
+ *
+ * Most of the accessibility tree gets filtered out when converting from Blink
+ * AX Tree to Platform-specific AX-Tree or by assistive technologies themselves.
+ * By default, Puppeteer tries to approximate this filtering, exposing only
+ * the "interesting" nodes of the tree.
+ *
+ * @public
+ */
+export class Accessibility {
+ #client: CDPSession;
+
+ /**
+ * @internal
+ */
+ constructor(client: CDPSession) {
+ this.#client = client;
+ }
+
+ /**
+ * @internal
+ */
+ updateClient(client: CDPSession): void {
+ this.#client = client;
+ }
+
+ /**
+ * Captures the current state of the accessibility tree.
+ * The returned object represents the root accessible node of the page.
+ *
+ * @remarks
+ *
+ * **NOTE** The Chrome accessibility tree contains nodes that go unused on
+ * most platforms and by most screen readers. Puppeteer will discard them as
+ * well for an easier to process tree, unless `interestingOnly` is set to
+ * `false`.
+ *
+ * @example
+ * An example of dumping the entire accessibility tree:
+ *
+ * ```ts
+ * const snapshot = await page.accessibility.snapshot();
+ * console.log(snapshot);
+ * ```
+ *
+ * @example
+ * An example of logging the focused node's name:
+ *
+ * ```ts
+ * const snapshot = await page.accessibility.snapshot();
+ * const node = findFocusedNode(snapshot);
+ * console.log(node && node.name);
+ *
+ * function findFocusedNode(node) {
+ * if (node.focused) return node;
+ * for (const child of node.children || []) {
+ * const foundNode = findFocusedNode(child);
+ * return foundNode;
+ * }
+ * return null;
+ * }
+ * ```
+ *
+ * @returns An AXNode object representing the snapshot.
+ */
+ public async snapshot(
+ options: SnapshotOptions = {}
+ ): Promise<SerializedAXNode | null> {
+ const {interestingOnly = true, root = null} = options;
+ const {nodes} = await this.#client.send('Accessibility.getFullAXTree');
+ let backendNodeId: number | undefined;
+ if (root) {
+ const {node} = await this.#client.send('DOM.describeNode', {
+ objectId: root.id,
+ });
+ backendNodeId = node.backendNodeId;
+ }
+ const defaultRoot = AXNode.createTree(nodes);
+ let needle: AXNode | null = defaultRoot;
+ if (backendNodeId) {
+ needle = defaultRoot.find(node => {
+ return node.payload.backendDOMNodeId === backendNodeId;
+ });
+ if (!needle) {
+ return null;
+ }
+ }
+ if (!interestingOnly) {
+ return this.serializeTree(needle)[0] ?? null;
+ }
+
+ const interestingNodes = new Set<AXNode>();
+ this.collectInterestingNodes(interestingNodes, defaultRoot, false);
+ if (!interestingNodes.has(needle)) {
+ return null;
+ }
+ return this.serializeTree(needle, interestingNodes)[0] ?? null;
+ }
+
+ private serializeTree(
+ node: AXNode,
+ interestingNodes?: Set<AXNode>
+ ): SerializedAXNode[] {
+ const children: SerializedAXNode[] = [];
+ for (const child of node.children) {
+ children.push(...this.serializeTree(child, interestingNodes));
+ }
+
+ if (interestingNodes && !interestingNodes.has(node)) {
+ return children;
+ }
+
+ const serializedNode = node.serialize();
+ if (children.length) {
+ serializedNode.children = children;
+ }
+ return [serializedNode];
+ }
+
+ private collectInterestingNodes(
+ collection: Set<AXNode>,
+ node: AXNode,
+ insideControl: boolean
+ ): void {
+ if (node.isInteresting(insideControl)) {
+ collection.add(node);
+ }
+ if (node.isLeafNode()) {
+ return;
+ }
+ insideControl = insideControl || node.isControl();
+ for (const child of node.children) {
+ this.collectInterestingNodes(collection, child, insideControl);
+ }
+ }
+}
+
+class AXNode {
+ public payload: Protocol.Accessibility.AXNode;
+ public children: AXNode[] = [];
+
+ #richlyEditable = false;
+ #editable = false;
+ #focusable = false;
+ #hidden = false;
+ #name: string;
+ #role: string;
+ #ignored: boolean;
+ #cachedHasFocusableChild?: boolean;
+
+ constructor(payload: Protocol.Accessibility.AXNode) {
+ this.payload = payload;
+ this.#name = this.payload.name ? this.payload.name.value : '';
+ this.#role = this.payload.role ? this.payload.role.value : 'Unknown';
+ this.#ignored = this.payload.ignored;
+
+ for (const property of this.payload.properties || []) {
+ if (property.name === 'editable') {
+ this.#richlyEditable = property.value.value === 'richtext';
+ this.#editable = true;
+ }
+ if (property.name === 'focusable') {
+ this.#focusable = property.value.value;
+ }
+ if (property.name === 'hidden') {
+ this.#hidden = property.value.value;
+ }
+ }
+ }
+
+ #isPlainTextField(): boolean {
+ if (this.#richlyEditable) {
+ return false;
+ }
+ if (this.#editable) {
+ return true;
+ }
+ return this.#role === 'textbox' || this.#role === 'searchbox';
+ }
+
+ #isTextOnlyObject(): boolean {
+ const role = this.#role;
+ return (
+ role === 'LineBreak' ||
+ role === 'text' ||
+ role === 'InlineTextBox' ||
+ role === 'StaticText'
+ );
+ }
+
+ #hasFocusableChild(): boolean {
+ if (this.#cachedHasFocusableChild === undefined) {
+ this.#cachedHasFocusableChild = false;
+ for (const child of this.children) {
+ if (child.#focusable || child.#hasFocusableChild()) {
+ this.#cachedHasFocusableChild = true;
+ break;
+ }
+ }
+ }
+ return this.#cachedHasFocusableChild;
+ }
+
+ public find(predicate: (x: AXNode) => boolean): AXNode | null {
+ if (predicate(this)) {
+ return this;
+ }
+ for (const child of this.children) {
+ const result = child.find(predicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ public isLeafNode(): boolean {
+ if (!this.children.length) {
+ return true;
+ }
+
+ // These types of objects may have children that we use as internal
+ // implementation details, but we want to expose them as leaves to platform
+ // accessibility APIs because screen readers might be confused if they find
+ // any children.
+ if (this.#isPlainTextField() || this.#isTextOnlyObject()) {
+ return true;
+ }
+
+ // Roles whose children are only presentational according to the ARIA and
+ // HTML5 Specs should be hidden from screen readers.
+ // (Note that whilst ARIA buttons can have only presentational children, HTML5
+ // buttons are allowed to have content.)
+ switch (this.#role) {
+ case 'doc-cover':
+ case 'graphics-symbol':
+ case 'img':
+ case 'image':
+ case 'Meter':
+ case 'scrollbar':
+ case 'slider':
+ case 'separator':
+ case 'progressbar':
+ return true;
+ default:
+ break;
+ }
+
+ // Here and below: Android heuristics
+ if (this.#hasFocusableChild()) {
+ return false;
+ }
+ if (this.#focusable && this.#name) {
+ return true;
+ }
+ if (this.#role === 'heading' && this.#name) {
+ return true;
+ }
+ return false;
+ }
+
+ public isControl(): boolean {
+ switch (this.#role) {
+ case 'button':
+ case 'checkbox':
+ case 'ColorWell':
+ case 'combobox':
+ case 'DisclosureTriangle':
+ case 'listbox':
+ case 'menu':
+ case 'menubar':
+ case 'menuitem':
+ case 'menuitemcheckbox':
+ case 'menuitemradio':
+ case 'radio':
+ case 'scrollbar':
+ case 'searchbox':
+ case 'slider':
+ case 'spinbutton':
+ case 'switch':
+ case 'tab':
+ case 'textbox':
+ case 'tree':
+ case 'treeitem':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public isInteresting(insideControl: boolean): boolean {
+ const role = this.#role;
+ if (role === 'Ignored' || this.#hidden || this.#ignored) {
+ return false;
+ }
+
+ if (this.#focusable || this.#richlyEditable) {
+ return true;
+ }
+
+ // If it's not focusable but has a control role, then it's interesting.
+ if (this.isControl()) {
+ return true;
+ }
+
+ // A non focusable child of a control is not interesting
+ if (insideControl) {
+ return false;
+ }
+
+ return this.isLeafNode() && !!this.#name;
+ }
+
+ public serialize(): SerializedAXNode {
+ const properties = new Map<string, number | string | boolean>();
+ for (const property of this.payload.properties || []) {
+ properties.set(property.name.toLowerCase(), property.value.value);
+ }
+ if (this.payload.name) {
+ properties.set('name', this.payload.name.value);
+ }
+ if (this.payload.value) {
+ properties.set('value', this.payload.value.value);
+ }
+ if (this.payload.description) {
+ properties.set('description', this.payload.description.value);
+ }
+
+ const node: SerializedAXNode = {
+ role: this.#role,
+ };
+
+ type UserStringProperty =
+ | 'name'
+ | 'value'
+ | 'description'
+ | 'keyshortcuts'
+ | 'roledescription'
+ | 'valuetext';
+
+ const userStringProperties: UserStringProperty[] = [
+ 'name',
+ 'value',
+ 'description',
+ 'keyshortcuts',
+ 'roledescription',
+ 'valuetext',
+ ];
+ const getUserStringPropertyValue = (key: UserStringProperty): string => {
+ return properties.get(key) as string;
+ };
+
+ for (const userStringProperty of userStringProperties) {
+ if (!properties.has(userStringProperty)) {
+ continue;
+ }
+
+ node[userStringProperty] = getUserStringPropertyValue(userStringProperty);
+ }
+
+ type BooleanProperty =
+ | 'disabled'
+ | 'expanded'
+ | 'focused'
+ | 'modal'
+ | 'multiline'
+ | 'multiselectable'
+ | 'readonly'
+ | 'required'
+ | 'selected';
+ const booleanProperties: BooleanProperty[] = [
+ 'disabled',
+ 'expanded',
+ 'focused',
+ 'modal',
+ 'multiline',
+ 'multiselectable',
+ 'readonly',
+ 'required',
+ 'selected',
+ ];
+ const getBooleanPropertyValue = (key: BooleanProperty): boolean => {
+ return properties.get(key) as boolean;
+ };
+
+ for (const booleanProperty of booleanProperties) {
+ // RootWebArea's treat focus differently than other nodes. They report whether
+ // their frame has focus, not whether focus is specifically on the root
+ // node.
+ if (booleanProperty === 'focused' && this.#role === 'RootWebArea') {
+ continue;
+ }
+ const value = getBooleanPropertyValue(booleanProperty);
+ if (!value) {
+ continue;
+ }
+ node[booleanProperty] = getBooleanPropertyValue(booleanProperty);
+ }
+
+ type TristateProperty = 'checked' | 'pressed';
+ const tristateProperties: TristateProperty[] = ['checked', 'pressed'];
+ for (const tristateProperty of tristateProperties) {
+ if (!properties.has(tristateProperty)) {
+ continue;
+ }
+ const value = properties.get(tristateProperty);
+ node[tristateProperty] =
+ value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
+ }
+
+ type NumbericalProperty = 'level' | 'valuemax' | 'valuemin';
+ const numericalProperties: NumbericalProperty[] = [
+ 'level',
+ 'valuemax',
+ 'valuemin',
+ ];
+ const getNumericalPropertyValue = (key: NumbericalProperty): number => {
+ return properties.get(key) as number;
+ };
+ for (const numericalProperty of numericalProperties) {
+ if (!properties.has(numericalProperty)) {
+ continue;
+ }
+ node[numericalProperty] = getNumericalPropertyValue(numericalProperty);
+ }
+
+ type TokenProperty =
+ | 'autocomplete'
+ | 'haspopup'
+ | 'invalid'
+ | 'orientation';
+ const tokenProperties: TokenProperty[] = [
+ 'autocomplete',
+ 'haspopup',
+ 'invalid',
+ 'orientation',
+ ];
+ const getTokenPropertyValue = (key: TokenProperty): string => {
+ return properties.get(key) as string;
+ };
+ for (const tokenProperty of tokenProperties) {
+ const value = getTokenPropertyValue(tokenProperty);
+ if (!value || value === 'false') {
+ continue;
+ }
+ node[tokenProperty] = getTokenPropertyValue(tokenProperty);
+ }
+ return node;
+ }
+
+ public static createTree(payloads: Protocol.Accessibility.AXNode[]): AXNode {
+ const nodeById = new Map<string, AXNode>();
+ for (const payload of payloads) {
+ nodeById.set(payload.nodeId, new AXNode(payload));
+ }
+ for (const node of nodeById.values()) {
+ for (const childId of node.payload.childIds || []) {
+ const child = nodeById.get(childId);
+ if (child) {
+ node.children.push(child);
+ }
+ }
+ }
+ return nodeById.values().next().value;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/AriaQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/AriaQueryHandler.ts
new file mode 100644
index 0000000000..2286723758
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/AriaQueryHandler.ts
@@ -0,0 +1,120 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {ElementHandle} from '../api/ElementHandle.js';
+import {QueryHandler, type QuerySelector} from '../common/QueryHandler.js';
+import type {AwaitableIterable} from '../common/types.js';
+import {assert} from '../util/assert.js';
+import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
+
+const NON_ELEMENT_NODE_ROLES = new Set(['StaticText', 'InlineTextBox']);
+
+const queryAXTree = async (
+ client: CDPSession,
+ element: ElementHandle<Node>,
+ accessibleName?: string,
+ role?: string
+): Promise<Protocol.Accessibility.AXNode[]> => {
+ const {nodes} = await client.send('Accessibility.queryAXTree', {
+ objectId: element.id,
+ accessibleName,
+ role,
+ });
+ return nodes.filter((node: Protocol.Accessibility.AXNode) => {
+ return !node.role || !NON_ELEMENT_NODE_ROLES.has(node.role.value);
+ });
+};
+
+interface ARIASelector {
+ name?: string;
+ role?: string;
+}
+
+const isKnownAttribute = (
+ attribute: string
+): attribute is keyof ARIASelector => {
+ return ['name', 'role'].includes(attribute);
+};
+
+const normalizeValue = (value: string): string => {
+ return value.replace(/ +/g, ' ').trim();
+};
+
+/**
+ * The selectors consist of an accessible name to query for and optionally
+ * further aria attributes on the form `[<attribute>=<value>]`.
+ * Currently, we only support the `name` and `role` attribute.
+ * The following examples showcase how the syntax works wrt. querying:
+ *
+ * - 'title[role="heading"]' queries for elements with name 'title' and role 'heading'.
+ * - '[role="image"]' queries for elements with role 'image' and any name.
+ * - 'label' queries for elements with name 'label' and any role.
+ * - '[name=""][role="button"]' queries for elements with no name and role 'button'.
+ */
+const ATTRIBUTE_REGEXP =
+ /\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/g;
+const parseARIASelector = (selector: string): ARIASelector => {
+ const queryOptions: ARIASelector = {};
+ const defaultName = selector.replace(
+ ATTRIBUTE_REGEXP,
+ (_, attribute, __, value) => {
+ attribute = attribute.trim();
+ assert(
+ isKnownAttribute(attribute),
+ `Unknown aria attribute "${attribute}" in selector`
+ );
+ queryOptions[attribute] = normalizeValue(value);
+ return '';
+ }
+ );
+ if (defaultName && !queryOptions.name) {
+ queryOptions.name = normalizeValue(defaultName);
+ }
+ return queryOptions;
+};
+
+/**
+ * @internal
+ */
+export class ARIAQueryHandler extends QueryHandler {
+ static override querySelector: QuerySelector = async (
+ node,
+ selector,
+ {ariaQuerySelector}
+ ) => {
+ return await ariaQuerySelector(node, selector);
+ };
+
+ static override async *queryAll(
+ element: ElementHandle<Node>,
+ selector: string
+ ): AwaitableIterable<ElementHandle<Node>> {
+ const {name, role} = parseARIASelector(selector);
+ const results = await queryAXTree(
+ element.realm.environment.client,
+ element,
+ name,
+ role
+ );
+ yield* AsyncIterableUtil.map(results, node => {
+ return element.realm.adoptBackendNode(node.backendDOMNodeId) as Promise<
+ ElementHandle<Node>
+ >;
+ });
+ }
+
+ static override queryOne = async (
+ element: ElementHandle<Node>,
+ selector: string
+ ): Promise<ElementHandle<Node> | null> => {
+ return (
+ (await AsyncIterableUtil.first(this.queryAll(element, selector))) ?? null
+ );
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts
new file mode 100644
index 0000000000..7a6a6f8582
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Binding.ts
@@ -0,0 +1,118 @@
+import {JSHandle} from '../api/JSHandle.js';
+import {debugError} from '../common/util.js';
+import {DisposableStack} from '../util/disposable.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import type {ExecutionContext} from './ExecutionContext.js';
+
+/**
+ * @internal
+ */
+export class Binding {
+ #name: string;
+ #fn: (...args: unknown[]) => unknown;
+ constructor(name: string, fn: (...args: unknown[]) => unknown) {
+ this.#name = name;
+ this.#fn = fn;
+ }
+
+ get name(): string {
+ return this.#name;
+ }
+
+ /**
+ * @param context - Context to run the binding in; the context should have
+ * the binding added to it beforehand.
+ * @param id - ID of the call. This should come from the CDP
+ * `onBindingCalled` response.
+ * @param args - Plain arguments from CDP.
+ */
+ async run(
+ context: ExecutionContext,
+ id: number,
+ args: unknown[],
+ isTrivial: boolean
+ ): Promise<void> {
+ const stack = new DisposableStack();
+ try {
+ if (!isTrivial) {
+ // Getting non-trivial arguments.
+ using handles = await context.evaluateHandle(
+ (name, seq) => {
+ // @ts-expect-error Code is evaluated in a different context.
+ return globalThis[name].args.get(seq);
+ },
+ this.#name,
+ id
+ );
+ const properties = await handles.getProperties();
+ for (const [index, handle] of properties) {
+ // This is not straight-forward since some arguments can stringify, but
+ // aren't plain objects so add subtypes when the use-case arises.
+ if (index in args) {
+ switch (handle.remoteObject().subtype) {
+ case 'node':
+ args[+index] = handle;
+ break;
+ default:
+ stack.use(handle);
+ }
+ } else {
+ stack.use(handle);
+ }
+ }
+ }
+
+ await context.evaluate(
+ (name, seq, result) => {
+ // @ts-expect-error Code is evaluated in a different context.
+ const callbacks = globalThis[name].callbacks;
+ callbacks.get(seq).resolve(result);
+ callbacks.delete(seq);
+ },
+ this.#name,
+ id,
+ await this.#fn(...args)
+ );
+
+ for (const arg of args) {
+ if (arg instanceof JSHandle) {
+ stack.use(arg);
+ }
+ }
+ } catch (error) {
+ if (isErrorLike(error)) {
+ await context
+ .evaluate(
+ (name, seq, message, stack) => {
+ const error = new Error(message);
+ error.stack = stack;
+ // @ts-expect-error Code is evaluated in a different context.
+ const callbacks = globalThis[name].callbacks;
+ callbacks.get(seq).reject(error);
+ callbacks.delete(seq);
+ },
+ this.#name,
+ id,
+ error.message,
+ error.stack
+ )
+ .catch(debugError);
+ } else {
+ await context
+ .evaluate(
+ (name, seq, error) => {
+ // @ts-expect-error Code is evaluated in a different context.
+ const callbacks = globalThis[name].callbacks;
+ callbacks.get(seq).reject(error);
+ callbacks.delete(seq);
+ },
+ this.#name,
+ id,
+ error
+ )
+ .catch(debugError);
+ }
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts
new file mode 100644
index 0000000000..7698acd164
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Browser.ts
@@ -0,0 +1,523 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ChildProcess} from 'child_process';
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {DebugInfo} from '../api/Browser.js';
+import {
+ Browser as BrowserBase,
+ BrowserEvent,
+ WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
+ type BrowserCloseCallback,
+ type BrowserContextOptions,
+ type IsPageTargetCallback,
+ type Permission,
+ type TargetFilterCallback,
+ type WaitForTargetOptions,
+} from '../api/Browser.js';
+import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
+import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
+import type {Page} from '../api/Page.js';
+import type {Target} from '../api/Target.js';
+import type {Viewport} from '../common/Viewport.js';
+import {assert} from '../util/assert.js';
+
+import {ChromeTargetManager} from './ChromeTargetManager.js';
+import type {Connection} from './Connection.js';
+import {FirefoxTargetManager} from './FirefoxTargetManager.js';
+import {
+ DevToolsTarget,
+ InitializationStatus,
+ OtherTarget,
+ PageTarget,
+ WorkerTarget,
+ type CdpTarget,
+} from './Target.js';
+import {TargetManagerEvent, type TargetManager} from './TargetManager.js';
+
+/**
+ * @internal
+ */
+export class CdpBrowser extends BrowserBase {
+ readonly protocol = 'cdp';
+
+ static async _create(
+ product: 'firefox' | 'chrome' | undefined,
+ connection: Connection,
+ contextIds: string[],
+ ignoreHTTPSErrors: boolean,
+ defaultViewport?: Viewport | null,
+ process?: ChildProcess,
+ closeCallback?: BrowserCloseCallback,
+ targetFilterCallback?: TargetFilterCallback,
+ isPageTargetCallback?: IsPageTargetCallback,
+ waitForInitiallyDiscoveredTargets = true
+ ): Promise<CdpBrowser> {
+ const browser = new CdpBrowser(
+ product,
+ connection,
+ contextIds,
+ ignoreHTTPSErrors,
+ defaultViewport,
+ process,
+ closeCallback,
+ targetFilterCallback,
+ isPageTargetCallback,
+ waitForInitiallyDiscoveredTargets
+ );
+ await browser._attach();
+ return browser;
+ }
+ #ignoreHTTPSErrors: boolean;
+ #defaultViewport?: Viewport | null;
+ #process?: ChildProcess;
+ #connection: Connection;
+ #closeCallback: BrowserCloseCallback;
+ #targetFilterCallback: TargetFilterCallback;
+ #isPageTargetCallback!: IsPageTargetCallback;
+ #defaultContext: CdpBrowserContext;
+ #contexts = new Map<string, CdpBrowserContext>();
+ #targetManager: TargetManager;
+
+ constructor(
+ product: 'chrome' | 'firefox' | undefined,
+ connection: Connection,
+ contextIds: string[],
+ ignoreHTTPSErrors: boolean,
+ defaultViewport?: Viewport | null,
+ process?: ChildProcess,
+ closeCallback?: BrowserCloseCallback,
+ targetFilterCallback?: TargetFilterCallback,
+ isPageTargetCallback?: IsPageTargetCallback,
+ waitForInitiallyDiscoveredTargets = true
+ ) {
+ super();
+ product = product || 'chrome';
+ this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
+ this.#defaultViewport = defaultViewport;
+ this.#process = process;
+ this.#connection = connection;
+ this.#closeCallback = closeCallback || function (): void {};
+ this.#targetFilterCallback =
+ targetFilterCallback ||
+ ((): boolean => {
+ return true;
+ });
+ this.#setIsPageTargetCallback(isPageTargetCallback);
+ if (product === 'firefox') {
+ this.#targetManager = new FirefoxTargetManager(
+ connection,
+ this.#createTarget,
+ this.#targetFilterCallback
+ );
+ } else {
+ this.#targetManager = new ChromeTargetManager(
+ connection,
+ this.#createTarget,
+ this.#targetFilterCallback,
+ waitForInitiallyDiscoveredTargets
+ );
+ }
+ this.#defaultContext = new CdpBrowserContext(this.#connection, this);
+ for (const contextId of contextIds) {
+ this.#contexts.set(
+ contextId,
+ new CdpBrowserContext(this.#connection, this, contextId)
+ );
+ }
+ }
+
+ #emitDisconnected = () => {
+ this.emit(BrowserEvent.Disconnected, undefined);
+ };
+
+ async _attach(): Promise<void> {
+ this.#connection.on(CDPSessionEvent.Disconnected, this.#emitDisconnected);
+ this.#targetManager.on(
+ TargetManagerEvent.TargetAvailable,
+ this.#onAttachedToTarget
+ );
+ this.#targetManager.on(
+ TargetManagerEvent.TargetGone,
+ this.#onDetachedFromTarget
+ );
+ this.#targetManager.on(
+ TargetManagerEvent.TargetChanged,
+ this.#onTargetChanged
+ );
+ this.#targetManager.on(
+ TargetManagerEvent.TargetDiscovered,
+ this.#onTargetDiscovered
+ );
+ await this.#targetManager.initialize();
+ }
+
+ _detach(): void {
+ this.#connection.off(CDPSessionEvent.Disconnected, this.#emitDisconnected);
+ this.#targetManager.off(
+ TargetManagerEvent.TargetAvailable,
+ this.#onAttachedToTarget
+ );
+ this.#targetManager.off(
+ TargetManagerEvent.TargetGone,
+ this.#onDetachedFromTarget
+ );
+ this.#targetManager.off(
+ TargetManagerEvent.TargetChanged,
+ this.#onTargetChanged
+ );
+ this.#targetManager.off(
+ TargetManagerEvent.TargetDiscovered,
+ this.#onTargetDiscovered
+ );
+ }
+
+ override process(): ChildProcess | null {
+ return this.#process ?? null;
+ }
+
+ _targetManager(): TargetManager {
+ return this.#targetManager;
+ }
+
+ #setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
+ this.#isPageTargetCallback =
+ isPageTargetCallback ||
+ ((target: Target): boolean => {
+ return (
+ target.type() === 'page' ||
+ target.type() === 'background_page' ||
+ target.type() === 'webview'
+ );
+ });
+ }
+
+ _getIsPageTargetCallback(): IsPageTargetCallback | undefined {
+ return this.#isPageTargetCallback;
+ }
+
+ override async createIncognitoBrowserContext(
+ options: BrowserContextOptions = {}
+ ): Promise<CdpBrowserContext> {
+ const {proxyServer, proxyBypassList} = options;
+
+ const {browserContextId} = await this.#connection.send(
+ 'Target.createBrowserContext',
+ {
+ proxyServer,
+ proxyBypassList: proxyBypassList && proxyBypassList.join(','),
+ }
+ );
+ const context = new CdpBrowserContext(
+ this.#connection,
+ this,
+ browserContextId
+ );
+ this.#contexts.set(browserContextId, context);
+ return context;
+ }
+
+ override browserContexts(): CdpBrowserContext[] {
+ return [this.#defaultContext, ...Array.from(this.#contexts.values())];
+ }
+
+ override defaultBrowserContext(): CdpBrowserContext {
+ return this.#defaultContext;
+ }
+
+ async _disposeContext(contextId?: string): Promise<void> {
+ if (!contextId) {
+ return;
+ }
+ await this.#connection.send('Target.disposeBrowserContext', {
+ browserContextId: contextId,
+ });
+ this.#contexts.delete(contextId);
+ }
+
+ #createTarget = (
+ targetInfo: Protocol.Target.TargetInfo,
+ session?: CDPSession
+ ) => {
+ const {browserContextId} = targetInfo;
+ const context =
+ browserContextId && this.#contexts.has(browserContextId)
+ ? this.#contexts.get(browserContextId)
+ : this.#defaultContext;
+
+ if (!context) {
+ throw new Error('Missing browser context');
+ }
+
+ const createSession = (isAutoAttachEmulated: boolean) => {
+ return this.#connection._createSession(targetInfo, isAutoAttachEmulated);
+ };
+ const otherTarget = new OtherTarget(
+ targetInfo,
+ session,
+ context,
+ this.#targetManager,
+ createSession
+ );
+ if (targetInfo.url?.startsWith('devtools://')) {
+ return new DevToolsTarget(
+ targetInfo,
+ session,
+ context,
+ this.#targetManager,
+ createSession,
+ this.#ignoreHTTPSErrors,
+ this.#defaultViewport ?? null
+ );
+ }
+ if (this.#isPageTargetCallback(otherTarget)) {
+ return new PageTarget(
+ targetInfo,
+ session,
+ context,
+ this.#targetManager,
+ createSession,
+ this.#ignoreHTTPSErrors,
+ this.#defaultViewport ?? null
+ );
+ }
+ if (
+ targetInfo.type === 'service_worker' ||
+ targetInfo.type === 'shared_worker'
+ ) {
+ return new WorkerTarget(
+ targetInfo,
+ session,
+ context,
+ this.#targetManager,
+ createSession
+ );
+ }
+ return otherTarget;
+ };
+
+ #onAttachedToTarget = async (target: CdpTarget) => {
+ if (
+ target._isTargetExposed() &&
+ (await target._initializedDeferred.valueOrThrow()) ===
+ InitializationStatus.SUCCESS
+ ) {
+ this.emit(BrowserEvent.TargetCreated, target);
+ target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
+ }
+ };
+
+ #onDetachedFromTarget = async (target: CdpTarget): Promise<void> => {
+ target._initializedDeferred.resolve(InitializationStatus.ABORTED);
+ target._isClosedDeferred.resolve();
+ if (
+ target._isTargetExposed() &&
+ (await target._initializedDeferred.valueOrThrow()) ===
+ InitializationStatus.SUCCESS
+ ) {
+ this.emit(BrowserEvent.TargetDestroyed, target);
+ target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
+ }
+ };
+
+ #onTargetChanged = ({target}: {target: CdpTarget}): void => {
+ this.emit(BrowserEvent.TargetChanged, target);
+ target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
+ };
+
+ #onTargetDiscovered = (targetInfo: Protocol.Target.TargetInfo): void => {
+ this.emit(BrowserEvent.TargetDiscovered, targetInfo);
+ };
+
+ override wsEndpoint(): string {
+ return this.#connection.url();
+ }
+
+ override async newPage(): Promise<Page> {
+ return await this.#defaultContext.newPage();
+ }
+
+ async _createPageInContext(contextId?: string): Promise<Page> {
+ const {targetId} = await this.#connection.send('Target.createTarget', {
+ url: 'about:blank',
+ browserContextId: contextId || undefined,
+ });
+ const target = (await this.waitForTarget(t => {
+ return (t as CdpTarget)._targetId === targetId;
+ })) as CdpTarget;
+ if (!target) {
+ throw new Error(`Missing target for page (id = ${targetId})`);
+ }
+ const initialized =
+ (await target._initializedDeferred.valueOrThrow()) ===
+ InitializationStatus.SUCCESS;
+ if (!initialized) {
+ throw new Error(`Failed to create target for page (id = ${targetId})`);
+ }
+ const page = await target.page();
+ if (!page) {
+ throw new Error(
+ `Failed to create a page for context (id = ${contextId})`
+ );
+ }
+ return page;
+ }
+
+ override targets(): CdpTarget[] {
+ return Array.from(
+ this.#targetManager.getAvailableTargets().values()
+ ).filter(target => {
+ return (
+ target._isTargetExposed() &&
+ target._initializedDeferred.value() === InitializationStatus.SUCCESS
+ );
+ });
+ }
+
+ override target(): CdpTarget {
+ const browserTarget = this.targets().find(target => {
+ return target.type() === 'browser';
+ });
+ if (!browserTarget) {
+ throw new Error('Browser target is not found');
+ }
+ return browserTarget;
+ }
+
+ override async version(): Promise<string> {
+ const version = await this.#getVersion();
+ return version.product;
+ }
+
+ override async userAgent(): Promise<string> {
+ const version = await this.#getVersion();
+ return version.userAgent;
+ }
+
+ override async close(): Promise<void> {
+ await this.#closeCallback.call(null);
+ await this.disconnect();
+ }
+
+ override disconnect(): Promise<void> {
+ this.#targetManager.dispose();
+ this.#connection.dispose();
+ this._detach();
+ return Promise.resolve();
+ }
+
+ override get connected(): boolean {
+ return !this.#connection._closed;
+ }
+
+ #getVersion(): Promise<Protocol.Browser.GetVersionResponse> {
+ return this.#connection.send('Browser.getVersion');
+ }
+
+ override get debugInfo(): DebugInfo {
+ return {
+ pendingProtocolErrors: this.#connection.getPendingProtocolErrors(),
+ };
+ }
+}
+
+/**
+ * @internal
+ */
+export class CdpBrowserContext extends BrowserContext {
+ #connection: Connection;
+ #browser: CdpBrowser;
+ #id?: string;
+
+ constructor(connection: Connection, browser: CdpBrowser, contextId?: string) {
+ super();
+ this.#connection = connection;
+ this.#browser = browser;
+ this.#id = contextId;
+ }
+
+ override get id(): string | undefined {
+ return this.#id;
+ }
+
+ override targets(): CdpTarget[] {
+ return this.#browser.targets().filter(target => {
+ return target.browserContext() === this;
+ });
+ }
+
+ override waitForTarget(
+ predicate: (x: Target) => boolean | Promise<boolean>,
+ options: WaitForTargetOptions = {}
+ ): Promise<Target> {
+ return this.#browser.waitForTarget(target => {
+ return target.browserContext() === this && predicate(target);
+ }, options);
+ }
+
+ override async pages(): Promise<Page[]> {
+ const pages = await Promise.all(
+ this.targets()
+ .filter(target => {
+ return (
+ target.type() === 'page' ||
+ (target.type() === 'other' &&
+ this.#browser._getIsPageTargetCallback()?.(target))
+ );
+ })
+ .map(target => {
+ return target.page();
+ })
+ );
+ return pages.filter((page): page is Page => {
+ return !!page;
+ });
+ }
+
+ override isIncognito(): boolean {
+ return !!this.#id;
+ }
+
+ override async overridePermissions(
+ origin: string,
+ permissions: Permission[]
+ ): Promise<void> {
+ const protocolPermissions = permissions.map(permission => {
+ const protocolPermission =
+ WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
+ if (!protocolPermission) {
+ throw new Error('Unknown permission: ' + permission);
+ }
+ return protocolPermission;
+ });
+ await this.#connection.send('Browser.grantPermissions', {
+ origin,
+ browserContextId: this.#id || undefined,
+ permissions: protocolPermissions,
+ });
+ }
+
+ override async clearPermissionOverrides(): Promise<void> {
+ await this.#connection.send('Browser.resetPermissions', {
+ browserContextId: this.#id || undefined,
+ });
+ }
+
+ override newPage(): Promise<Page> {
+ return this.#browser._createPageInContext(this.#id);
+ }
+
+ override browser(): CdpBrowser {
+ return this.#browser;
+ }
+
+ override async close(): Promise<void> {
+ assert(this.#id, 'Non-incognito profiles cannot be closed!');
+ await this.#browser._disposeContext(this.#id);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/BrowserConnector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/BrowserConnector.ts
new file mode 100644
index 0000000000..ef4aebe747
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/BrowserConnector.ts
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ConnectionTransport} from '../common/ConnectionTransport.js';
+import type {
+ BrowserConnectOptions,
+ ConnectOptions,
+} from '../common/ConnectOptions.js';
+import {debugError, DEFAULT_VIEWPORT} from '../common/util.js';
+
+import {CdpBrowser} from './Browser.js';
+import {Connection} from './Connection.js';
+
+/**
+ * Users should never call this directly; it's called when calling
+ * `puppeteer.connect` with `protocol: 'cdp'`.
+ *
+ * @internal
+ */
+export async function _connectToCdpBrowser(
+ connectionTransport: ConnectionTransport,
+ url: string,
+ options: BrowserConnectOptions & ConnectOptions
+): Promise<CdpBrowser> {
+ const {
+ ignoreHTTPSErrors = false,
+ defaultViewport = DEFAULT_VIEWPORT,
+ targetFilter,
+ _isPageTarget: isPageTarget,
+ slowMo = 0,
+ protocolTimeout,
+ } = options;
+
+ const connection = new Connection(
+ url,
+ connectionTransport,
+ slowMo,
+ protocolTimeout
+ );
+
+ const version = await connection.send('Browser.getVersion');
+ const product = version.product.toLowerCase().includes('firefox')
+ ? 'firefox'
+ : 'chrome';
+
+ const {browserContextIds} = await connection.send(
+ 'Target.getBrowserContexts'
+ );
+ const browser = await CdpBrowser._create(
+ product || 'chrome',
+ connection,
+ browserContextIds,
+ ignoreHTTPSErrors,
+ defaultViewport,
+ undefined,
+ () => {
+ return connection.send('Browser.close').catch(debugError);
+ },
+ targetFilter,
+ isPageTarget
+ );
+ return browser;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/CDPSession.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/CDPSession.ts
new file mode 100644
index 0000000000..fe5faa5647
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/CDPSession.ts
@@ -0,0 +1,167 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
+
+import {
+ type CDPEvents,
+ CDPSession,
+ CDPSessionEvent,
+ type CommandOptions,
+} from '../api/CDPSession.js';
+import {CallbackRegistry} from '../common/CallbackRegistry.js';
+import {TargetCloseError} from '../common/Errors.js';
+import {assert} from '../util/assert.js';
+import {createProtocolErrorMessage} from '../util/ErrorLike.js';
+
+import type {Connection} from './Connection.js';
+import type {CdpTarget} from './Target.js';
+
+/**
+ * @internal
+ */
+
+export class CdpCDPSession extends CDPSession {
+ #sessionId: string;
+ #targetType: string;
+ #callbacks = new CallbackRegistry();
+ #connection?: Connection;
+ #parentSessionId?: string;
+ #target?: CdpTarget;
+
+ /**
+ * @internal
+ */
+ constructor(
+ connection: Connection,
+ targetType: string,
+ sessionId: string,
+ parentSessionId: string | undefined
+ ) {
+ super();
+ this.#connection = connection;
+ this.#targetType = targetType;
+ this.#sessionId = sessionId;
+ this.#parentSessionId = parentSessionId;
+ }
+
+ /**
+ * Sets the {@link CdpTarget} associated with the session instance.
+ *
+ * @internal
+ */
+ _setTarget(target: CdpTarget): void {
+ this.#target = target;
+ }
+
+ /**
+ * Gets the {@link CdpTarget} associated with the session instance.
+ *
+ * @internal
+ */
+ _target(): CdpTarget {
+ assert(this.#target, 'Target must exist');
+ return this.#target;
+ }
+
+ override connection(): Connection | undefined {
+ return this.#connection;
+ }
+
+ override parentSession(): CDPSession | undefined {
+ if (!this.#parentSessionId) {
+ // To make it work in Firefox that does not have parent (tab) sessions.
+ return this;
+ }
+ const parent = this.#connection?.session(this.#parentSessionId);
+ return parent ?? undefined;
+ }
+
+ override send<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ params?: ProtocolMapping.Commands[T]['paramsType'][0],
+ options?: CommandOptions
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ if (!this.#connection) {
+ return Promise.reject(
+ new TargetCloseError(
+ `Protocol error (${method}): Session closed. Most likely the ${this.#targetType} has been closed.`
+ )
+ );
+ }
+ return this.#connection._rawSend(
+ this.#callbacks,
+ method,
+ params,
+ this.#sessionId,
+ options
+ );
+ }
+
+ /**
+ * @internal
+ */
+ _onMessage(object: {
+ id?: number;
+ method: keyof CDPEvents;
+ params: CDPEvents[keyof CDPEvents];
+ error: {message: string; data: any; code: number};
+ result?: any;
+ }): void {
+ if (object.id) {
+ if (object.error) {
+ this.#callbacks.reject(
+ object.id,
+ createProtocolErrorMessage(object),
+ object.error.message
+ );
+ } else {
+ this.#callbacks.resolve(object.id, object.result);
+ }
+ } else {
+ assert(!object.id);
+ this.emit(object.method, object.params);
+ }
+ }
+
+ /**
+ * Detaches the cdpSession from the target. Once detached, the cdpSession object
+ * won't emit any events and can't be used to send messages.
+ */
+ override async detach(): Promise<void> {
+ if (!this.#connection) {
+ throw new Error(
+ `Session already detached. Most likely the ${this.#targetType} has been closed.`
+ );
+ }
+ await this.#connection.send('Target.detachFromTarget', {
+ sessionId: this.#sessionId,
+ });
+ }
+
+ /**
+ * @internal
+ */
+ _onClosed(): void {
+ this.#callbacks.clear();
+ this.#connection = undefined;
+ this.emit(CDPSessionEvent.Disconnected, undefined);
+ }
+
+ /**
+ * Returns the session's id.
+ */
+ override id(): string {
+ return this.#sessionId;
+ }
+
+ /**
+ * @internal
+ */
+ getPendingProtocolErrors(): Error[] {
+ return this.#callbacks.getPendingProtocolErrors();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ChromeTargetManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ChromeTargetManager.ts
new file mode 100644
index 0000000000..e87d71fff9
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ChromeTargetManager.ts
@@ -0,0 +1,417 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {TargetFilterCallback} from '../api/Browser.js';
+import {CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
+import {EventEmitter} from '../common/EventEmitter.js';
+import {debugError} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+
+import type {CdpCDPSession} from './CDPSession.js';
+import type {Connection} from './Connection.js';
+import {CdpTarget, InitializationStatus} from './Target.js';
+import {
+ type TargetFactory,
+ type TargetManager,
+ TargetManagerEvent,
+ type TargetManagerEvents,
+} from './TargetManager.js';
+
+function isPageTargetBecomingPrimary(
+ target: CdpTarget,
+ newTargetInfo: Protocol.Target.TargetInfo
+): boolean {
+ return Boolean(target._subtype()) && !newTargetInfo.subtype;
+}
+
+/**
+ * ChromeTargetManager uses the CDP's auto-attach mechanism to intercept
+ * new targets and allow the rest of Puppeteer to configure listeners while
+ * the target is paused.
+ *
+ * @internal
+ */
+export class ChromeTargetManager
+ extends EventEmitter<TargetManagerEvents>
+ implements TargetManager
+{
+ #connection: Connection;
+ /**
+ * Keeps track of the following events: 'Target.targetCreated',
+ * 'Target.targetDestroyed', 'Target.targetInfoChanged'.
+ *
+ * A target becomes discovered when 'Target.targetCreated' is received.
+ * A target is removed from this map once 'Target.targetDestroyed' is
+ * received.
+ *
+ * `targetFilterCallback` has no effect on this map.
+ */
+ #discoveredTargetsByTargetId = new Map<string, Protocol.Target.TargetInfo>();
+ /**
+ * A target is added to this map once ChromeTargetManager has created
+ * a Target and attached at least once to it.
+ */
+ #attachedTargetsByTargetId = new Map<string, CdpTarget>();
+ /**
+ * Tracks which sessions attach to which target.
+ */
+ #attachedTargetsBySessionId = new Map<string, CdpTarget>();
+ /**
+ * If a target was filtered out by `targetFilterCallback`, we still receive
+ * events about it from CDP, but we don't forward them to the rest of Puppeteer.
+ */
+ #ignoredTargets = new Set<string>();
+ #targetFilterCallback: TargetFilterCallback | undefined;
+ #targetFactory: TargetFactory;
+
+ #attachedToTargetListenersBySession = new WeakMap<
+ CDPSession | Connection,
+ (event: Protocol.Target.AttachedToTargetEvent) => void
+ >();
+ #detachedFromTargetListenersBySession = new WeakMap<
+ CDPSession | Connection,
+ (event: Protocol.Target.DetachedFromTargetEvent) => void
+ >();
+
+ #initializeDeferred = Deferred.create<void>();
+ #targetsIdsForInit = new Set<string>();
+ #waitForInitiallyDiscoveredTargets = true;
+
+ #discoveryFilter: Protocol.Target.FilterEntry[] = [{}];
+
+ constructor(
+ connection: Connection,
+ targetFactory: TargetFactory,
+ targetFilterCallback?: TargetFilterCallback,
+ waitForInitiallyDiscoveredTargets = true
+ ) {
+ super();
+ this.#connection = connection;
+ this.#targetFilterCallback = targetFilterCallback;
+ this.#targetFactory = targetFactory;
+ this.#waitForInitiallyDiscoveredTargets = waitForInitiallyDiscoveredTargets;
+
+ this.#connection.on('Target.targetCreated', this.#onTargetCreated);
+ this.#connection.on('Target.targetDestroyed', this.#onTargetDestroyed);
+ this.#connection.on('Target.targetInfoChanged', this.#onTargetInfoChanged);
+ this.#connection.on(
+ CDPSessionEvent.SessionDetached,
+ this.#onSessionDetached
+ );
+ this.#setupAttachmentListeners(this.#connection);
+ }
+
+ #storeExistingTargetsForInit = () => {
+ if (!this.#waitForInitiallyDiscoveredTargets) {
+ return;
+ }
+ for (const [
+ targetId,
+ targetInfo,
+ ] of this.#discoveredTargetsByTargetId.entries()) {
+ const targetForFilter = new CdpTarget(
+ targetInfo,
+ undefined,
+ undefined,
+ this,
+ undefined
+ );
+ if (
+ (!this.#targetFilterCallback ||
+ this.#targetFilterCallback(targetForFilter)) &&
+ targetInfo.type !== 'browser'
+ ) {
+ this.#targetsIdsForInit.add(targetId);
+ }
+ }
+ };
+
+ async initialize(): Promise<void> {
+ await this.#connection.send('Target.setDiscoverTargets', {
+ discover: true,
+ filter: this.#discoveryFilter,
+ });
+
+ this.#storeExistingTargetsForInit();
+
+ await this.#connection.send('Target.setAutoAttach', {
+ waitForDebuggerOnStart: true,
+ flatten: true,
+ autoAttach: true,
+ filter: [
+ {
+ type: 'page',
+ exclude: true,
+ },
+ ...this.#discoveryFilter,
+ ],
+ });
+ this.#finishInitializationIfReady();
+ await this.#initializeDeferred.valueOrThrow();
+ }
+
+ dispose(): void {
+ this.#connection.off('Target.targetCreated', this.#onTargetCreated);
+ this.#connection.off('Target.targetDestroyed', this.#onTargetDestroyed);
+ this.#connection.off('Target.targetInfoChanged', this.#onTargetInfoChanged);
+ this.#connection.off(
+ CDPSessionEvent.SessionDetached,
+ this.#onSessionDetached
+ );
+
+ this.#removeAttachmentListeners(this.#connection);
+ }
+
+ getAvailableTargets(): ReadonlyMap<string, CdpTarget> {
+ return this.#attachedTargetsByTargetId;
+ }
+
+ #setupAttachmentListeners(session: CDPSession | Connection): void {
+ const listener = (event: Protocol.Target.AttachedToTargetEvent) => {
+ void this.#onAttachedToTarget(session, event);
+ };
+ assert(!this.#attachedToTargetListenersBySession.has(session));
+ this.#attachedToTargetListenersBySession.set(session, listener);
+ session.on('Target.attachedToTarget', listener);
+
+ const detachedListener = (
+ event: Protocol.Target.DetachedFromTargetEvent
+ ) => {
+ return this.#onDetachedFromTarget(session, event);
+ };
+ assert(!this.#detachedFromTargetListenersBySession.has(session));
+ this.#detachedFromTargetListenersBySession.set(session, detachedListener);
+ session.on('Target.detachedFromTarget', detachedListener);
+ }
+
+ #removeAttachmentListeners(session: CDPSession | Connection): void {
+ const listener = this.#attachedToTargetListenersBySession.get(session);
+ if (listener) {
+ session.off('Target.attachedToTarget', listener);
+ this.#attachedToTargetListenersBySession.delete(session);
+ }
+
+ if (this.#detachedFromTargetListenersBySession.has(session)) {
+ session.off(
+ 'Target.detachedFromTarget',
+ this.#detachedFromTargetListenersBySession.get(session)!
+ );
+ this.#detachedFromTargetListenersBySession.delete(session);
+ }
+ }
+
+ #onSessionDetached = (session: CDPSession) => {
+ this.#removeAttachmentListeners(session);
+ };
+
+ #onTargetCreated = async (event: Protocol.Target.TargetCreatedEvent) => {
+ this.#discoveredTargetsByTargetId.set(
+ event.targetInfo.targetId,
+ event.targetInfo
+ );
+
+ this.emit(TargetManagerEvent.TargetDiscovered, event.targetInfo);
+
+ // The connection is already attached to the browser target implicitly,
+ // therefore, no new CDPSession is created and we have special handling
+ // here.
+ if (event.targetInfo.type === 'browser' && event.targetInfo.attached) {
+ if (this.#attachedTargetsByTargetId.has(event.targetInfo.targetId)) {
+ return;
+ }
+ const target = this.#targetFactory(event.targetInfo, undefined);
+ target._initialize();
+ this.#attachedTargetsByTargetId.set(event.targetInfo.targetId, target);
+ }
+ };
+
+ #onTargetDestroyed = (event: Protocol.Target.TargetDestroyedEvent) => {
+ const targetInfo = this.#discoveredTargetsByTargetId.get(event.targetId);
+ this.#discoveredTargetsByTargetId.delete(event.targetId);
+ this.#finishInitializationIfReady(event.targetId);
+ if (
+ targetInfo?.type === 'service_worker' &&
+ this.#attachedTargetsByTargetId.has(event.targetId)
+ ) {
+ // Special case for service workers: report TargetGone event when
+ // the worker is destroyed.
+ const target = this.#attachedTargetsByTargetId.get(event.targetId);
+ if (target) {
+ this.emit(TargetManagerEvent.TargetGone, target);
+ this.#attachedTargetsByTargetId.delete(event.targetId);
+ }
+ }
+ };
+
+ #onTargetInfoChanged = (event: Protocol.Target.TargetInfoChangedEvent) => {
+ this.#discoveredTargetsByTargetId.set(
+ event.targetInfo.targetId,
+ event.targetInfo
+ );
+
+ if (
+ this.#ignoredTargets.has(event.targetInfo.targetId) ||
+ !this.#attachedTargetsByTargetId.has(event.targetInfo.targetId) ||
+ !event.targetInfo.attached
+ ) {
+ return;
+ }
+
+ const target = this.#attachedTargetsByTargetId.get(
+ event.targetInfo.targetId
+ );
+ if (!target) {
+ return;
+ }
+ const previousURL = target.url();
+ const wasInitialized =
+ target._initializedDeferred.value() === InitializationStatus.SUCCESS;
+
+ if (isPageTargetBecomingPrimary(target, event.targetInfo)) {
+ const session = target?._session();
+ assert(
+ session,
+ 'Target that is being activated is missing a CDPSession.'
+ );
+ session.parentSession()?.emit(CDPSessionEvent.Swapped, session);
+ }
+
+ target._targetInfoChanged(event.targetInfo);
+
+ if (wasInitialized && previousURL !== target.url()) {
+ this.emit(TargetManagerEvent.TargetChanged, {
+ target,
+ wasInitialized,
+ previousURL,
+ });
+ }
+ };
+
+ #onAttachedToTarget = async (
+ parentSession: Connection | CDPSession,
+ event: Protocol.Target.AttachedToTargetEvent
+ ) => {
+ const targetInfo = event.targetInfo;
+ const session = this.#connection.session(event.sessionId);
+ if (!session) {
+ throw new Error(`Session ${event.sessionId} was not created.`);
+ }
+
+ const silentDetach = async () => {
+ await session.send('Runtime.runIfWaitingForDebugger').catch(debugError);
+ // We don't use `session.detach()` because that dispatches all commands on
+ // the connection instead of the parent session.
+ await parentSession
+ .send('Target.detachFromTarget', {
+ sessionId: session.id(),
+ })
+ .catch(debugError);
+ };
+
+ if (!this.#connection.isAutoAttached(targetInfo.targetId)) {
+ return;
+ }
+
+ // Special case for service workers: being attached to service workers will
+ // prevent them from ever being destroyed. Therefore, we silently detach
+ // from service workers unless the connection was manually created via
+ // `page.worker()`. To determine this, we use
+ // `this.#connection.isAutoAttached(targetInfo.targetId)`. In the future, we
+ // should determine if a target is auto-attached or not with the help of
+ // CDP.
+ if (targetInfo.type === 'service_worker') {
+ this.#finishInitializationIfReady(targetInfo.targetId);
+ await silentDetach();
+ if (this.#attachedTargetsByTargetId.has(targetInfo.targetId)) {
+ return;
+ }
+ const target = this.#targetFactory(targetInfo);
+ target._initialize();
+ this.#attachedTargetsByTargetId.set(targetInfo.targetId, target);
+ this.emit(TargetManagerEvent.TargetAvailable, target);
+ return;
+ }
+
+ const isExistingTarget = this.#attachedTargetsByTargetId.has(
+ targetInfo.targetId
+ );
+
+ const target = isExistingTarget
+ ? this.#attachedTargetsByTargetId.get(targetInfo.targetId)!
+ : this.#targetFactory(
+ targetInfo,
+ session,
+ parentSession instanceof CDPSession ? parentSession : undefined
+ );
+
+ if (this.#targetFilterCallback && !this.#targetFilterCallback(target)) {
+ this.#ignoredTargets.add(targetInfo.targetId);
+ this.#finishInitializationIfReady(targetInfo.targetId);
+ await silentDetach();
+ return;
+ }
+
+ this.#setupAttachmentListeners(session);
+
+ if (isExistingTarget) {
+ (session as CdpCDPSession)._setTarget(target);
+ this.#attachedTargetsBySessionId.set(
+ session.id(),
+ this.#attachedTargetsByTargetId.get(targetInfo.targetId)!
+ );
+ } else {
+ target._initialize();
+ this.#attachedTargetsByTargetId.set(targetInfo.targetId, target);
+ this.#attachedTargetsBySessionId.set(session.id(), target);
+ }
+
+ parentSession.emit(CDPSessionEvent.Ready, session);
+
+ this.#targetsIdsForInit.delete(target._targetId);
+ if (!isExistingTarget) {
+ this.emit(TargetManagerEvent.TargetAvailable, target);
+ }
+ this.#finishInitializationIfReady();
+
+ // TODO: the browser might be shutting down here. What do we do with the
+ // error?
+ await Promise.all([
+ session.send('Target.setAutoAttach', {
+ waitForDebuggerOnStart: true,
+ flatten: true,
+ autoAttach: true,
+ filter: this.#discoveryFilter,
+ }),
+ session.send('Runtime.runIfWaitingForDebugger'),
+ ]).catch(debugError);
+ };
+
+ #finishInitializationIfReady(targetId?: string): void {
+ targetId !== undefined && this.#targetsIdsForInit.delete(targetId);
+ if (this.#targetsIdsForInit.size === 0) {
+ this.#initializeDeferred.resolve();
+ }
+ }
+
+ #onDetachedFromTarget = (
+ _parentSession: Connection | CDPSession,
+ event: Protocol.Target.DetachedFromTargetEvent
+ ) => {
+ const target = this.#attachedTargetsBySessionId.get(event.sessionId);
+
+ this.#attachedTargetsBySessionId.delete(event.sessionId);
+
+ if (!target) {
+ return;
+ }
+
+ this.#attachedTargetsByTargetId.delete(target._targetId);
+ this.emit(TargetManagerEvent.TargetGone, target);
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Connection.ts
new file mode 100644
index 0000000000..3c565341b3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Connection.ts
@@ -0,0 +1,273 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
+
+import type {CommandOptions} from '../api/CDPSession.js';
+import {
+ CDPSessionEvent,
+ type CDPSession,
+ type CDPSessionEvents,
+} from '../api/CDPSession.js';
+import {CallbackRegistry} from '../common/CallbackRegistry.js';
+import type {ConnectionTransport} from '../common/ConnectionTransport.js';
+import {debug} from '../common/Debug.js';
+import {TargetCloseError} from '../common/Errors.js';
+import {EventEmitter} from '../common/EventEmitter.js';
+import {createProtocolErrorMessage} from '../util/ErrorLike.js';
+
+import {CdpCDPSession} from './CDPSession.js';
+
+const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
+const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀');
+
+/**
+ * @public
+ */
+export type {ConnectionTransport, ProtocolMapping};
+
+/**
+ * @public
+ */
+export class Connection extends EventEmitter<CDPSessionEvents> {
+ #url: string;
+ #transport: ConnectionTransport;
+ #delay: number;
+ #timeout: number;
+ #sessions = new Map<string, CdpCDPSession>();
+ #closed = false;
+ #manuallyAttached = new Set<string>();
+ #callbacks = new CallbackRegistry();
+
+ constructor(
+ url: string,
+ transport: ConnectionTransport,
+ delay = 0,
+ timeout?: number
+ ) {
+ super();
+ this.#url = url;
+ this.#delay = delay;
+ this.#timeout = timeout ?? 180_000;
+
+ this.#transport = transport;
+ this.#transport.onmessage = this.onMessage.bind(this);
+ this.#transport.onclose = this.#onClose.bind(this);
+ }
+
+ static fromSession(session: CDPSession): Connection | undefined {
+ return session.connection();
+ }
+
+ get timeout(): number {
+ return this.#timeout;
+ }
+
+ /**
+ * @internal
+ */
+ get _closed(): boolean {
+ return this.#closed;
+ }
+
+ /**
+ * @internal
+ */
+ get _sessions(): Map<string, CDPSession> {
+ return this.#sessions;
+ }
+
+ /**
+ * @param sessionId - The session id
+ * @returns The current CDP session if it exists
+ */
+ session(sessionId: string): CDPSession | null {
+ return this.#sessions.get(sessionId) || null;
+ }
+
+ url(): string {
+ return this.#url;
+ }
+
+ send<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ params?: ProtocolMapping.Commands[T]['paramsType'][0],
+ options?: CommandOptions
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ // There is only ever 1 param arg passed, but the Protocol defines it as an
+ // array of 0 or 1 items See this comment:
+ // https://github.com/ChromeDevTools/devtools-protocol/pull/113#issuecomment-412603285
+ // which explains why the protocol defines the params this way for better
+ // type-inference.
+ // So now we check if there are any params or not and deal with them accordingly.
+ return this._rawSend(this.#callbacks, method, params, undefined, options);
+ }
+
+ /**
+ * @internal
+ */
+ _rawSend<T extends keyof ProtocolMapping.Commands>(
+ callbacks: CallbackRegistry,
+ method: T,
+ params: ProtocolMapping.Commands[T]['paramsType'][0],
+ sessionId?: string,
+ options?: CommandOptions
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ return callbacks.create(method, options?.timeout ?? this.#timeout, id => {
+ const stringifiedMessage = JSON.stringify({
+ method,
+ params,
+ id,
+ sessionId,
+ });
+ debugProtocolSend(stringifiedMessage);
+ this.#transport.send(stringifiedMessage);
+ }) as Promise<ProtocolMapping.Commands[T]['returnType']>;
+ }
+
+ /**
+ * @internal
+ */
+ async closeBrowser(): Promise<void> {
+ await this.send('Browser.close');
+ }
+
+ /**
+ * @internal
+ */
+ protected async onMessage(message: string): Promise<void> {
+ if (this.#delay) {
+ await new Promise(r => {
+ return setTimeout(r, this.#delay);
+ });
+ }
+ debugProtocolReceive(message);
+ const object = JSON.parse(message);
+ if (object.method === 'Target.attachedToTarget') {
+ const sessionId = object.params.sessionId;
+ const session = new CdpCDPSession(
+ this,
+ object.params.targetInfo.type,
+ sessionId,
+ object.sessionId
+ );
+ this.#sessions.set(sessionId, session);
+ this.emit(CDPSessionEvent.SessionAttached, session);
+ const parentSession = this.#sessions.get(object.sessionId);
+ if (parentSession) {
+ parentSession.emit(CDPSessionEvent.SessionAttached, session);
+ }
+ } else if (object.method === 'Target.detachedFromTarget') {
+ const session = this.#sessions.get(object.params.sessionId);
+ if (session) {
+ session._onClosed();
+ this.#sessions.delete(object.params.sessionId);
+ this.emit(CDPSessionEvent.SessionDetached, session);
+ const parentSession = this.#sessions.get(object.sessionId);
+ if (parentSession) {
+ parentSession.emit(CDPSessionEvent.SessionDetached, session);
+ }
+ }
+ }
+ if (object.sessionId) {
+ const session = this.#sessions.get(object.sessionId);
+ if (session) {
+ session._onMessage(object);
+ }
+ } else if (object.id) {
+ if (object.error) {
+ this.#callbacks.reject(
+ object.id,
+ createProtocolErrorMessage(object),
+ object.error.message
+ );
+ } else {
+ this.#callbacks.resolve(object.id, object.result);
+ }
+ } else {
+ this.emit(object.method, object.params);
+ }
+ }
+
+ #onClose(): void {
+ if (this.#closed) {
+ return;
+ }
+ this.#closed = true;
+ this.#transport.onmessage = undefined;
+ this.#transport.onclose = undefined;
+ this.#callbacks.clear();
+ for (const session of this.#sessions.values()) {
+ session._onClosed();
+ }
+ this.#sessions.clear();
+ this.emit(CDPSessionEvent.Disconnected, undefined);
+ }
+
+ dispose(): void {
+ this.#onClose();
+ this.#transport.close();
+ }
+
+ /**
+ * @internal
+ */
+ isAutoAttached(targetId: string): boolean {
+ return !this.#manuallyAttached.has(targetId);
+ }
+
+ /**
+ * @internal
+ */
+ async _createSession(
+ targetInfo: Protocol.Target.TargetInfo,
+ isAutoAttachEmulated = true
+ ): Promise<CDPSession> {
+ if (!isAutoAttachEmulated) {
+ this.#manuallyAttached.add(targetInfo.targetId);
+ }
+ const {sessionId} = await this.send('Target.attachToTarget', {
+ targetId: targetInfo.targetId,
+ flatten: true,
+ });
+ this.#manuallyAttached.delete(targetInfo.targetId);
+ const session = this.#sessions.get(sessionId);
+ if (!session) {
+ throw new Error('CDPSession creation failed.');
+ }
+ return session;
+ }
+
+ /**
+ * @param targetInfo - The target info
+ * @returns The CDP session that is created
+ */
+ async createSession(
+ targetInfo: Protocol.Target.TargetInfo
+ ): Promise<CDPSession> {
+ return await this._createSession(targetInfo, false);
+ }
+
+ /**
+ * @internal
+ */
+ getPendingProtocolErrors(): Error[] {
+ const result: Error[] = [];
+ result.push(...this.#callbacks.getPendingProtocolErrors());
+ for (const session of this.#sessions.values()) {
+ result.push(...session.getPendingProtocolErrors());
+ }
+ return result;
+ }
+}
+
+/**
+ * @internal
+ */
+export function isTargetClosedError(error: Error): boolean {
+ return error instanceof TargetCloseError;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Coverage.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Coverage.ts
new file mode 100644
index 0000000000..db995fb45b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Coverage.ts
@@ -0,0 +1,513 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import {EventSubscription} from '../common/EventEmitter.js';
+import {debugError, PuppeteerURL} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {DisposableStack} from '../util/disposable.js';
+
+/**
+ * The CoverageEntry class represents one entry of the coverage report.
+ * @public
+ */
+export interface CoverageEntry {
+ /**
+ * The URL of the style sheet or script.
+ */
+ url: string;
+ /**
+ * The content of the style sheet or script.
+ */
+ text: string;
+ /**
+ * The covered range as start and end positions.
+ */
+ ranges: Array<{start: number; end: number}>;
+}
+
+/**
+ * The CoverageEntry class for JavaScript
+ * @public
+ */
+export interface JSCoverageEntry extends CoverageEntry {
+ /**
+ * Raw V8 script coverage entry.
+ */
+ rawScriptCoverage?: Protocol.Profiler.ScriptCoverage;
+}
+
+/**
+ * Set of configurable options for JS coverage.
+ * @public
+ */
+export interface JSCoverageOptions {
+ /**
+ * Whether to reset coverage on every navigation.
+ */
+ resetOnNavigation?: boolean;
+ /**
+ * Whether anonymous scripts generated by the page should be reported.
+ */
+ reportAnonymousScripts?: boolean;
+ /**
+ * Whether the result includes raw V8 script coverage entries.
+ */
+ includeRawScriptCoverage?: boolean;
+ /**
+ * Whether to collect coverage information at the block level.
+ * If true, coverage will be collected at the block level (this is the default).
+ * If false, coverage will be collected at the function level.
+ */
+ useBlockCoverage?: boolean;
+}
+
+/**
+ * Set of configurable options for CSS coverage.
+ * @public
+ */
+export interface CSSCoverageOptions {
+ /**
+ * Whether to reset coverage on every navigation.
+ */
+ resetOnNavigation?: boolean;
+}
+
+/**
+ * The Coverage class provides methods to gather information about parts of
+ * JavaScript and CSS that were used by the page.
+ *
+ * @remarks
+ * To output coverage in a form consumable by {@link https://github.com/istanbuljs | Istanbul},
+ * see {@link https://github.com/istanbuljs/puppeteer-to-istanbul | puppeteer-to-istanbul}.
+ *
+ * @example
+ * An example of using JavaScript and CSS coverage to get percentage of initially
+ * executed code:
+ *
+ * ```ts
+ * // Enable both JavaScript and CSS coverage
+ * await Promise.all([
+ * page.coverage.startJSCoverage(),
+ * page.coverage.startCSSCoverage(),
+ * ]);
+ * // Navigate to page
+ * await page.goto('https://example.com');
+ * // Disable both JavaScript and CSS coverage
+ * const [jsCoverage, cssCoverage] = await Promise.all([
+ * page.coverage.stopJSCoverage(),
+ * page.coverage.stopCSSCoverage(),
+ * ]);
+ * let totalBytes = 0;
+ * let usedBytes = 0;
+ * const coverage = [...jsCoverage, ...cssCoverage];
+ * for (const entry of coverage) {
+ * totalBytes += entry.text.length;
+ * for (const range of entry.ranges) usedBytes += range.end - range.start - 1;
+ * }
+ * console.log(`Bytes used: ${(usedBytes / totalBytes) * 100}%`);
+ * ```
+ *
+ * @public
+ */
+export class Coverage {
+ #jsCoverage: JSCoverage;
+ #cssCoverage: CSSCoverage;
+
+ constructor(client: CDPSession) {
+ this.#jsCoverage = new JSCoverage(client);
+ this.#cssCoverage = new CSSCoverage(client);
+ }
+
+ /**
+ * @internal
+ */
+ updateClient(client: CDPSession): void {
+ this.#jsCoverage.updateClient(client);
+ this.#cssCoverage.updateClient(client);
+ }
+
+ /**
+ * @param options - Set of configurable options for coverage defaults to
+ * `resetOnNavigation : true, reportAnonymousScripts : false,`
+ * `includeRawScriptCoverage : false, useBlockCoverage : true`
+ * @returns Promise that resolves when coverage is started.
+ *
+ * @remarks
+ * Anonymous scripts are ones that don't have an associated url. These are
+ * scripts that are dynamically created on the page using `eval` or
+ * `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous
+ * scripts URL will start with `debugger://VM` (unless a magic //# sourceURL
+ * comment is present, in which case that will the be URL).
+ */
+ async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
+ return await this.#jsCoverage.start(options);
+ }
+
+ /**
+ * Promise that resolves to the array of coverage reports for
+ * all scripts.
+ *
+ * @remarks
+ * JavaScript Coverage doesn't include anonymous scripts by default.
+ * However, scripts with sourceURLs are reported.
+ */
+ async stopJSCoverage(): Promise<JSCoverageEntry[]> {
+ return await this.#jsCoverage.stop();
+ }
+
+ /**
+ * @param options - Set of configurable options for coverage, defaults to
+ * `resetOnNavigation : true`
+ * @returns Promise that resolves when coverage is started.
+ */
+ async startCSSCoverage(options: CSSCoverageOptions = {}): Promise<void> {
+ return await this.#cssCoverage.start(options);
+ }
+
+ /**
+ * Promise that resolves to the array of coverage reports
+ * for all stylesheets.
+ *
+ * @remarks
+ * CSS Coverage doesn't include dynamically injected style tags
+ * without sourceURLs.
+ */
+ async stopCSSCoverage(): Promise<CoverageEntry[]> {
+ return await this.#cssCoverage.stop();
+ }
+}
+
+/**
+ * @public
+ */
+export class JSCoverage {
+ #client: CDPSession;
+ #enabled = false;
+ #scriptURLs = new Map<string, string>();
+ #scriptSources = new Map<string, string>();
+ #subscriptions?: DisposableStack;
+ #resetOnNavigation = false;
+ #reportAnonymousScripts = false;
+ #includeRawScriptCoverage = false;
+
+ constructor(client: CDPSession) {
+ this.#client = client;
+ }
+
+ /**
+ * @internal
+ */
+ updateClient(client: CDPSession): void {
+ this.#client = client;
+ }
+
+ async start(
+ options: {
+ resetOnNavigation?: boolean;
+ reportAnonymousScripts?: boolean;
+ includeRawScriptCoverage?: boolean;
+ useBlockCoverage?: boolean;
+ } = {}
+ ): Promise<void> {
+ assert(!this.#enabled, 'JSCoverage is already enabled');
+ const {
+ resetOnNavigation = true,
+ reportAnonymousScripts = false,
+ includeRawScriptCoverage = false,
+ useBlockCoverage = true,
+ } = options;
+ this.#resetOnNavigation = resetOnNavigation;
+ this.#reportAnonymousScripts = reportAnonymousScripts;
+ this.#includeRawScriptCoverage = includeRawScriptCoverage;
+ this.#enabled = true;
+ this.#scriptURLs.clear();
+ this.#scriptSources.clear();
+ this.#subscriptions = new DisposableStack();
+ this.#subscriptions.use(
+ new EventSubscription(
+ this.#client,
+ 'Debugger.scriptParsed',
+ this.#onScriptParsed.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ this.#client,
+ 'Runtime.executionContextsCleared',
+ this.#onExecutionContextsCleared.bind(this)
+ )
+ );
+ await Promise.all([
+ this.#client.send('Profiler.enable'),
+ this.#client.send('Profiler.startPreciseCoverage', {
+ callCount: this.#includeRawScriptCoverage,
+ detailed: useBlockCoverage,
+ }),
+ this.#client.send('Debugger.enable'),
+ this.#client.send('Debugger.setSkipAllPauses', {skip: true}),
+ ]);
+ }
+
+ #onExecutionContextsCleared(): void {
+ if (!this.#resetOnNavigation) {
+ return;
+ }
+ this.#scriptURLs.clear();
+ this.#scriptSources.clear();
+ }
+
+ async #onScriptParsed(
+ event: Protocol.Debugger.ScriptParsedEvent
+ ): Promise<void> {
+ // Ignore puppeteer-injected scripts
+ if (PuppeteerURL.isPuppeteerURL(event.url)) {
+ return;
+ }
+ // Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
+ if (!event.url && !this.#reportAnonymousScripts) {
+ return;
+ }
+ try {
+ const response = await this.#client.send('Debugger.getScriptSource', {
+ scriptId: event.scriptId,
+ });
+ this.#scriptURLs.set(event.scriptId, event.url);
+ this.#scriptSources.set(event.scriptId, response.scriptSource);
+ } catch (error) {
+ // This might happen if the page has already navigated away.
+ debugError(error);
+ }
+ }
+
+ async stop(): Promise<JSCoverageEntry[]> {
+ assert(this.#enabled, 'JSCoverage is not enabled');
+ this.#enabled = false;
+
+ const result = await Promise.all([
+ this.#client.send('Profiler.takePreciseCoverage'),
+ this.#client.send('Profiler.stopPreciseCoverage'),
+ this.#client.send('Profiler.disable'),
+ this.#client.send('Debugger.disable'),
+ ]);
+
+ this.#subscriptions?.dispose();
+
+ const coverage = [];
+ const profileResponse = result[0];
+
+ for (const entry of profileResponse.result) {
+ let url = this.#scriptURLs.get(entry.scriptId);
+ if (!url && this.#reportAnonymousScripts) {
+ url = 'debugger://VM' + entry.scriptId;
+ }
+ const text = this.#scriptSources.get(entry.scriptId);
+ if (text === undefined || url === undefined) {
+ continue;
+ }
+ const flattenRanges = [];
+ for (const func of entry.functions) {
+ flattenRanges.push(...func.ranges);
+ }
+ const ranges = convertToDisjointRanges(flattenRanges);
+ if (!this.#includeRawScriptCoverage) {
+ coverage.push({url, ranges, text});
+ } else {
+ coverage.push({url, ranges, text, rawScriptCoverage: entry});
+ }
+ }
+ return coverage;
+ }
+}
+
+/**
+ * @public
+ */
+export class CSSCoverage {
+ #client: CDPSession;
+ #enabled = false;
+ #stylesheetURLs = new Map<string, string>();
+ #stylesheetSources = new Map<string, string>();
+ #eventListeners?: DisposableStack;
+ #resetOnNavigation = false;
+
+ constructor(client: CDPSession) {
+ this.#client = client;
+ }
+
+ /**
+ * @internal
+ */
+ updateClient(client: CDPSession): void {
+ this.#client = client;
+ }
+
+ async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> {
+ assert(!this.#enabled, 'CSSCoverage is already enabled');
+ const {resetOnNavigation = true} = options;
+ this.#resetOnNavigation = resetOnNavigation;
+ this.#enabled = true;
+ this.#stylesheetURLs.clear();
+ this.#stylesheetSources.clear();
+ this.#eventListeners = new DisposableStack();
+ this.#eventListeners.use(
+ new EventSubscription(
+ this.#client,
+ 'CSS.styleSheetAdded',
+ this.#onStyleSheet.bind(this)
+ )
+ );
+ this.#eventListeners.use(
+ new EventSubscription(
+ this.#client,
+ 'Runtime.executionContextsCleared',
+ this.#onExecutionContextsCleared.bind(this)
+ )
+ );
+ await Promise.all([
+ this.#client.send('DOM.enable'),
+ this.#client.send('CSS.enable'),
+ this.#client.send('CSS.startRuleUsageTracking'),
+ ]);
+ }
+
+ #onExecutionContextsCleared(): void {
+ if (!this.#resetOnNavigation) {
+ return;
+ }
+ this.#stylesheetURLs.clear();
+ this.#stylesheetSources.clear();
+ }
+
+ async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
+ const header = event.header;
+ // Ignore anonymous scripts
+ if (!header.sourceURL) {
+ return;
+ }
+ try {
+ const response = await this.#client.send('CSS.getStyleSheetText', {
+ styleSheetId: header.styleSheetId,
+ });
+ this.#stylesheetURLs.set(header.styleSheetId, header.sourceURL);
+ this.#stylesheetSources.set(header.styleSheetId, response.text);
+ } catch (error) {
+ // This might happen if the page has already navigated away.
+ debugError(error);
+ }
+ }
+
+ async stop(): Promise<CoverageEntry[]> {
+ assert(this.#enabled, 'CSSCoverage is not enabled');
+ this.#enabled = false;
+ const ruleTrackingResponse = await this.#client.send(
+ 'CSS.stopRuleUsageTracking'
+ );
+ await Promise.all([
+ this.#client.send('CSS.disable'),
+ this.#client.send('DOM.disable'),
+ ]);
+ this.#eventListeners?.dispose();
+
+ // aggregate by styleSheetId
+ const styleSheetIdToCoverage = new Map();
+ for (const entry of ruleTrackingResponse.ruleUsage) {
+ let ranges = styleSheetIdToCoverage.get(entry.styleSheetId);
+ if (!ranges) {
+ ranges = [];
+ styleSheetIdToCoverage.set(entry.styleSheetId, ranges);
+ }
+ ranges.push({
+ startOffset: entry.startOffset,
+ endOffset: entry.endOffset,
+ count: entry.used ? 1 : 0,
+ });
+ }
+
+ const coverage: CoverageEntry[] = [];
+ for (const styleSheetId of this.#stylesheetURLs.keys()) {
+ const url = this.#stylesheetURLs.get(styleSheetId);
+ assert(
+ typeof url !== 'undefined',
+ `Stylesheet URL is undefined (styleSheetId=${styleSheetId})`
+ );
+ const text = this.#stylesheetSources.get(styleSheetId);
+ assert(
+ typeof text !== 'undefined',
+ `Stylesheet text is undefined (styleSheetId=${styleSheetId})`
+ );
+ const ranges = convertToDisjointRanges(
+ styleSheetIdToCoverage.get(styleSheetId) || []
+ );
+ coverage.push({url, ranges, text});
+ }
+
+ return coverage;
+ }
+}
+
+function convertToDisjointRanges(
+ nestedRanges: Array<{startOffset: number; endOffset: number; count: number}>
+): Array<{start: number; end: number}> {
+ const points = [];
+ for (const range of nestedRanges) {
+ points.push({offset: range.startOffset, type: 0, range});
+ points.push({offset: range.endOffset, type: 1, range});
+ }
+ // Sort points to form a valid parenthesis sequence.
+ points.sort((a, b) => {
+ // Sort with increasing offsets.
+ if (a.offset !== b.offset) {
+ return a.offset - b.offset;
+ }
+ // All "end" points should go before "start" points.
+ if (a.type !== b.type) {
+ return b.type - a.type;
+ }
+ const aLength = a.range.endOffset - a.range.startOffset;
+ const bLength = b.range.endOffset - b.range.startOffset;
+ // For two "start" points, the one with longer range goes first.
+ if (a.type === 0) {
+ return bLength - aLength;
+ }
+ // For two "end" points, the one with shorter range goes first.
+ return aLength - bLength;
+ });
+
+ const hitCountStack = [];
+ const results: Array<{
+ start: number;
+ end: number;
+ }> = [];
+ let lastOffset = 0;
+ // Run scanning line to intersect all ranges.
+ for (const point of points) {
+ if (
+ hitCountStack.length &&
+ lastOffset < point.offset &&
+ hitCountStack[hitCountStack.length - 1]! > 0
+ ) {
+ const lastResult = results[results.length - 1];
+ if (lastResult && lastResult.end === lastOffset) {
+ lastResult.end = point.offset;
+ } else {
+ results.push({start: lastOffset, end: point.offset});
+ }
+ }
+ lastOffset = point.offset;
+ if (point.type === 0) {
+ hitCountStack.push(point.range.count);
+ } else {
+ hitCountStack.pop();
+ }
+ }
+ // Filter out empty ranges.
+ return results.filter(range => {
+ return range.end - range.start > 0;
+ });
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/DeviceRequestPrompt.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/DeviceRequestPrompt.test.ts
new file mode 100644
index 0000000000..7d75e97eaf
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/DeviceRequestPrompt.test.ts
@@ -0,0 +1,471 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import type {CDPSessionEvents} from '../api/CDPSession.js';
+import {TimeoutError} from '../common/Errors.js';
+import {EventEmitter} from '../common/EventEmitter.js';
+import {TimeoutSettings} from '../common/TimeoutSettings.js';
+
+import {
+ DeviceRequestPrompt,
+ DeviceRequestPromptDevice,
+ DeviceRequestPromptManager,
+} from './DeviceRequestPrompt.js';
+
+class MockCDPSession extends EventEmitter<CDPSessionEvents> {
+ async send(): Promise<any> {}
+ connection() {
+ return undefined;
+ }
+ async detach() {}
+ id() {
+ return '1';
+ }
+ parentSession() {
+ return undefined;
+ }
+}
+
+describe('DeviceRequestPrompt', function () {
+ describe('waitForDevicePrompt', function () {
+ it('should return prompt', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ const [prompt] = await Promise.all([
+ manager.waitForDevicePrompt(),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ })(),
+ ]);
+ expect(prompt).toBeTruthy();
+ });
+
+ it('should respect timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ await expect(
+ manager.waitForDevicePrompt({timeout: 1})
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should respect default timeout when there is no custom timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ timeoutSettings.setDefaultTimeout(1);
+ await expect(manager.waitForDevicePrompt()).rejects.toBeInstanceOf(
+ TimeoutError
+ );
+ });
+
+ it('should prioritize exact timeout over default timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ timeoutSettings.setDefaultTimeout(0);
+ await expect(
+ manager.waitForDevicePrompt({timeout: 1})
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should work with no timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ const [prompt] = await Promise.all([
+ manager.waitForDevicePrompt({timeout: 0}),
+ (async () => {
+ await new Promise(resolve => {
+ setTimeout(resolve, 50);
+ });
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ })(),
+ ]);
+ expect(prompt).toBeTruthy();
+ });
+
+ it('should return the same prompt when there are many watchdogs simultaneously', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ const [prompt1, prompt2] = await Promise.all([
+ manager.waitForDevicePrompt(),
+ manager.waitForDevicePrompt(),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ })(),
+ ]);
+ expect(prompt1 === prompt2).toBeTruthy();
+ });
+
+ it('should listen and shortcut when there are no watchdogs', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const manager = new DeviceRequestPromptManager(client, timeoutSettings);
+
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ expect(manager).toBeTruthy();
+ });
+ });
+
+ describe('DeviceRequestPrompt.devices', function () {
+ it('lists devices as they arrive', function () {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ expect(prompt.devices).toHaveLength(0);
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [{id: '00000000', name: 'Device 0'}],
+ });
+ expect(prompt.devices).toHaveLength(1);
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ expect(prompt.devices).toHaveLength(2);
+ expect(prompt.devices[0]).toBeInstanceOf(DeviceRequestPromptDevice);
+ expect(prompt.devices[1]).toBeInstanceOf(DeviceRequestPromptDevice);
+ });
+
+ it('does not list devices from events of another prompt', function () {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ expect(prompt.devices).toHaveLength(0);
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '88888888888888888888888888888888',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ expect(prompt.devices).toHaveLength(0);
+ });
+ });
+
+ describe('DeviceRequestPrompt.waitForDevice', function () {
+ it('should return first matching device', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [{id: '00000000', name: 'Device 0'}],
+ });
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ expect(device).toBeInstanceOf(DeviceRequestPromptDevice);
+ });
+
+ it('should return first matching device from already known devices', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+
+ const device = await prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ });
+ expect(device).toBeInstanceOf(DeviceRequestPromptDevice);
+ });
+
+ it('should return device in the devices list', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ expect(prompt.devices).toContain(device);
+ });
+
+ it('should respect timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ await expect(
+ prompt.waitForDevice(
+ ({name}) => {
+ return name.includes('Device');
+ },
+ {timeout: 1}
+ )
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should respect default timeout when there is no custom timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ timeoutSettings.setDefaultTimeout(1);
+ await expect(
+ prompt.waitForDevice(
+ ({name}) => {
+ return name.includes('Device');
+ },
+ {timeout: 1}
+ )
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should prioritize exact timeout over default timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ timeoutSettings.setDefaultTimeout(0);
+ await expect(
+ prompt.waitForDevice(
+ ({name}) => {
+ return name.includes('Device');
+ },
+ {timeout: 1}
+ )
+ ).rejects.toBeInstanceOf(TimeoutError);
+ });
+
+ it('should work with no timeout', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(
+ ({name}) => {
+ return name.includes('1');
+ },
+ {timeout: 0}
+ ),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [{id: '00000000', name: 'Device 0'}],
+ });
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ expect(device).toBeInstanceOf(DeviceRequestPromptDevice);
+ });
+
+ it('should return same device from multiple watchdogs', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device1, device2] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [{id: '00000000', name: 'Device 0'}],
+ });
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ expect(device1 === device2).toBeTruthy();
+ });
+ });
+
+ describe('DeviceRequestPrompt.select', function () {
+ it('should succeed with listed device', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ await prompt.select(device);
+ });
+
+ it('should error for device not listed in devices', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ await expect(
+ prompt.select(new DeviceRequestPromptDevice('11111111', 'Device 1'))
+ ).rejects.toThrowError('Cannot select unknown device!');
+ });
+
+ it('should fail when selecting prompt twice', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+
+ const [device] = await Promise.all([
+ prompt.waitForDevice(({name}) => {
+ return name.includes('1');
+ }),
+ (() => {
+ client.emit('DeviceAccess.deviceRequestPrompted', {
+ id: '00000000000000000000000000000000',
+ devices: [
+ {id: '00000000', name: 'Device 0'},
+ {id: '11111111', name: 'Device 1'},
+ ],
+ });
+ })(),
+ ]);
+ await prompt.select(device);
+ await expect(prompt.select(device)).rejects.toThrowError(
+ 'Cannot select DeviceRequestPrompt which is already handled!'
+ );
+ });
+ });
+
+ describe('DeviceRequestPrompt.cancel', function () {
+ it('should succeed on first call', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ await prompt.cancel();
+ });
+
+ it('should fail when canceling prompt twice', async () => {
+ const client = new MockCDPSession();
+ const timeoutSettings = new TimeoutSettings();
+ const prompt = new DeviceRequestPrompt(client, timeoutSettings, {
+ id: '00000000000000000000000000000000',
+ devices: [],
+ });
+ await prompt.cancel();
+ await expect(prompt.cancel()).rejects.toThrowError(
+ 'Cannot cancel DeviceRequestPrompt which is already handled!'
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/DeviceRequestPrompt.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/DeviceRequestPrompt.ts
new file mode 100644
index 0000000000..f5bd73bf72
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/DeviceRequestPrompt.ts
@@ -0,0 +1,280 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type Protocol from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {WaitTimeoutOptions} from '../api/Page.js';
+import type {TimeoutSettings} from '../common/TimeoutSettings.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+
+/**
+ * Device in a request prompt.
+ *
+ * @public
+ */
+export class DeviceRequestPromptDevice {
+ /**
+ * Device id during a prompt.
+ */
+ id: string;
+
+ /**
+ * Device name as it appears in a prompt.
+ */
+ name: string;
+
+ /**
+ * @internal
+ */
+ constructor(id: string, name: string) {
+ this.id = id;
+ this.name = name;
+ }
+}
+
+/**
+ * Device request prompts let you respond to the page requesting for a device
+ * through an API like WebBluetooth.
+ *
+ * @remarks
+ * `DeviceRequestPrompt` instances are returned via the
+ * {@link Page.waitForDevicePrompt} method.
+ *
+ * @example
+ *
+ * ```ts
+ * const [deviceRequest] = Promise.all([
+ * page.waitForDevicePrompt(),
+ * page.click('#connect-bluetooth'),
+ * ]);
+ * await devicePrompt.select(
+ * await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+ * );
+ * ```
+ *
+ * @public
+ */
+export class DeviceRequestPrompt {
+ #client: CDPSession | null;
+ #timeoutSettings: TimeoutSettings;
+ #id: string;
+ #handled = false;
+ #updateDevicesHandle = this.#updateDevices.bind(this);
+ #waitForDevicePromises = new Set<{
+ filter: (device: DeviceRequestPromptDevice) => boolean;
+ promise: Deferred<DeviceRequestPromptDevice>;
+ }>();
+
+ /**
+ * Current list of selectable devices.
+ */
+ devices: DeviceRequestPromptDevice[] = [];
+
+ /**
+ * @internal
+ */
+ constructor(
+ client: CDPSession,
+ timeoutSettings: TimeoutSettings,
+ firstEvent: Protocol.DeviceAccess.DeviceRequestPromptedEvent
+ ) {
+ this.#client = client;
+ this.#timeoutSettings = timeoutSettings;
+ this.#id = firstEvent.id;
+
+ this.#client.on(
+ 'DeviceAccess.deviceRequestPrompted',
+ this.#updateDevicesHandle
+ );
+ this.#client.on('Target.detachedFromTarget', () => {
+ this.#client = null;
+ });
+
+ this.#updateDevices(firstEvent);
+ }
+
+ #updateDevices(event: Protocol.DeviceAccess.DeviceRequestPromptedEvent) {
+ if (event.id !== this.#id) {
+ return;
+ }
+
+ for (const rawDevice of event.devices) {
+ if (
+ this.devices.some(device => {
+ return device.id === rawDevice.id;
+ })
+ ) {
+ continue;
+ }
+
+ const newDevice = new DeviceRequestPromptDevice(
+ rawDevice.id,
+ rawDevice.name
+ );
+ this.devices.push(newDevice);
+
+ for (const waitForDevicePromise of this.#waitForDevicePromises) {
+ if (waitForDevicePromise.filter(newDevice)) {
+ waitForDevicePromise.promise.resolve(newDevice);
+ }
+ }
+ }
+ }
+
+ /**
+ * Resolve to the first device in the prompt matching a filter.
+ */
+ async waitForDevice(
+ filter: (device: DeviceRequestPromptDevice) => boolean,
+ options: WaitTimeoutOptions = {}
+ ): Promise<DeviceRequestPromptDevice> {
+ for (const device of this.devices) {
+ if (filter(device)) {
+ return device;
+ }
+ }
+
+ const {timeout = this.#timeoutSettings.timeout()} = options;
+ const deferred = Deferred.create<DeviceRequestPromptDevice>({
+ message: `Waiting for \`DeviceRequestPromptDevice\` failed: ${timeout}ms exceeded`,
+ timeout,
+ });
+ const handle = {filter, promise: deferred};
+ this.#waitForDevicePromises.add(handle);
+ try {
+ return await deferred.valueOrThrow();
+ } finally {
+ this.#waitForDevicePromises.delete(handle);
+ }
+ }
+
+ /**
+ * Select a device in the prompt's list.
+ */
+ async select(device: DeviceRequestPromptDevice): Promise<void> {
+ assert(
+ this.#client !== null,
+ 'Cannot select device through detached session!'
+ );
+ assert(this.devices.includes(device), 'Cannot select unknown device!');
+ assert(
+ !this.#handled,
+ 'Cannot select DeviceRequestPrompt which is already handled!'
+ );
+ this.#client.off(
+ 'DeviceAccess.deviceRequestPrompted',
+ this.#updateDevicesHandle
+ );
+ this.#handled = true;
+ return await this.#client.send('DeviceAccess.selectPrompt', {
+ id: this.#id,
+ deviceId: device.id,
+ });
+ }
+
+ /**
+ * Cancel the prompt.
+ */
+ async cancel(): Promise<void> {
+ assert(
+ this.#client !== null,
+ 'Cannot cancel prompt through detached session!'
+ );
+ assert(
+ !this.#handled,
+ 'Cannot cancel DeviceRequestPrompt which is already handled!'
+ );
+ this.#client.off(
+ 'DeviceAccess.deviceRequestPrompted',
+ this.#updateDevicesHandle
+ );
+ this.#handled = true;
+ return await this.#client.send('DeviceAccess.cancelPrompt', {id: this.#id});
+ }
+}
+
+/**
+ * @internal
+ */
+export class DeviceRequestPromptManager {
+ #client: CDPSession | null;
+ #timeoutSettings: TimeoutSettings;
+ #deviceRequestPrompDeferreds = new Set<Deferred<DeviceRequestPrompt>>();
+
+ /**
+ * @internal
+ */
+ constructor(client: CDPSession, timeoutSettings: TimeoutSettings) {
+ this.#client = client;
+ this.#timeoutSettings = timeoutSettings;
+
+ this.#client.on('DeviceAccess.deviceRequestPrompted', event => {
+ this.#onDeviceRequestPrompted(event);
+ });
+ this.#client.on('Target.detachedFromTarget', () => {
+ this.#client = null;
+ });
+ }
+
+ /**
+ * Wait for device prompt created by an action like calling WebBluetooth's
+ * requestDevice.
+ */
+ async waitForDevicePrompt(
+ options: WaitTimeoutOptions = {}
+ ): Promise<DeviceRequestPrompt> {
+ assert(
+ this.#client !== null,
+ 'Cannot wait for device prompt through detached session!'
+ );
+ const needsEnable = this.#deviceRequestPrompDeferreds.size === 0;
+ let enablePromise: Promise<void> | undefined;
+ if (needsEnable) {
+ enablePromise = this.#client.send('DeviceAccess.enable');
+ }
+
+ const {timeout = this.#timeoutSettings.timeout()} = options;
+ const deferred = Deferred.create<DeviceRequestPrompt>({
+ message: `Waiting for \`DeviceRequestPrompt\` failed: ${timeout}ms exceeded`,
+ timeout,
+ });
+ this.#deviceRequestPrompDeferreds.add(deferred);
+
+ try {
+ const [result] = await Promise.all([
+ deferred.valueOrThrow(),
+ enablePromise,
+ ]);
+ return result;
+ } finally {
+ this.#deviceRequestPrompDeferreds.delete(deferred);
+ }
+ }
+
+ /**
+ * @internal
+ */
+ #onDeviceRequestPrompted(
+ event: Protocol.DeviceAccess.DeviceRequestPromptedEvent
+ ) {
+ if (!this.#deviceRequestPrompDeferreds.size) {
+ return;
+ }
+
+ assert(this.#client !== null);
+ const devicePrompt = new DeviceRequestPrompt(
+ this.#client,
+ this.#timeoutSettings,
+ event
+ );
+ for (const promise of this.#deviceRequestPrompDeferreds) {
+ promise.resolve(devicePrompt);
+ }
+ this.#deviceRequestPrompDeferreds.clear();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Dialog.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Dialog.ts
new file mode 100644
index 0000000000..fe8fffbcad
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Dialog.ts
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import {Dialog} from '../api/Dialog.js';
+
+/**
+ * @internal
+ */
+export class CdpDialog extends Dialog {
+ #client: CDPSession;
+
+ constructor(
+ client: CDPSession,
+ type: Protocol.Page.DialogType,
+ message: string,
+ defaultValue = ''
+ ) {
+ super(type, message, defaultValue);
+ this.#client = client;
+ }
+
+ override async handle(options: {
+ accept: boolean;
+ text?: string;
+ }): Promise<void> {
+ await this.#client.send('Page.handleJavaScriptDialog', {
+ accept: options.accept,
+ promptText: options.text,
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ElementHandle.ts
new file mode 100644
index 0000000000..a47d546a87
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ElementHandle.ts
@@ -0,0 +1,172 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type Path from 'path';
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import {ElementHandle, type AutofillData} from '../api/ElementHandle.js';
+import {debugError} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {throwIfDisposed} from '../util/decorators.js';
+
+import type {CdpFrame} from './Frame.js';
+import type {FrameManager} from './FrameManager.js';
+import type {IsolatedWorld} from './IsolatedWorld.js';
+import {CdpJSHandle} from './JSHandle.js';
+
+/**
+ * The CdpElementHandle extends ElementHandle now to keep compatibility
+ * with `instanceof` because of that we need to have methods for
+ * CdpJSHandle to in this implementation as well.
+ *
+ * @internal
+ */
+export class CdpElementHandle<
+ ElementType extends Node = Element,
+> extends ElementHandle<ElementType> {
+ protected declare readonly handle: CdpJSHandle<ElementType>;
+
+ constructor(
+ world: IsolatedWorld,
+ remoteObject: Protocol.Runtime.RemoteObject
+ ) {
+ super(new CdpJSHandle(world, remoteObject));
+ }
+
+ override get realm(): IsolatedWorld {
+ return this.handle.realm;
+ }
+
+ get client(): CDPSession {
+ return this.handle.client;
+ }
+
+ override remoteObject(): Protocol.Runtime.RemoteObject {
+ return this.handle.remoteObject();
+ }
+
+ get #frameManager(): FrameManager {
+ return this.frame._frameManager;
+ }
+
+ override get frame(): CdpFrame {
+ return this.realm.environment as CdpFrame;
+ }
+
+ override async contentFrame(
+ this: ElementHandle<HTMLIFrameElement>
+ ): Promise<CdpFrame>;
+
+ @throwIfDisposed()
+ override async contentFrame(): Promise<CdpFrame | null> {
+ const nodeInfo = await this.client.send('DOM.describeNode', {
+ objectId: this.id,
+ });
+ if (typeof nodeInfo.node.frameId !== 'string') {
+ return null;
+ }
+ return this.#frameManager.frame(nodeInfo.node.frameId);
+ }
+
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ override async scrollIntoView(
+ this: CdpElementHandle<Element>
+ ): Promise<void> {
+ await this.assertConnectedElement();
+ try {
+ await this.client.send('DOM.scrollIntoViewIfNeeded', {
+ objectId: this.id,
+ });
+ } catch (error) {
+ debugError(error);
+ // Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported
+ await super.scrollIntoView();
+ }
+ }
+
+ @throwIfDisposed()
+ @ElementHandle.bindIsolatedHandle
+ override async uploadFile(
+ this: CdpElementHandle<HTMLInputElement>,
+ ...filePaths: string[]
+ ): Promise<void> {
+ const isMultiple = await this.evaluate(element => {
+ return element.multiple;
+ });
+ assert(
+ filePaths.length <= 1 || isMultiple,
+ 'Multiple file uploads only work with <input type=file multiple>'
+ );
+
+ // Locate all files and confirm that they exist.
+ let path: typeof Path;
+ try {
+ path = await import('path');
+ } catch (error) {
+ if (error instanceof TypeError) {
+ throw new Error(
+ `JSHandle#uploadFile can only be used in Node-like environments.`
+ );
+ }
+ throw error;
+ }
+ const files = filePaths.map(filePath => {
+ if (path.win32.isAbsolute(filePath) || path.posix.isAbsolute(filePath)) {
+ return filePath;
+ } else {
+ return path.resolve(filePath);
+ }
+ });
+
+ /**
+ * The zero-length array is a special case, it seems that
+ * DOM.setFileInputFiles does not actually update the files in that case, so
+ * the solution is to eval the element value to a new FileList directly.
+ */
+ if (files.length === 0) {
+ // XXX: These events should converted to trusted events. Perhaps do this
+ // in `DOM.setFileInputFiles`?
+ await this.evaluate(element => {
+ element.files = new DataTransfer().files;
+
+ // Dispatch events for this case because it should behave akin to a user action.
+ element.dispatchEvent(
+ new Event('input', {bubbles: true, composed: true})
+ );
+ element.dispatchEvent(new Event('change', {bubbles: true}));
+ });
+ return;
+ }
+
+ const {
+ node: {backendNodeId},
+ } = await this.client.send('DOM.describeNode', {
+ objectId: this.id,
+ });
+ await this.client.send('DOM.setFileInputFiles', {
+ objectId: this.id,
+ files,
+ backendNodeId,
+ });
+ }
+
+ @throwIfDisposed()
+ override async autofill(data: AutofillData): Promise<void> {
+ const nodeInfo = await this.client.send('DOM.describeNode', {
+ objectId: this.handle.id,
+ });
+ const fieldId = nodeInfo.node.backendNodeId;
+ const frameId = this.frame._id;
+ await this.client.send('Autofill.trigger', {
+ fieldId,
+ frameId,
+ card: data.creditCard,
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts
new file mode 100644
index 0000000000..8598967fe7
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/EmulationManager.ts
@@ -0,0 +1,554 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {Protocol} from 'devtools-protocol';
+
+import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
+import type {GeolocationOptions, MediaFeature} from '../api/Page.js';
+import {debugError} from '../common/util.js';
+import type {Viewport} from '../common/Viewport.js';
+import {assert} from '../util/assert.js';
+import {invokeAtMostOnceForArguments} from '../util/decorators.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+interface ViewportState {
+ viewport?: Viewport;
+ active: boolean;
+}
+
+interface IdleOverridesState {
+ overrides?: {
+ isUserActive: boolean;
+ isScreenUnlocked: boolean;
+ };
+ active: boolean;
+}
+
+interface TimezoneState {
+ timezoneId?: string;
+ active: boolean;
+}
+
+interface VisionDeficiencyState {
+ visionDeficiency?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'];
+ active: boolean;
+}
+
+interface CpuThrottlingState {
+ factor?: number;
+ active: boolean;
+}
+
+interface MediaFeaturesState {
+ mediaFeatures?: MediaFeature[];
+ active: boolean;
+}
+
+interface MediaTypeState {
+ type?: string;
+ active: boolean;
+}
+
+interface GeoLocationState {
+ geoLocation?: GeolocationOptions;
+ active: boolean;
+}
+
+interface DefaultBackgroundColorState {
+ color?: Protocol.DOM.RGBA;
+ active: boolean;
+}
+
+interface JavascriptEnabledState {
+ javaScriptEnabled: boolean;
+ active: boolean;
+}
+
+/**
+ * @internal
+ */
+export interface ClientProvider {
+ clients(): CDPSession[];
+ registerState(state: EmulatedState<any>): void;
+}
+
+/**
+ * @internal
+ */
+export class EmulatedState<T extends {active: boolean}> {
+ #state: T;
+ #clientProvider: ClientProvider;
+ #updater: (client: CDPSession, state: T) => Promise<void>;
+
+ constructor(
+ initialState: T,
+ clientProvider: ClientProvider,
+ updater: (client: CDPSession, state: T) => Promise<void>
+ ) {
+ this.#state = initialState;
+ this.#clientProvider = clientProvider;
+ this.#updater = updater;
+ this.#clientProvider.registerState(this);
+ }
+
+ async setState(state: T): Promise<void> {
+ this.#state = state;
+ await this.sync();
+ }
+
+ get state(): T {
+ return this.#state;
+ }
+
+ async sync(): Promise<void> {
+ await Promise.all(
+ this.#clientProvider.clients().map(client => {
+ return this.#updater(client, this.#state);
+ })
+ );
+ }
+}
+
+/**
+ * @internal
+ */
+export class EmulationManager {
+ #client: CDPSession;
+
+ #emulatingMobile = false;
+ #hasTouch = false;
+
+ #states: Array<EmulatedState<any>> = [];
+
+ #viewportState = new EmulatedState<ViewportState>(
+ {
+ active: false,
+ },
+ this,
+ this.#applyViewport
+ );
+ #idleOverridesState = new EmulatedState<IdleOverridesState>(
+ {
+ active: false,
+ },
+ this,
+ this.#emulateIdleState
+ );
+ #timezoneState = new EmulatedState<TimezoneState>(
+ {
+ active: false,
+ },
+ this,
+ this.#emulateTimezone
+ );
+ #visionDeficiencyState = new EmulatedState<VisionDeficiencyState>(
+ {
+ active: false,
+ },
+ this,
+ this.#emulateVisionDeficiency
+ );
+ #cpuThrottlingState = new EmulatedState<CpuThrottlingState>(
+ {
+ active: false,
+ },
+ this,
+ this.#emulateCpuThrottling
+ );
+ #mediaFeaturesState = new EmulatedState<MediaFeaturesState>(
+ {
+ active: false,
+ },
+ this,
+ this.#emulateMediaFeatures
+ );
+ #mediaTypeState = new EmulatedState<MediaTypeState>(
+ {
+ active: false,
+ },
+ this,
+ this.#emulateMediaType
+ );
+ #geoLocationState = new EmulatedState<GeoLocationState>(
+ {
+ active: false,
+ },
+ this,
+ this.#setGeolocation
+ );
+ #defaultBackgroundColorState = new EmulatedState<DefaultBackgroundColorState>(
+ {
+ active: false,
+ },
+ this,
+ this.#setDefaultBackgroundColor
+ );
+ #javascriptEnabledState = new EmulatedState<JavascriptEnabledState>(
+ {
+ javaScriptEnabled: true,
+ active: false,
+ },
+ this,
+ this.#setJavaScriptEnabled
+ );
+
+ #secondaryClients = new Set<CDPSession>();
+
+ constructor(client: CDPSession) {
+ this.#client = client;
+ }
+
+ updateClient(client: CDPSession): void {
+ this.#client = client;
+ this.#secondaryClients.delete(client);
+ }
+
+ registerState(state: EmulatedState<any>): void {
+ this.#states.push(state);
+ }
+
+ clients(): CDPSession[] {
+ return [this.#client, ...Array.from(this.#secondaryClients)];
+ }
+
+ async registerSpeculativeSession(client: CDPSession): Promise<void> {
+ this.#secondaryClients.add(client);
+ client.once(CDPSessionEvent.Disconnected, () => {
+ this.#secondaryClients.delete(client);
+ });
+ // We don't await here because we want to register all state changes before
+ // the target is unpaused.
+ void Promise.all(
+ this.#states.map(s => {
+ return s.sync().catch(debugError);
+ })
+ );
+ }
+
+ get javascriptEnabled(): boolean {
+ return this.#javascriptEnabledState.state.javaScriptEnabled;
+ }
+
+ async emulateViewport(viewport: Viewport): Promise<boolean> {
+ await this.#viewportState.setState({
+ viewport,
+ active: true,
+ });
+
+ const mobile = viewport.isMobile || false;
+ const hasTouch = viewport.hasTouch || false;
+ const reloadNeeded =
+ this.#emulatingMobile !== mobile || this.#hasTouch !== hasTouch;
+ this.#emulatingMobile = mobile;
+ this.#hasTouch = hasTouch;
+
+ return reloadNeeded;
+ }
+
+ @invokeAtMostOnceForArguments
+ async #applyViewport(
+ client: CDPSession,
+ viewportState: ViewportState
+ ): Promise<void> {
+ if (!viewportState.viewport) {
+ return;
+ }
+ const {viewport} = viewportState;
+ const mobile = viewport.isMobile || false;
+ const width = viewport.width;
+ const height = viewport.height;
+ const deviceScaleFactor = viewport.deviceScaleFactor ?? 1;
+ const screenOrientation: Protocol.Emulation.ScreenOrientation =
+ viewport.isLandscape
+ ? {angle: 90, type: 'landscapePrimary'}
+ : {angle: 0, type: 'portraitPrimary'};
+ const hasTouch = viewport.hasTouch || false;
+
+ await Promise.all([
+ client.send('Emulation.setDeviceMetricsOverride', {
+ mobile,
+ width,
+ height,
+ deviceScaleFactor,
+ screenOrientation,
+ }),
+ client.send('Emulation.setTouchEmulationEnabled', {
+ enabled: hasTouch,
+ }),
+ ]);
+ }
+
+ async emulateIdleState(overrides?: {
+ isUserActive: boolean;
+ isScreenUnlocked: boolean;
+ }): Promise<void> {
+ await this.#idleOverridesState.setState({
+ active: true,
+ overrides,
+ });
+ }
+
+ @invokeAtMostOnceForArguments
+ async #emulateIdleState(
+ client: CDPSession,
+ idleStateState: IdleOverridesState
+ ): Promise<void> {
+ if (!idleStateState.active) {
+ return;
+ }
+ if (idleStateState.overrides) {
+ await client.send('Emulation.setIdleOverride', {
+ isUserActive: idleStateState.overrides.isUserActive,
+ isScreenUnlocked: idleStateState.overrides.isScreenUnlocked,
+ });
+ } else {
+ await client.send('Emulation.clearIdleOverride');
+ }
+ }
+
+ @invokeAtMostOnceForArguments
+ async #emulateTimezone(
+ client: CDPSession,
+ timezoneState: TimezoneState
+ ): Promise<void> {
+ if (!timezoneState.active) {
+ return;
+ }
+ try {
+ await client.send('Emulation.setTimezoneOverride', {
+ timezoneId: timezoneState.timezoneId || '',
+ });
+ } catch (error) {
+ if (isErrorLike(error) && error.message.includes('Invalid timezone')) {
+ throw new Error(`Invalid timezone ID: ${timezoneState.timezoneId}`);
+ }
+ throw error;
+ }
+ }
+
+ async emulateTimezone(timezoneId?: string): Promise<void> {
+ await this.#timezoneState.setState({
+ timezoneId,
+ active: true,
+ });
+ }
+
+ @invokeAtMostOnceForArguments
+ async #emulateVisionDeficiency(
+ client: CDPSession,
+ visionDeficiency: VisionDeficiencyState
+ ): Promise<void> {
+ if (!visionDeficiency.active) {
+ return;
+ }
+ await client.send('Emulation.setEmulatedVisionDeficiency', {
+ type: visionDeficiency.visionDeficiency || 'none',
+ });
+ }
+
+ async emulateVisionDeficiency(
+ type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
+ ): Promise<void> {
+ const visionDeficiencies = new Set<
+ Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
+ >([
+ 'none',
+ 'achromatopsia',
+ 'blurredVision',
+ 'deuteranopia',
+ 'protanopia',
+ 'tritanopia',
+ ]);
+ assert(
+ !type || visionDeficiencies.has(type),
+ `Unsupported vision deficiency: ${type}`
+ );
+ await this.#visionDeficiencyState.setState({
+ active: true,
+ visionDeficiency: type,
+ });
+ }
+
+ @invokeAtMostOnceForArguments
+ async #emulateCpuThrottling(
+ client: CDPSession,
+ state: CpuThrottlingState
+ ): Promise<void> {
+ if (!state.active) {
+ return;
+ }
+ await client.send('Emulation.setCPUThrottlingRate', {
+ rate: state.factor ?? 1,
+ });
+ }
+
+ async emulateCPUThrottling(factor: number | null): Promise<void> {
+ assert(
+ factor === null || factor >= 1,
+ 'Throttling rate should be greater or equal to 1'
+ );
+ await this.#cpuThrottlingState.setState({
+ active: true,
+ factor: factor ?? undefined,
+ });
+ }
+
+ @invokeAtMostOnceForArguments
+ async #emulateMediaFeatures(
+ client: CDPSession,
+ state: MediaFeaturesState
+ ): Promise<void> {
+ if (!state.active) {
+ return;
+ }
+ await client.send('Emulation.setEmulatedMedia', {
+ features: state.mediaFeatures,
+ });
+ }
+
+ async emulateMediaFeatures(features?: MediaFeature[]): Promise<void> {
+ if (Array.isArray(features)) {
+ for (const mediaFeature of features) {
+ const name = mediaFeature.name;
+ assert(
+ /^(?:prefers-(?:color-scheme|reduced-motion)|color-gamut)$/.test(
+ name
+ ),
+ 'Unsupported media feature: ' + name
+ );
+ }
+ }
+ await this.#mediaFeaturesState.setState({
+ active: true,
+ mediaFeatures: features,
+ });
+ }
+
+ @invokeAtMostOnceForArguments
+ async #emulateMediaType(
+ client: CDPSession,
+ state: MediaTypeState
+ ): Promise<void> {
+ if (!state.active) {
+ return;
+ }
+ await client.send('Emulation.setEmulatedMedia', {
+ media: state.type || '',
+ });
+ }
+
+ async emulateMediaType(type?: string): Promise<void> {
+ assert(
+ type === 'screen' ||
+ type === 'print' ||
+ (type ?? undefined) === undefined,
+ 'Unsupported media type: ' + type
+ );
+ await this.#mediaTypeState.setState({
+ type,
+ active: true,
+ });
+ }
+
+ @invokeAtMostOnceForArguments
+ async #setGeolocation(
+ client: CDPSession,
+ state: GeoLocationState
+ ): Promise<void> {
+ if (!state.active) {
+ return;
+ }
+ await client.send(
+ 'Emulation.setGeolocationOverride',
+ state.geoLocation
+ ? {
+ longitude: state.geoLocation.longitude,
+ latitude: state.geoLocation.latitude,
+ accuracy: state.geoLocation.accuracy,
+ }
+ : undefined
+ );
+ }
+
+ async setGeolocation(options: GeolocationOptions): Promise<void> {
+ const {longitude, latitude, accuracy = 0} = options;
+ if (longitude < -180 || longitude > 180) {
+ throw new Error(
+ `Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`
+ );
+ }
+ if (latitude < -90 || latitude > 90) {
+ throw new Error(
+ `Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`
+ );
+ }
+ if (accuracy < 0) {
+ throw new Error(
+ `Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`
+ );
+ }
+ await this.#geoLocationState.setState({
+ active: true,
+ geoLocation: {
+ longitude,
+ latitude,
+ accuracy,
+ },
+ });
+ }
+
+ @invokeAtMostOnceForArguments
+ async #setDefaultBackgroundColor(
+ client: CDPSession,
+ state: DefaultBackgroundColorState
+ ): Promise<void> {
+ if (!state.active) {
+ return;
+ }
+ await client.send('Emulation.setDefaultBackgroundColorOverride', {
+ color: state.color,
+ });
+ }
+
+ /**
+ * Resets default white background
+ */
+ async resetDefaultBackgroundColor(): Promise<void> {
+ await this.#defaultBackgroundColorState.setState({
+ active: true,
+ color: undefined,
+ });
+ }
+
+ /**
+ * Hides default white background
+ */
+ async setTransparentBackgroundColor(): Promise<void> {
+ await this.#defaultBackgroundColorState.setState({
+ active: true,
+ color: {r: 0, g: 0, b: 0, a: 0},
+ });
+ }
+
+ @invokeAtMostOnceForArguments
+ async #setJavaScriptEnabled(
+ client: CDPSession,
+ state: JavascriptEnabledState
+ ): Promise<void> {
+ if (!state.active) {
+ return;
+ }
+ await client.send('Emulation.setScriptExecutionDisabled', {
+ value: !state.javaScriptEnabled,
+ });
+ }
+
+ async setJavaScriptEnabled(enabled: boolean): Promise<void> {
+ await this.#javascriptEnabledState.setState({
+ active: true,
+ javaScriptEnabled: enabled,
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts
new file mode 100644
index 0000000000..6efdf8ac76
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts
@@ -0,0 +1,392 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {ElementHandle} from '../api/ElementHandle.js';
+import type {JSHandle} from '../api/JSHandle.js';
+import {LazyArg} from '../common/LazyArg.js';
+import {scriptInjector} from '../common/ScriptInjector.js';
+import type {EvaluateFunc, HandleFor} from '../common/types.js';
+import {
+ PuppeteerURL,
+ SOURCE_URL_REGEX,
+ getSourcePuppeteerURLIfAvailable,
+ getSourceUrlComment,
+ isString,
+} from '../common/util.js';
+import type PuppeteerUtil from '../injected/injected.js';
+import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
+import {stringifyFunction} from '../util/Function.js';
+
+import {ARIAQueryHandler} from './AriaQueryHandler.js';
+import {Binding} from './Binding.js';
+import {CdpElementHandle} from './ElementHandle.js';
+import type {IsolatedWorld} from './IsolatedWorld.js';
+import {CdpJSHandle} from './JSHandle.js';
+import {createEvaluationError, valueFromRemoteObject} from './utils.js';
+
+/**
+ * @internal
+ */
+export class ExecutionContext {
+ _client: CDPSession;
+ _world: IsolatedWorld;
+ _contextId: number;
+ _contextName?: string;
+
+ constructor(
+ client: CDPSession,
+ contextPayload: Protocol.Runtime.ExecutionContextDescription,
+ world: IsolatedWorld
+ ) {
+ this._client = client;
+ this._world = world;
+ this._contextId = contextPayload.id;
+ if (contextPayload.name) {
+ this._contextName = contextPayload.name;
+ }
+ }
+
+ #bindingsInstalled = false;
+ #puppeteerUtil?: Promise<JSHandle<PuppeteerUtil>>;
+ get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
+ let promise = Promise.resolve() as Promise<unknown>;
+ if (!this.#bindingsInstalled) {
+ promise = Promise.all([
+ this.#installGlobalBinding(
+ new Binding(
+ '__ariaQuerySelector',
+ ARIAQueryHandler.queryOne as (...args: unknown[]) => unknown
+ )
+ ),
+ this.#installGlobalBinding(
+ new Binding('__ariaQuerySelectorAll', (async (
+ element: ElementHandle<Node>,
+ selector: string
+ ): Promise<JSHandle<Node[]>> => {
+ const results = ARIAQueryHandler.queryAll(element, selector);
+ return await element.realm.evaluateHandle(
+ (...elements) => {
+ return elements;
+ },
+ ...(await AsyncIterableUtil.collect(results))
+ );
+ }) as (...args: unknown[]) => unknown)
+ ),
+ ]);
+ this.#bindingsInstalled = true;
+ }
+ scriptInjector.inject(script => {
+ if (this.#puppeteerUtil) {
+ void this.#puppeteerUtil.then(handle => {
+ void handle.dispose();
+ });
+ }
+ this.#puppeteerUtil = promise.then(() => {
+ return this.evaluateHandle(script) as Promise<JSHandle<PuppeteerUtil>>;
+ });
+ }, !this.#puppeteerUtil);
+ return this.#puppeteerUtil as Promise<JSHandle<PuppeteerUtil>>;
+ }
+
+ async #installGlobalBinding(binding: Binding) {
+ try {
+ if (this._world) {
+ this._world._bindings.set(binding.name, binding);
+ await this._world._addBindingToContext(this, binding.name);
+ }
+ } catch {
+ // If the binding cannot be added, then either the browser doesn't support
+ // bindings (e.g. Firefox) or the context is broken. Either breakage is
+ // okay, so we ignore the error.
+ }
+ }
+
+ /**
+ * Evaluates the given function.
+ *
+ * @example
+ *
+ * ```ts
+ * const executionContext = await page.mainFrame().executionContext();
+ * const result = await executionContext.evaluate(() => Promise.resolve(8 * 7))* ;
+ * console.log(result); // prints "56"
+ * ```
+ *
+ * @example
+ * A string can also be passed in instead of a function:
+ *
+ * ```ts
+ * console.log(await executionContext.evaluate('1 + 2')); // prints "3"
+ * ```
+ *
+ * @example
+ * Handles can also be passed as `args`. They resolve to their referenced object:
+ *
+ * ```ts
+ * const oneHandle = await executionContext.evaluateHandle(() => 1);
+ * const twoHandle = await executionContext.evaluateHandle(() => 2);
+ * const result = await executionContext.evaluate(
+ * (a, b) => a + b,
+ * oneHandle,
+ * twoHandle
+ * );
+ * await oneHandle.dispose();
+ * await twoHandle.dispose();
+ * console.log(result); // prints '3'.
+ * ```
+ *
+ * @param pageFunction - The function to evaluate.
+ * @param args - Additional arguments to pass into the function.
+ * @returns The result of evaluating the function. If the result is an object,
+ * a vanilla object containing the serializable properties of the result is
+ * returned.
+ */
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ return await this.#evaluate(true, pageFunction, ...args);
+ }
+
+ /**
+ * Evaluates the given function.
+ *
+ * Unlike {@link ExecutionContext.evaluate | evaluate}, this method returns a
+ * handle to the result of the function.
+ *
+ * This method may be better suited if the object cannot be serialized (e.g.
+ * `Map`) and requires further manipulation.
+ *
+ * @example
+ *
+ * ```ts
+ * const context = await page.mainFrame().executionContext();
+ * const handle: JSHandle<typeof globalThis> = await context.evaluateHandle(
+ * () => Promise.resolve(self)
+ * );
+ * ```
+ *
+ * @example
+ * A string can also be passed in instead of a function.
+ *
+ * ```ts
+ * const handle: JSHandle<number> = await context.evaluateHandle('1 + 2');
+ * ```
+ *
+ * @example
+ * Handles can also be passed as `args`. They resolve to their referenced object:
+ *
+ * ```ts
+ * const bodyHandle: ElementHandle<HTMLBodyElement> =
+ * await context.evaluateHandle(() => {
+ * return document.body;
+ * });
+ * const stringHandle: JSHandle<string> = await context.evaluateHandle(
+ * body => body.innerHTML,
+ * body
+ * );
+ * console.log(await stringHandle.jsonValue()); // prints body's innerHTML
+ * // Always dispose your garbage! :)
+ * await bodyHandle.dispose();
+ * await stringHandle.dispose();
+ * ```
+ *
+ * @param pageFunction - The function to evaluate.
+ * @param args - Additional arguments to pass into the function.
+ * @returns A {@link JSHandle | handle} to the result of evaluating the
+ * function. If the result is a `Node`, then this will return an
+ * {@link ElementHandle | element handle}.
+ */
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ return await this.#evaluate(false, pageFunction, ...args);
+ }
+
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ returnByValue: true,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>>;
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ returnByValue: false,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ returnByValue: boolean,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
+ const sourceUrlComment = getSourceUrlComment(
+ getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
+ PuppeteerURL.INTERNAL_URL
+ );
+
+ if (isString(pageFunction)) {
+ const contextId = this._contextId;
+ const expression = pageFunction;
+ const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
+ ? expression
+ : `${expression}\n${sourceUrlComment}\n`;
+
+ const {exceptionDetails, result: remoteObject} = await this._client
+ .send('Runtime.evaluate', {
+ expression: expressionWithSourceUrl,
+ contextId,
+ returnByValue,
+ awaitPromise: true,
+ userGesture: true,
+ })
+ .catch(rewriteError);
+
+ if (exceptionDetails) {
+ throw createEvaluationError(exceptionDetails);
+ }
+
+ return returnByValue
+ ? valueFromRemoteObject(remoteObject)
+ : createCdpHandle(this._world, remoteObject);
+ }
+
+ const functionDeclaration = stringifyFunction(pageFunction);
+ const functionDeclarationWithSourceUrl = SOURCE_URL_REGEX.test(
+ functionDeclaration
+ )
+ ? functionDeclaration
+ : `${functionDeclaration}\n${sourceUrlComment}\n`;
+ let callFunctionOnPromise;
+ try {
+ callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
+ functionDeclaration: functionDeclarationWithSourceUrl,
+ executionContextId: this._contextId,
+ arguments: args.length
+ ? await Promise.all(args.map(convertArgument.bind(this)))
+ : [],
+ returnByValue,
+ awaitPromise: true,
+ userGesture: true,
+ });
+ } catch (error) {
+ if (
+ error instanceof TypeError &&
+ error.message.startsWith('Converting circular structure to JSON')
+ ) {
+ error.message += ' Recursive objects are not allowed.';
+ }
+ throw error;
+ }
+ const {exceptionDetails, result: remoteObject} =
+ await callFunctionOnPromise.catch(rewriteError);
+ if (exceptionDetails) {
+ throw createEvaluationError(exceptionDetails);
+ }
+ return returnByValue
+ ? valueFromRemoteObject(remoteObject)
+ : createCdpHandle(this._world, remoteObject);
+
+ async function convertArgument(
+ this: ExecutionContext,
+ arg: unknown
+ ): Promise<Protocol.Runtime.CallArgument> {
+ if (arg instanceof LazyArg) {
+ arg = await arg.get(this);
+ }
+ if (typeof arg === 'bigint') {
+ // eslint-disable-line valid-typeof
+ return {unserializableValue: `${arg.toString()}n`};
+ }
+ if (Object.is(arg, -0)) {
+ return {unserializableValue: '-0'};
+ }
+ if (Object.is(arg, Infinity)) {
+ return {unserializableValue: 'Infinity'};
+ }
+ if (Object.is(arg, -Infinity)) {
+ return {unserializableValue: '-Infinity'};
+ }
+ if (Object.is(arg, NaN)) {
+ return {unserializableValue: 'NaN'};
+ }
+ const objectHandle =
+ arg && (arg instanceof CdpJSHandle || arg instanceof CdpElementHandle)
+ ? arg
+ : null;
+ if (objectHandle) {
+ if (objectHandle.realm !== this._world) {
+ throw new Error(
+ 'JSHandles can be evaluated only in the context they were created!'
+ );
+ }
+ if (objectHandle.disposed) {
+ throw new Error('JSHandle is disposed!');
+ }
+ if (objectHandle.remoteObject().unserializableValue) {
+ return {
+ unserializableValue:
+ objectHandle.remoteObject().unserializableValue,
+ };
+ }
+ if (!objectHandle.remoteObject().objectId) {
+ return {value: objectHandle.remoteObject().value};
+ }
+ return {objectId: objectHandle.remoteObject().objectId};
+ }
+ return {value: arg};
+ }
+ }
+}
+
+const rewriteError = (error: Error): Protocol.Runtime.EvaluateResponse => {
+ if (error.message.includes('Object reference chain is too long')) {
+ return {result: {type: 'undefined'}};
+ }
+ if (error.message.includes("Object couldn't be returned by value")) {
+ return {result: {type: 'undefined'}};
+ }
+
+ if (
+ error.message.endsWith('Cannot find context with specified id') ||
+ error.message.endsWith('Inspected target navigated or closed')
+ ) {
+ throw new Error(
+ 'Execution context was destroyed, most likely because of a navigation.'
+ );
+ }
+ throw error;
+};
+
+/**
+ * @internal
+ */
+export function createCdpHandle(
+ realm: IsolatedWorld,
+ remoteObject: Protocol.Runtime.RemoteObject
+): JSHandle | ElementHandle<Node> {
+ if (remoteObject.subtype === 'node') {
+ return new CdpElementHandle(realm, remoteObject);
+ }
+ return new CdpJSHandle(realm, remoteObject);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FirefoxTargetManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FirefoxTargetManager.ts
new file mode 100644
index 0000000000..0ef09a0093
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FirefoxTargetManager.ts
@@ -0,0 +1,210 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {TargetFilterCallback} from '../api/Browser.js';
+import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
+import {EventEmitter} from '../common/EventEmitter.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+
+import type {CdpCDPSession} from './CDPSession.js';
+import type {Connection} from './Connection.js';
+import type {CdpTarget} from './Target.js';
+import {
+ type TargetFactory,
+ TargetManagerEvent,
+ type TargetManager,
+ type TargetManagerEvents,
+} from './TargetManager.js';
+
+/**
+ * FirefoxTargetManager implements target management using
+ * `Target.setDiscoverTargets` without using auto-attach. It, therefore, creates
+ * targets that lazily establish their CDP sessions.
+ *
+ * Although the approach is potentially flaky, there is no other way for Firefox
+ * because Firefox's CDP implementation does not support auto-attach.
+ *
+ * Firefox does not support targetInfoChanged and detachedFromTarget events:
+ *
+ * - https://bugzilla.mozilla.org/show_bug.cgi?id=1610855
+ * - https://bugzilla.mozilla.org/show_bug.cgi?id=1636979
+ * @internal
+ */
+export class FirefoxTargetManager
+ extends EventEmitter<TargetManagerEvents>
+ implements TargetManager
+{
+ #connection: Connection;
+ /**
+ * Keeps track of the following events: 'Target.targetCreated',
+ * 'Target.targetDestroyed'.
+ *
+ * A target becomes discovered when 'Target.targetCreated' is received.
+ * A target is removed from this map once 'Target.targetDestroyed' is
+ * received.
+ *
+ * `targetFilterCallback` has no effect on this map.
+ */
+ #discoveredTargetsByTargetId = new Map<string, Protocol.Target.TargetInfo>();
+ /**
+ * Keeps track of targets that were created via 'Target.targetCreated'
+ * and which one are not filtered out by `targetFilterCallback`.
+ *
+ * The target is removed from here once it's been destroyed.
+ */
+ #availableTargetsByTargetId = new Map<string, CdpTarget>();
+ /**
+ * Tracks which sessions attach to which target.
+ */
+ #availableTargetsBySessionId = new Map<string, CdpTarget>();
+ #targetFilterCallback: TargetFilterCallback | undefined;
+ #targetFactory: TargetFactory;
+
+ #attachedToTargetListenersBySession = new WeakMap<
+ CDPSession | Connection,
+ (event: Protocol.Target.AttachedToTargetEvent) => Promise<void>
+ >();
+
+ #initializeDeferred = Deferred.create<void>();
+ #targetsIdsForInit = new Set<string>();
+
+ constructor(
+ connection: Connection,
+ targetFactory: TargetFactory,
+ targetFilterCallback?: TargetFilterCallback
+ ) {
+ super();
+ this.#connection = connection;
+ this.#targetFilterCallback = targetFilterCallback;
+ this.#targetFactory = targetFactory;
+
+ this.#connection.on('Target.targetCreated', this.#onTargetCreated);
+ this.#connection.on('Target.targetDestroyed', this.#onTargetDestroyed);
+ this.#connection.on(
+ CDPSessionEvent.SessionDetached,
+ this.#onSessionDetached
+ );
+ this.setupAttachmentListeners(this.#connection);
+ }
+
+ setupAttachmentListeners(session: CDPSession | Connection): void {
+ const listener = (event: Protocol.Target.AttachedToTargetEvent) => {
+ return this.#onAttachedToTarget(session, event);
+ };
+ assert(!this.#attachedToTargetListenersBySession.has(session));
+ this.#attachedToTargetListenersBySession.set(session, listener);
+ session.on('Target.attachedToTarget', listener);
+ }
+
+ #onSessionDetached = (session: CDPSession) => {
+ this.removeSessionListeners(session);
+ this.#availableTargetsBySessionId.delete(session.id());
+ };
+
+ removeSessionListeners(session: CDPSession): void {
+ if (this.#attachedToTargetListenersBySession.has(session)) {
+ session.off(
+ 'Target.attachedToTarget',
+ this.#attachedToTargetListenersBySession.get(session)!
+ );
+ this.#attachedToTargetListenersBySession.delete(session);
+ }
+ }
+
+ getAvailableTargets(): ReadonlyMap<string, CdpTarget> {
+ return this.#availableTargetsByTargetId;
+ }
+
+ dispose(): void {
+ this.#connection.off('Target.targetCreated', this.#onTargetCreated);
+ this.#connection.off('Target.targetDestroyed', this.#onTargetDestroyed);
+ }
+
+ async initialize(): Promise<void> {
+ await this.#connection.send('Target.setDiscoverTargets', {
+ discover: true,
+ filter: [{}],
+ });
+ this.#targetsIdsForInit = new Set(this.#discoveredTargetsByTargetId.keys());
+ await this.#initializeDeferred.valueOrThrow();
+ }
+
+ #onTargetCreated = async (
+ event: Protocol.Target.TargetCreatedEvent
+ ): Promise<void> => {
+ if (this.#discoveredTargetsByTargetId.has(event.targetInfo.targetId)) {
+ return;
+ }
+
+ this.#discoveredTargetsByTargetId.set(
+ event.targetInfo.targetId,
+ event.targetInfo
+ );
+
+ if (event.targetInfo.type === 'browser' && event.targetInfo.attached) {
+ const target = this.#targetFactory(event.targetInfo, undefined);
+ target._initialize();
+ this.#availableTargetsByTargetId.set(event.targetInfo.targetId, target);
+ this.#finishInitializationIfReady(target._targetId);
+ return;
+ }
+
+ const target = this.#targetFactory(event.targetInfo, undefined);
+ if (this.#targetFilterCallback && !this.#targetFilterCallback(target)) {
+ this.#finishInitializationIfReady(event.targetInfo.targetId);
+ return;
+ }
+ target._initialize();
+ this.#availableTargetsByTargetId.set(event.targetInfo.targetId, target);
+ this.emit(TargetManagerEvent.TargetAvailable, target);
+ this.#finishInitializationIfReady(target._targetId);
+ };
+
+ #onTargetDestroyed = (event: Protocol.Target.TargetDestroyedEvent): void => {
+ this.#discoveredTargetsByTargetId.delete(event.targetId);
+ this.#finishInitializationIfReady(event.targetId);
+ const target = this.#availableTargetsByTargetId.get(event.targetId);
+ if (target) {
+ this.emit(TargetManagerEvent.TargetGone, target);
+ this.#availableTargetsByTargetId.delete(event.targetId);
+ }
+ };
+
+ #onAttachedToTarget = async (
+ parentSession: Connection | CDPSession,
+ event: Protocol.Target.AttachedToTargetEvent
+ ) => {
+ const targetInfo = event.targetInfo;
+ const session = this.#connection.session(event.sessionId);
+ if (!session) {
+ throw new Error(`Session ${event.sessionId} was not created.`);
+ }
+
+ const target = this.#availableTargetsByTargetId.get(targetInfo.targetId);
+
+ assert(target, `Target ${targetInfo.targetId} is missing`);
+
+ (session as CdpCDPSession)._setTarget(target);
+ this.setupAttachmentListeners(session);
+
+ this.#availableTargetsBySessionId.set(
+ session.id(),
+ this.#availableTargetsByTargetId.get(targetInfo.targetId)!
+ );
+
+ parentSession.emit(CDPSessionEvent.Ready, session);
+ };
+
+ #finishInitializationIfReady(targetId: string): void {
+ this.#targetsIdsForInit.delete(targetId);
+ if (this.#targetsIdsForInit.size === 0) {
+ this.#initializeDeferred.resolve();
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts
new file mode 100644
index 0000000000..844120d7ff
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts
@@ -0,0 +1,351 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import {Frame, FrameEvent, throwIfDetached} from '../api/Frame.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+import type {WaitTimeoutOptions} from '../api/Page.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import {Deferred} from '../util/Deferred.js';
+import {disposeSymbol} from '../util/disposable.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import type {
+ DeviceRequestPrompt,
+ DeviceRequestPromptManager,
+} from './DeviceRequestPrompt.js';
+import type {FrameManager} from './FrameManager.js';
+import {IsolatedWorld} from './IsolatedWorld.js';
+import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
+import {
+ LifecycleWatcher,
+ type PuppeteerLifeCycleEvent,
+} from './LifecycleWatcher.js';
+import type {CdpPage} from './Page.js';
+
+/**
+ * @internal
+ */
+export class CdpFrame extends Frame {
+ #url = '';
+ #detached = false;
+ #client!: CDPSession;
+
+ _frameManager: FrameManager;
+ override _id: string;
+ _loaderId = '';
+ _lifecycleEvents = new Set<string>();
+ override _parentId?: string;
+
+ constructor(
+ frameManager: FrameManager,
+ frameId: string,
+ parentFrameId: string | undefined,
+ client: CDPSession
+ ) {
+ super();
+ this._frameManager = frameManager;
+ this.#url = '';
+ this._id = frameId;
+ this._parentId = parentFrameId;
+ this.#detached = false;
+
+ this._loaderId = '';
+
+ this.updateClient(client);
+
+ this.on(FrameEvent.FrameSwappedByActivation, () => {
+ // Emulate loading process for swapped frames.
+ this._onLoadingStarted();
+ this._onLoadingStopped();
+ });
+ }
+
+ /**
+ * This is used internally in DevTools.
+ *
+ * @internal
+ */
+ _client(): CDPSession {
+ return this.#client;
+ }
+
+ /**
+ * Updates the frame ID with the new ID. This happens when the main frame is
+ * replaced by a different frame.
+ */
+ updateId(id: string): void {
+ this._id = id;
+ }
+
+ updateClient(client: CDPSession, keepWorlds = false): void {
+ this.#client = client;
+ if (!keepWorlds) {
+ // Clear the current contexts on previous world instances.
+ if (this.worlds) {
+ this.worlds[MAIN_WORLD].clearContext();
+ this.worlds[PUPPETEER_WORLD].clearContext();
+ }
+ this.worlds = {
+ [MAIN_WORLD]: new IsolatedWorld(
+ this,
+ this._frameManager.timeoutSettings
+ ),
+ [PUPPETEER_WORLD]: new IsolatedWorld(
+ this,
+ this._frameManager.timeoutSettings
+ ),
+ };
+ } else {
+ this.worlds[MAIN_WORLD].frameUpdated();
+ this.worlds[PUPPETEER_WORLD].frameUpdated();
+ }
+ }
+
+ override page(): CdpPage {
+ return this._frameManager.page();
+ }
+
+ override isOOPFrame(): boolean {
+ return this.#client !== this._frameManager.client;
+ }
+
+ @throwIfDetached
+ override async goto(
+ url: string,
+ options: {
+ referer?: string;
+ referrerPolicy?: string;
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ } = {}
+ ): Promise<HTTPResponse | null> {
+ const {
+ referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'],
+ referrerPolicy = this._frameManager.networkManager.extraHTTPHeaders()[
+ 'referer-policy'
+ ],
+ waitUntil = ['load'],
+ timeout = this._frameManager.timeoutSettings.navigationTimeout(),
+ } = options;
+
+ let ensureNewDocumentNavigation = false;
+ const watcher = new LifecycleWatcher(
+ this._frameManager.networkManager,
+ this,
+ waitUntil,
+ timeout
+ );
+ let error = await Deferred.race([
+ navigate(
+ this.#client,
+ url,
+ referer,
+ referrerPolicy as Protocol.Page.ReferrerPolicy,
+ this._id
+ ),
+ watcher.terminationPromise(),
+ ]);
+ if (!error) {
+ error = await Deferred.race([
+ watcher.terminationPromise(),
+ ensureNewDocumentNavigation
+ ? watcher.newDocumentNavigationPromise()
+ : watcher.sameDocumentNavigationPromise(),
+ ]);
+ }
+
+ try {
+ if (error) {
+ throw error;
+ }
+ return await watcher.navigationResponse();
+ } finally {
+ watcher.dispose();
+ }
+
+ async function navigate(
+ client: CDPSession,
+ url: string,
+ referrer: string | undefined,
+ referrerPolicy: Protocol.Page.ReferrerPolicy | undefined,
+ frameId: string
+ ): Promise<Error | null> {
+ try {
+ const response = await client.send('Page.navigate', {
+ url,
+ referrer,
+ frameId,
+ referrerPolicy,
+ });
+ ensureNewDocumentNavigation = !!response.loaderId;
+ if (response.errorText === 'net::ERR_HTTP_RESPONSE_CODE_FAILURE') {
+ return null;
+ }
+ return response.errorText
+ ? new Error(`${response.errorText} at ${url}`)
+ : null;
+ } catch (error) {
+ if (isErrorLike(error)) {
+ return error;
+ }
+ throw error;
+ }
+ }
+ }
+
+ @throwIfDetached
+ override async waitForNavigation(
+ options: {
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ } = {}
+ ): Promise<HTTPResponse | null> {
+ const {
+ waitUntil = ['load'],
+ timeout = this._frameManager.timeoutSettings.navigationTimeout(),
+ } = options;
+ const watcher = new LifecycleWatcher(
+ this._frameManager.networkManager,
+ this,
+ waitUntil,
+ timeout
+ );
+ const error = await Deferred.race([
+ watcher.terminationPromise(),
+ watcher.sameDocumentNavigationPromise(),
+ watcher.newDocumentNavigationPromise(),
+ ]);
+ try {
+ if (error) {
+ throw error;
+ }
+ return await watcher.navigationResponse();
+ } finally {
+ watcher.dispose();
+ }
+ }
+
+ override get client(): CDPSession {
+ return this.#client;
+ }
+
+ override mainRealm(): IsolatedWorld {
+ return this.worlds[MAIN_WORLD];
+ }
+
+ override isolatedRealm(): IsolatedWorld {
+ return this.worlds[PUPPETEER_WORLD];
+ }
+
+ @throwIfDetached
+ override async setContent(
+ html: string,
+ options: {
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ } = {}
+ ): Promise<void> {
+ const {
+ waitUntil = ['load'],
+ timeout = this._frameManager.timeoutSettings.navigationTimeout(),
+ } = options;
+
+ // We rely upon the fact that document.open() will reset frame lifecycle with "init"
+ // lifecycle event. @see https://crrev.com/608658
+ await this.setFrameContent(html);
+
+ const watcher = new LifecycleWatcher(
+ this._frameManager.networkManager,
+ this,
+ waitUntil,
+ timeout
+ );
+ const error = await Deferred.race<void | Error | undefined>([
+ watcher.terminationPromise(),
+ watcher.lifecyclePromise(),
+ ]);
+ watcher.dispose();
+ if (error) {
+ throw error;
+ }
+ }
+
+ override url(): string {
+ return this.#url;
+ }
+
+ override parentFrame(): CdpFrame | null {
+ return this._frameManager._frameTree.parentFrame(this._id) || null;
+ }
+
+ override childFrames(): CdpFrame[] {
+ return this._frameManager._frameTree.childFrames(this._id);
+ }
+
+ #deviceRequestPromptManager(): DeviceRequestPromptManager {
+ const rootFrame = this.page().mainFrame();
+ if (this.isOOPFrame() || rootFrame === null) {
+ return this._frameManager._deviceRequestPromptManager(this.#client);
+ } else {
+ return rootFrame._frameManager._deviceRequestPromptManager(this.#client);
+ }
+ }
+
+ @throwIfDetached
+ override async waitForDevicePrompt(
+ options: WaitTimeoutOptions = {}
+ ): Promise<DeviceRequestPrompt> {
+ return await this.#deviceRequestPromptManager().waitForDevicePrompt(
+ options
+ );
+ }
+
+ _navigated(framePayload: Protocol.Page.Frame): void {
+ this._name = framePayload.name;
+ this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`;
+ }
+
+ _navigatedWithinDocument(url: string): void {
+ this.#url = url;
+ }
+
+ _onLifecycleEvent(loaderId: string, name: string): void {
+ if (name === 'init') {
+ this._loaderId = loaderId;
+ this._lifecycleEvents.clear();
+ }
+ this._lifecycleEvents.add(name);
+ }
+
+ _onLoadingStopped(): void {
+ this._lifecycleEvents.add('DOMContentLoaded');
+ this._lifecycleEvents.add('load');
+ }
+
+ _onLoadingStarted(): void {
+ this._hasStartedLoading = true;
+ }
+
+ override get detached(): boolean {
+ return this.#detached;
+ }
+
+ [disposeSymbol](): void {
+ if (this.#detached) {
+ return;
+ }
+ this.#detached = true;
+ this.worlds[MAIN_WORLD][disposeSymbol]();
+ this.worlds[PUPPETEER_WORLD][disposeSymbol]();
+ }
+
+ exposeFunction(): never {
+ throw new UnsupportedOperation();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameManager.ts
new file mode 100644
index 0000000000..48ed9ac2f5
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameManager.ts
@@ -0,0 +1,551 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
+import {FrameEvent} from '../api/Frame.js';
+import {EventEmitter} from '../common/EventEmitter.js';
+import type {TimeoutSettings} from '../common/TimeoutSettings.js';
+import {debugError, PuppeteerURL, UTILITY_WORLD_NAME} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+import {disposeSymbol} from '../util/disposable.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import {CdpCDPSession} from './CDPSession.js';
+import {isTargetClosedError} from './Connection.js';
+import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
+import {ExecutionContext} from './ExecutionContext.js';
+import {CdpFrame} from './Frame.js';
+import type {FrameManagerEvents} from './FrameManagerEvents.js';
+import {FrameManagerEvent} from './FrameManagerEvents.js';
+import {FrameTree} from './FrameTree.js';
+import type {IsolatedWorld} from './IsolatedWorld.js';
+import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
+import {NetworkManager} from './NetworkManager.js';
+import type {CdpPage} from './Page.js';
+import type {CdpTarget} from './Target.js';
+
+const TIME_FOR_WAITING_FOR_SWAP = 100; // ms.
+
+/**
+ * A frame manager manages the frames for a given {@link Page | page}.
+ *
+ * @internal
+ */
+export class FrameManager extends EventEmitter<FrameManagerEvents> {
+ #page: CdpPage;
+ #networkManager: NetworkManager;
+ #timeoutSettings: TimeoutSettings;
+ #contextIdToContext = new Map<string, ExecutionContext>();
+ #isolatedWorlds = new Set<string>();
+ #client: CDPSession;
+
+ _frameTree = new FrameTree<CdpFrame>();
+
+ /**
+ * Set of frame IDs stored to indicate if a frame has received a
+ * frameNavigated event so that frame tree responses could be ignored as the
+ * frameNavigated event usually contains the latest information.
+ */
+ #frameNavigatedReceived = new Set<string>();
+
+ #deviceRequestPromptManagerMap = new WeakMap<
+ CDPSession,
+ DeviceRequestPromptManager
+ >();
+
+ #frameTreeHandled?: Deferred<void>;
+
+ get timeoutSettings(): TimeoutSettings {
+ return this.#timeoutSettings;
+ }
+
+ get networkManager(): NetworkManager {
+ return this.#networkManager;
+ }
+
+ get client(): CDPSession {
+ return this.#client;
+ }
+
+ constructor(
+ client: CDPSession,
+ page: CdpPage,
+ ignoreHTTPSErrors: boolean,
+ timeoutSettings: TimeoutSettings
+ ) {
+ super();
+ this.#client = client;
+ this.#page = page;
+ this.#networkManager = new NetworkManager(ignoreHTTPSErrors, this);
+ this.#timeoutSettings = timeoutSettings;
+ this.setupEventListeners(this.#client);
+ client.once(CDPSessionEvent.Disconnected, () => {
+ this.#onClientDisconnect().catch(debugError);
+ });
+ }
+
+ /**
+ * Called when the frame's client is disconnected. We don't know if the
+ * disconnect means that the frame is removed or if it will be replaced by a
+ * new frame. Therefore, we wait for a swap event.
+ */
+ async #onClientDisconnect() {
+ const mainFrame = this._frameTree.getMainFrame();
+ if (!mainFrame) {
+ return;
+ }
+ for (const child of mainFrame.childFrames()) {
+ this.#removeFramesRecursively(child);
+ }
+ const swapped = Deferred.create<void>({
+ timeout: TIME_FOR_WAITING_FOR_SWAP,
+ message: 'Frame was not swapped',
+ });
+ mainFrame.once(FrameEvent.FrameSwappedByActivation, () => {
+ swapped.resolve();
+ });
+ try {
+ await swapped.valueOrThrow();
+ } catch (err) {
+ this.#removeFramesRecursively(mainFrame);
+ }
+ }
+
+ /**
+ * When the main frame is replaced by another main frame,
+ * we maintain the main frame object identity while updating
+ * its frame tree and ID.
+ */
+ async swapFrameTree(client: CDPSession): Promise<void> {
+ this.#onExecutionContextsCleared(this.#client);
+
+ this.#client = client;
+ assert(
+ this.#client instanceof CdpCDPSession,
+ 'CDPSession is not an instance of CDPSessionImpl.'
+ );
+ const frame = this._frameTree.getMainFrame();
+ if (frame) {
+ this.#frameNavigatedReceived.add(this.#client._target()._targetId);
+ this._frameTree.removeFrame(frame);
+ frame.updateId(this.#client._target()._targetId);
+ frame.mainRealm().clearContext();
+ frame.isolatedRealm().clearContext();
+ this._frameTree.addFrame(frame);
+ frame.updateClient(client, true);
+ }
+ this.setupEventListeners(client);
+ client.once(CDPSessionEvent.Disconnected, () => {
+ this.#onClientDisconnect().catch(debugError);
+ });
+ await this.initialize(client);
+ await this.#networkManager.addClient(client);
+ if (frame) {
+ frame.emit(FrameEvent.FrameSwappedByActivation, undefined);
+ }
+ }
+
+ async registerSpeculativeSession(client: CdpCDPSession): Promise<void> {
+ await this.#networkManager.addClient(client);
+ }
+
+ private setupEventListeners(session: CDPSession) {
+ session.on('Page.frameAttached', async event => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onFrameAttached(session, event.frameId, event.parentFrameId);
+ });
+ session.on('Page.frameNavigated', async event => {
+ this.#frameNavigatedReceived.add(event.frame.id);
+ await this.#frameTreeHandled?.valueOrThrow();
+ void this.#onFrameNavigated(event.frame, event.type);
+ });
+ session.on('Page.navigatedWithinDocument', async event => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onFrameNavigatedWithinDocument(event.frameId, event.url);
+ });
+ session.on(
+ 'Page.frameDetached',
+ async (event: Protocol.Page.FrameDetachedEvent) => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onFrameDetached(
+ event.frameId,
+ event.reason as Protocol.Page.FrameDetachedEventReason
+ );
+ }
+ );
+ session.on('Page.frameStartedLoading', async event => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onFrameStartedLoading(event.frameId);
+ });
+ session.on('Page.frameStoppedLoading', async event => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onFrameStoppedLoading(event.frameId);
+ });
+ session.on('Runtime.executionContextCreated', async event => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onExecutionContextCreated(event.context, session);
+ });
+ session.on('Runtime.executionContextDestroyed', async event => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onExecutionContextDestroyed(event.executionContextId, session);
+ });
+ session.on('Runtime.executionContextsCleared', async () => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onExecutionContextsCleared(session);
+ });
+ session.on('Page.lifecycleEvent', async event => {
+ await this.#frameTreeHandled?.valueOrThrow();
+ this.#onLifecycleEvent(event);
+ });
+ }
+
+ async initialize(client: CDPSession): Promise<void> {
+ try {
+ this.#frameTreeHandled?.resolve();
+ this.#frameTreeHandled = Deferred.create();
+ // We need to schedule all these commands while the target is paused,
+ // therefore, it needs to happen synchroniously. At the same time we
+ // should not start processing execution context and frame events before
+ // we received the initial information about the frame tree.
+ await Promise.all([
+ this.#networkManager.addClient(client),
+ client.send('Page.enable'),
+ client.send('Page.getFrameTree').then(({frameTree}) => {
+ this.#handleFrameTree(client, frameTree);
+ this.#frameTreeHandled?.resolve();
+ }),
+ client.send('Page.setLifecycleEventsEnabled', {enabled: true}),
+ client.send('Runtime.enable').then(() => {
+ return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
+ }),
+ ]);
+ } catch (error) {
+ this.#frameTreeHandled?.resolve();
+ // The target might have been closed before the initialization finished.
+ if (isErrorLike(error) && isTargetClosedError(error)) {
+ return;
+ }
+
+ throw error;
+ }
+ }
+
+ executionContextById(
+ contextId: number,
+ session: CDPSession = this.#client
+ ): ExecutionContext {
+ const context = this.getExecutionContextById(contextId, session);
+ assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
+ return context;
+ }
+
+ getExecutionContextById(
+ contextId: number,
+ session: CDPSession = this.#client
+ ): ExecutionContext | undefined {
+ return this.#contextIdToContext.get(`${session.id()}:${contextId}`);
+ }
+
+ page(): CdpPage {
+ return this.#page;
+ }
+
+ mainFrame(): CdpFrame {
+ const mainFrame = this._frameTree.getMainFrame();
+ assert(mainFrame, 'Requesting main frame too early!');
+ return mainFrame;
+ }
+
+ frames(): CdpFrame[] {
+ return Array.from(this._frameTree.frames());
+ }
+
+ frame(frameId: string): CdpFrame | null {
+ return this._frameTree.getById(frameId) || null;
+ }
+
+ onAttachedToTarget(target: CdpTarget): void {
+ if (target._getTargetInfo().type !== 'iframe') {
+ return;
+ }
+
+ const frame = this.frame(target._getTargetInfo().targetId);
+ if (frame) {
+ frame.updateClient(target._session()!);
+ }
+ this.setupEventListeners(target._session()!);
+ void this.initialize(target._session()!);
+ }
+
+ _deviceRequestPromptManager(client: CDPSession): DeviceRequestPromptManager {
+ let manager = this.#deviceRequestPromptManagerMap.get(client);
+ if (manager === undefined) {
+ manager = new DeviceRequestPromptManager(client, this.#timeoutSettings);
+ this.#deviceRequestPromptManagerMap.set(client, manager);
+ }
+ return manager;
+ }
+
+ #onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
+ const frame = this.frame(event.frameId);
+ if (!frame) {
+ return;
+ }
+ frame._onLifecycleEvent(event.loaderId, event.name);
+ this.emit(FrameManagerEvent.LifecycleEvent, frame);
+ frame.emit(FrameEvent.LifecycleEvent, undefined);
+ }
+
+ #onFrameStartedLoading(frameId: string): void {
+ const frame = this.frame(frameId);
+ if (!frame) {
+ return;
+ }
+ frame._onLoadingStarted();
+ }
+
+ #onFrameStoppedLoading(frameId: string): void {
+ const frame = this.frame(frameId);
+ if (!frame) {
+ return;
+ }
+ frame._onLoadingStopped();
+ this.emit(FrameManagerEvent.LifecycleEvent, frame);
+ frame.emit(FrameEvent.LifecycleEvent, undefined);
+ }
+
+ #handleFrameTree(
+ session: CDPSession,
+ frameTree: Protocol.Page.FrameTree
+ ): void {
+ if (frameTree.frame.parentId) {
+ this.#onFrameAttached(
+ session,
+ frameTree.frame.id,
+ frameTree.frame.parentId
+ );
+ }
+ if (!this.#frameNavigatedReceived.has(frameTree.frame.id)) {
+ void this.#onFrameNavigated(frameTree.frame, 'Navigation');
+ } else {
+ this.#frameNavigatedReceived.delete(frameTree.frame.id);
+ }
+
+ if (!frameTree.childFrames) {
+ return;
+ }
+
+ for (const child of frameTree.childFrames) {
+ this.#handleFrameTree(session, child);
+ }
+ }
+
+ #onFrameAttached(
+ session: CDPSession,
+ frameId: string,
+ parentFrameId: string
+ ): void {
+ let frame = this.frame(frameId);
+ if (frame) {
+ if (session && frame.isOOPFrame()) {
+ // If an OOP iframes becomes a normal iframe again
+ // it is first attached to the parent page before
+ // the target is removed.
+ frame.updateClient(session);
+ }
+ return;
+ }
+
+ frame = new CdpFrame(this, frameId, parentFrameId, session);
+ this._frameTree.addFrame(frame);
+ this.emit(FrameManagerEvent.FrameAttached, frame);
+ }
+
+ async #onFrameNavigated(
+ framePayload: Protocol.Page.Frame,
+ navigationType: Protocol.Page.NavigationType
+ ): Promise<void> {
+ const frameId = framePayload.id;
+ const isMainFrame = !framePayload.parentId;
+
+ let frame = this._frameTree.getById(frameId);
+
+ // Detach all child frames first.
+ if (frame) {
+ for (const child of frame.childFrames()) {
+ this.#removeFramesRecursively(child);
+ }
+ }
+
+ // Update or create main frame.
+ if (isMainFrame) {
+ if (frame) {
+ // Update frame id to retain frame identity on cross-process navigation.
+ this._frameTree.removeFrame(frame);
+ frame._id = frameId;
+ } else {
+ // Initial main frame navigation.
+ frame = new CdpFrame(this, frameId, undefined, this.#client);
+ }
+ this._frameTree.addFrame(frame);
+ }
+
+ frame = await this._frameTree.waitForFrame(frameId);
+ frame._navigated(framePayload);
+ this.emit(FrameManagerEvent.FrameNavigated, frame);
+ frame.emit(FrameEvent.FrameNavigated, navigationType);
+ }
+
+ async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> {
+ const key = `${session.id()}:${name}`;
+
+ if (this.#isolatedWorlds.has(key)) {
+ return;
+ }
+
+ await session.send('Page.addScriptToEvaluateOnNewDocument', {
+ source: `//# sourceURL=${PuppeteerURL.INTERNAL_URL}`,
+ worldName: name,
+ });
+
+ await Promise.all(
+ this.frames()
+ .filter(frame => {
+ return frame.client === session;
+ })
+ .map(frame => {
+ // Frames might be removed before we send this, so we don't want to
+ // throw an error.
+ return session
+ .send('Page.createIsolatedWorld', {
+ frameId: frame._id,
+ worldName: name,
+ grantUniveralAccess: true,
+ })
+ .catch(debugError);
+ })
+ );
+
+ this.#isolatedWorlds.add(key);
+ }
+
+ #onFrameNavigatedWithinDocument(frameId: string, url: string): void {
+ const frame = this.frame(frameId);
+ if (!frame) {
+ return;
+ }
+ frame._navigatedWithinDocument(url);
+ this.emit(FrameManagerEvent.FrameNavigatedWithinDocument, frame);
+ frame.emit(FrameEvent.FrameNavigatedWithinDocument, undefined);
+ this.emit(FrameManagerEvent.FrameNavigated, frame);
+ frame.emit(FrameEvent.FrameNavigated, 'Navigation');
+ }
+
+ #onFrameDetached(
+ frameId: string,
+ reason: Protocol.Page.FrameDetachedEventReason
+ ): void {
+ const frame = this.frame(frameId);
+ if (!frame) {
+ return;
+ }
+ switch (reason) {
+ case 'remove':
+ // Only remove the frame if the reason for the detached event is
+ // an actual removement of the frame.
+ // For frames that become OOP iframes, the reason would be 'swap'.
+ this.#removeFramesRecursively(frame);
+ break;
+ case 'swap':
+ this.emit(FrameManagerEvent.FrameSwapped, frame);
+ frame.emit(FrameEvent.FrameSwapped, undefined);
+ break;
+ }
+ }
+
+ #onExecutionContextCreated(
+ contextPayload: Protocol.Runtime.ExecutionContextDescription,
+ session: CDPSession
+ ): void {
+ const auxData = contextPayload.auxData as {frameId?: string} | undefined;
+ const frameId = auxData && auxData.frameId;
+ const frame = typeof frameId === 'string' ? this.frame(frameId) : undefined;
+ let world: IsolatedWorld | undefined;
+ if (frame) {
+ // Only care about execution contexts created for the current session.
+ if (frame.client !== session) {
+ return;
+ }
+ if (contextPayload.auxData && contextPayload.auxData['isDefault']) {
+ world = frame.worlds[MAIN_WORLD];
+ } else if (
+ contextPayload.name === UTILITY_WORLD_NAME &&
+ !frame.worlds[PUPPETEER_WORLD].hasContext()
+ ) {
+ // In case of multiple sessions to the same target, there's a race between
+ // connections so we might end up creating multiple isolated worlds.
+ // We can use either.
+ world = frame.worlds[PUPPETEER_WORLD];
+ }
+ }
+ // If there is no world, the context is not meant to be handled by us.
+ if (!world) {
+ return;
+ }
+ const context = new ExecutionContext(
+ frame?.client || this.#client,
+ contextPayload,
+ world
+ );
+ if (world) {
+ world.setContext(context);
+ }
+ const key = `${session.id()}:${contextPayload.id}`;
+ this.#contextIdToContext.set(key, context);
+ }
+
+ #onExecutionContextDestroyed(
+ executionContextId: number,
+ session: CDPSession
+ ): void {
+ const key = `${session.id()}:${executionContextId}`;
+ const context = this.#contextIdToContext.get(key);
+ if (!context) {
+ return;
+ }
+ this.#contextIdToContext.delete(key);
+ if (context._world) {
+ context._world.clearContext();
+ }
+ }
+
+ #onExecutionContextsCleared(session: CDPSession): void {
+ for (const [key, context] of this.#contextIdToContext.entries()) {
+ // Make sure to only clear execution contexts that belong
+ // to the current session.
+ if (context._client !== session) {
+ continue;
+ }
+ if (context._world) {
+ context._world.clearContext();
+ }
+ this.#contextIdToContext.delete(key);
+ }
+ }
+
+ #removeFramesRecursively(frame: CdpFrame): void {
+ for (const child of frame.childFrames()) {
+ this.#removeFramesRecursively(child);
+ }
+ frame[disposeSymbol]();
+ this._frameTree.removeFrame(frame);
+ this.emit(FrameManagerEvent.FrameDetached, frame);
+ frame.emit(FrameEvent.FrameDetached, frame);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameManagerEvents.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameManagerEvents.ts
new file mode 100644
index 0000000000..645dd86d71
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameManagerEvents.ts
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {EventType} from '../common/EventEmitter.js';
+
+import type {CdpFrame} from './Frame.js';
+
+/**
+ * We use symbols to prevent external parties listening to these events.
+ * They are internal to Puppeteer.
+ *
+ * @internal
+ */
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace FrameManagerEvent {
+ export const FrameAttached = Symbol('FrameManager.FrameAttached');
+ export const FrameNavigated = Symbol('FrameManager.FrameNavigated');
+ export const FrameDetached = Symbol('FrameManager.FrameDetached');
+ export const FrameSwapped = Symbol('FrameManager.FrameSwapped');
+ export const LifecycleEvent = Symbol('FrameManager.LifecycleEvent');
+ export const FrameNavigatedWithinDocument = Symbol(
+ 'FrameManager.FrameNavigatedWithinDocument'
+ );
+}
+
+/**
+ * @internal
+ */
+export interface FrameManagerEvents extends Record<EventType, unknown> {
+ [FrameManagerEvent.FrameAttached]: CdpFrame;
+ [FrameManagerEvent.FrameNavigated]: CdpFrame;
+ [FrameManagerEvent.FrameDetached]: CdpFrame;
+ [FrameManagerEvent.FrameSwapped]: CdpFrame;
+ [FrameManagerEvent.LifecycleEvent]: CdpFrame;
+ [FrameManagerEvent.FrameNavigatedWithinDocument]: CdpFrame;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameTree.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameTree.ts
new file mode 100644
index 0000000000..7ee1b86b5f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/FrameTree.ts
@@ -0,0 +1,98 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Frame} from '../api/Frame.js';
+import {Deferred} from '../util/Deferred.js';
+
+/**
+ * Keeps track of the page frame tree and it's is managed by
+ * {@link FrameManager}. FrameTree uses frame IDs to reference frame and it
+ * means that referenced frames might not be in the tree anymore. Thus, the tree
+ * structure is eventually consistent.
+ * @internal
+ */
+export class FrameTree<FrameType extends Frame> {
+ #frames = new Map<string, FrameType>();
+ // frameID -> parentFrameID
+ #parentIds = new Map<string, string>();
+ // frameID -> childFrameIDs
+ #childIds = new Map<string, Set<string>>();
+ #mainFrame?: FrameType;
+ #waitRequests = new Map<string, Set<Deferred<FrameType>>>();
+
+ getMainFrame(): FrameType | undefined {
+ return this.#mainFrame;
+ }
+
+ getById(frameId: string): FrameType | undefined {
+ return this.#frames.get(frameId);
+ }
+
+ /**
+ * Returns a promise that is resolved once the frame with
+ * the given ID is added to the tree.
+ */
+ waitForFrame(frameId: string): Promise<FrameType> {
+ const frame = this.getById(frameId);
+ if (frame) {
+ return Promise.resolve(frame);
+ }
+ const deferred = Deferred.create<FrameType>();
+ const callbacks =
+ this.#waitRequests.get(frameId) || new Set<Deferred<FrameType>>();
+ callbacks.add(deferred);
+ return deferred.valueOrThrow();
+ }
+
+ frames(): FrameType[] {
+ return Array.from(this.#frames.values());
+ }
+
+ addFrame(frame: FrameType): void {
+ this.#frames.set(frame._id, frame);
+ if (frame._parentId) {
+ this.#parentIds.set(frame._id, frame._parentId);
+ if (!this.#childIds.has(frame._parentId)) {
+ this.#childIds.set(frame._parentId, new Set());
+ }
+ this.#childIds.get(frame._parentId)!.add(frame._id);
+ } else if (!this.#mainFrame) {
+ this.#mainFrame = frame;
+ }
+ this.#waitRequests.get(frame._id)?.forEach(request => {
+ return request.resolve(frame);
+ });
+ }
+
+ removeFrame(frame: FrameType): void {
+ this.#frames.delete(frame._id);
+ this.#parentIds.delete(frame._id);
+ if (frame._parentId) {
+ this.#childIds.get(frame._parentId)?.delete(frame._id);
+ } else {
+ this.#mainFrame = undefined;
+ }
+ }
+
+ childFrames(frameId: string): FrameType[] {
+ const childIds = this.#childIds.get(frameId);
+ if (!childIds) {
+ return [];
+ }
+ return Array.from(childIds)
+ .map(id => {
+ return this.getById(id);
+ })
+ .filter((frame): frame is FrameType => {
+ return frame !== undefined;
+ });
+ }
+
+ parentFrame(frameId: string): FrameType | undefined {
+ const parentId = this.#parentIds.get(frameId);
+ return parentId ? this.getById(parentId) : undefined;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts
new file mode 100644
index 0000000000..029e77470b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPRequest.ts
@@ -0,0 +1,449 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {Frame} from '../api/Frame.js';
+import {
+ type ContinueRequestOverrides,
+ type ErrorCode,
+ headersArray,
+ HTTPRequest,
+ InterceptResolutionAction,
+ type InterceptResolutionState,
+ type ResourceType,
+ type ResponseForRequest,
+ STATUS_TEXTS,
+} from '../api/HTTPRequest.js';
+import type {ProtocolError} from '../common/Errors.js';
+import {debugError, isString} from '../common/util.js';
+import {assert} from '../util/assert.js';
+
+import type {CdpHTTPResponse} from './HTTPResponse.js';
+
+/**
+ * @internal
+ */
+export class CdpHTTPRequest extends HTTPRequest {
+ declare _redirectChain: CdpHTTPRequest[];
+ declare _response: CdpHTTPResponse | null;
+
+ #client: CDPSession;
+ #isNavigationRequest: boolean;
+ #allowInterception: boolean;
+ #interceptionHandled = false;
+ #url: string;
+ #resourceType: ResourceType;
+
+ #method: string;
+ #hasPostData = false;
+ #postData?: string;
+ #headers: Record<string, string> = {};
+ #frame: Frame | null;
+ #continueRequestOverrides: ContinueRequestOverrides;
+ #responseForRequest: Partial<ResponseForRequest> | null = null;
+ #abortErrorReason: Protocol.Network.ErrorReason | null = null;
+ #interceptResolutionState: InterceptResolutionState = {
+ action: InterceptResolutionAction.None,
+ };
+ #interceptHandlers: Array<() => void | PromiseLike<any>>;
+ #initiator?: Protocol.Network.Initiator;
+
+ override get client(): CDPSession {
+ return this.#client;
+ }
+
+ constructor(
+ client: CDPSession,
+ frame: Frame | null,
+ interceptionId: string | undefined,
+ allowInterception: boolean,
+ data: {
+ /**
+ * Request identifier.
+ */
+ requestId: Protocol.Network.RequestId;
+ /**
+ * Loader identifier. Empty string if the request is fetched from worker.
+ */
+ loaderId?: Protocol.Network.LoaderId;
+ /**
+ * URL of the document this request is loaded for.
+ */
+ documentURL?: string;
+ /**
+ * Request data.
+ */
+ request: Protocol.Network.Request;
+ /**
+ * Request initiator.
+ */
+ initiator?: Protocol.Network.Initiator;
+ /**
+ * Type of this resource.
+ */
+ type?: Protocol.Network.ResourceType;
+ },
+ redirectChain: CdpHTTPRequest[]
+ ) {
+ super();
+ this.#client = client;
+ this._requestId = data.requestId;
+ this.#isNavigationRequest =
+ data.requestId === data.loaderId && data.type === 'Document';
+ this._interceptionId = interceptionId;
+ this.#allowInterception = allowInterception;
+ this.#url = data.request.url;
+ this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
+ this.#method = data.request.method;
+ this.#postData = data.request.postData;
+ this.#hasPostData = data.request.hasPostData ?? false;
+ this.#frame = frame;
+ this._redirectChain = redirectChain;
+ this.#continueRequestOverrides = {};
+ this.#interceptHandlers = [];
+ this.#initiator = data.initiator;
+
+ for (const [key, value] of Object.entries(data.request.headers)) {
+ this.#headers[key.toLowerCase()] = value;
+ }
+ }
+
+ override url(): string {
+ return this.#url;
+ }
+
+ override continueRequestOverrides(): ContinueRequestOverrides {
+ assert(this.#allowInterception, 'Request Interception is not enabled!');
+ return this.#continueRequestOverrides;
+ }
+
+ override responseForRequest(): Partial<ResponseForRequest> | null {
+ assert(this.#allowInterception, 'Request Interception is not enabled!');
+ return this.#responseForRequest;
+ }
+
+ override abortErrorReason(): Protocol.Network.ErrorReason | null {
+ assert(this.#allowInterception, 'Request Interception is not enabled!');
+ return this.#abortErrorReason;
+ }
+
+ override interceptResolutionState(): InterceptResolutionState {
+ if (!this.#allowInterception) {
+ return {action: InterceptResolutionAction.Disabled};
+ }
+ if (this.#interceptionHandled) {
+ return {action: InterceptResolutionAction.AlreadyHandled};
+ }
+ return {...this.#interceptResolutionState};
+ }
+
+ override isInterceptResolutionHandled(): boolean {
+ return this.#interceptionHandled;
+ }
+
+ enqueueInterceptAction(
+ pendingHandler: () => void | PromiseLike<unknown>
+ ): void {
+ this.#interceptHandlers.push(pendingHandler);
+ }
+
+ override async finalizeInterceptions(): Promise<void> {
+ await this.#interceptHandlers.reduce((promiseChain, interceptAction) => {
+ return promiseChain.then(interceptAction);
+ }, Promise.resolve());
+ const {action} = this.interceptResolutionState();
+ switch (action) {
+ case 'abort':
+ return await this.#abort(this.#abortErrorReason);
+ case 'respond':
+ if (this.#responseForRequest === null) {
+ throw new Error('Response is missing for the interception');
+ }
+ return await this.#respond(this.#responseForRequest);
+ case 'continue':
+ return await this.#continue(this.#continueRequestOverrides);
+ }
+ }
+
+ override resourceType(): ResourceType {
+ return this.#resourceType;
+ }
+
+ override method(): string {
+ return this.#method;
+ }
+
+ override postData(): string | undefined {
+ return this.#postData;
+ }
+
+ override hasPostData(): boolean {
+ return this.#hasPostData;
+ }
+
+ override async fetchPostData(): Promise<string | undefined> {
+ try {
+ const result = await this.#client.send('Network.getRequestPostData', {
+ requestId: this._requestId,
+ });
+ return result.postData;
+ } catch (err) {
+ debugError(err);
+ return;
+ }
+ }
+
+ override headers(): Record<string, string> {
+ return this.#headers;
+ }
+
+ override response(): CdpHTTPResponse | null {
+ return this._response;
+ }
+
+ override frame(): Frame | null {
+ return this.#frame;
+ }
+
+ override isNavigationRequest(): boolean {
+ return this.#isNavigationRequest;
+ }
+
+ override initiator(): Protocol.Network.Initiator | undefined {
+ return this.#initiator;
+ }
+
+ override redirectChain(): CdpHTTPRequest[] {
+ return this._redirectChain.slice();
+ }
+
+ override failure(): {errorText: string} | null {
+ if (!this._failureText) {
+ return null;
+ }
+ return {
+ errorText: this._failureText,
+ };
+ }
+
+ override async continue(
+ overrides: ContinueRequestOverrides = {},
+ priority?: number
+ ): Promise<void> {
+ // Request interception is not supported for data: urls.
+ if (this.#url.startsWith('data:')) {
+ return;
+ }
+ assert(this.#allowInterception, 'Request Interception is not enabled!');
+ assert(!this.#interceptionHandled, 'Request is already handled!');
+ if (priority === undefined) {
+ return await this.#continue(overrides);
+ }
+ this.#continueRequestOverrides = overrides;
+ if (
+ this.#interceptResolutionState.priority === undefined ||
+ priority > this.#interceptResolutionState.priority
+ ) {
+ this.#interceptResolutionState = {
+ action: InterceptResolutionAction.Continue,
+ priority,
+ };
+ return;
+ }
+ if (priority === this.#interceptResolutionState.priority) {
+ if (
+ this.#interceptResolutionState.action === 'abort' ||
+ this.#interceptResolutionState.action === 'respond'
+ ) {
+ return;
+ }
+ this.#interceptResolutionState.action =
+ InterceptResolutionAction.Continue;
+ }
+ return;
+ }
+
+ async #continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
+ const {url, method, postData, headers} = overrides;
+ this.#interceptionHandled = true;
+
+ const postDataBinaryBase64 = postData
+ ? Buffer.from(postData).toString('base64')
+ : undefined;
+
+ if (this._interceptionId === undefined) {
+ throw new Error(
+ 'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest'
+ );
+ }
+ await this.#client
+ .send('Fetch.continueRequest', {
+ requestId: this._interceptionId,
+ url,
+ method,
+ postData: postDataBinaryBase64,
+ headers: headers ? headersArray(headers) : undefined,
+ })
+ .catch(error => {
+ this.#interceptionHandled = false;
+ return handleError(error);
+ });
+ }
+
+ override async respond(
+ response: Partial<ResponseForRequest>,
+ priority?: number
+ ): Promise<void> {
+ // Mocking responses for dataURL requests is not currently supported.
+ if (this.#url.startsWith('data:')) {
+ return;
+ }
+ assert(this.#allowInterception, 'Request Interception is not enabled!');
+ assert(!this.#interceptionHandled, 'Request is already handled!');
+ if (priority === undefined) {
+ return await this.#respond(response);
+ }
+ this.#responseForRequest = response;
+ if (
+ this.#interceptResolutionState.priority === undefined ||
+ priority > this.#interceptResolutionState.priority
+ ) {
+ this.#interceptResolutionState = {
+ action: InterceptResolutionAction.Respond,
+ priority,
+ };
+ return;
+ }
+ if (priority === this.#interceptResolutionState.priority) {
+ if (this.#interceptResolutionState.action === 'abort') {
+ return;
+ }
+ this.#interceptResolutionState.action = InterceptResolutionAction.Respond;
+ }
+ }
+
+ async #respond(response: Partial<ResponseForRequest>): Promise<void> {
+ this.#interceptionHandled = true;
+
+ const responseBody: Buffer | null =
+ response.body && isString(response.body)
+ ? Buffer.from(response.body)
+ : (response.body as Buffer) || null;
+
+ const responseHeaders: Record<string, string | string[]> = {};
+ if (response.headers) {
+ for (const header of Object.keys(response.headers)) {
+ const value = response.headers[header];
+
+ responseHeaders[header.toLowerCase()] = Array.isArray(value)
+ ? value.map(item => {
+ return String(item);
+ })
+ : String(value);
+ }
+ }
+ if (response.contentType) {
+ responseHeaders['content-type'] = response.contentType;
+ }
+ if (responseBody && !('content-length' in responseHeaders)) {
+ responseHeaders['content-length'] = String(
+ Buffer.byteLength(responseBody)
+ );
+ }
+
+ const status = response.status || 200;
+ if (this._interceptionId === undefined) {
+ throw new Error(
+ 'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest'
+ );
+ }
+ await this.#client
+ .send('Fetch.fulfillRequest', {
+ requestId: this._interceptionId,
+ responseCode: status,
+ responsePhrase: STATUS_TEXTS[status],
+ responseHeaders: headersArray(responseHeaders),
+ body: responseBody ? responseBody.toString('base64') : undefined,
+ })
+ .catch(error => {
+ this.#interceptionHandled = false;
+ return handleError(error);
+ });
+ }
+
+ override async abort(
+ errorCode: ErrorCode = 'failed',
+ priority?: number
+ ): Promise<void> {
+ // Request interception is not supported for data: urls.
+ if (this.#url.startsWith('data:')) {
+ return;
+ }
+ const errorReason = errorReasons[errorCode];
+ assert(errorReason, 'Unknown error code: ' + errorCode);
+ assert(this.#allowInterception, 'Request Interception is not enabled!');
+ assert(!this.#interceptionHandled, 'Request is already handled!');
+ if (priority === undefined) {
+ return await this.#abort(errorReason);
+ }
+ this.#abortErrorReason = errorReason;
+ if (
+ this.#interceptResolutionState.priority === undefined ||
+ priority >= this.#interceptResolutionState.priority
+ ) {
+ this.#interceptResolutionState = {
+ action: InterceptResolutionAction.Abort,
+ priority,
+ };
+ return;
+ }
+ }
+
+ async #abort(
+ errorReason: Protocol.Network.ErrorReason | null
+ ): Promise<void> {
+ this.#interceptionHandled = true;
+ if (this._interceptionId === undefined) {
+ throw new Error(
+ 'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
+ );
+ }
+ await this.#client
+ .send('Fetch.failRequest', {
+ requestId: this._interceptionId,
+ errorReason: errorReason || 'Failed',
+ })
+ .catch(handleError);
+ }
+}
+
+const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = {
+ aborted: 'Aborted',
+ accessdenied: 'AccessDenied',
+ addressunreachable: 'AddressUnreachable',
+ blockedbyclient: 'BlockedByClient',
+ blockedbyresponse: 'BlockedByResponse',
+ connectionaborted: 'ConnectionAborted',
+ connectionclosed: 'ConnectionClosed',
+ connectionfailed: 'ConnectionFailed',
+ connectionrefused: 'ConnectionRefused',
+ connectionreset: 'ConnectionReset',
+ internetdisconnected: 'InternetDisconnected',
+ namenotresolved: 'NameNotResolved',
+ timedout: 'TimedOut',
+ failed: 'Failed',
+} as const;
+
+async function handleError(error: ProtocolError) {
+ if (['Invalid header'].includes(error.originalMessage)) {
+ throw error;
+ }
+ // In certain cases, protocol will return error if the request was
+ // already canceled or the page was closed. We should tolerate these
+ // errors.
+ debugError(error);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts
new file mode 100644
index 0000000000..2b2264ffd4
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/HTTPResponse.ts
@@ -0,0 +1,173 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {Frame} from '../api/Frame.js';
+import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
+import {ProtocolError} from '../common/Errors.js';
+import {SecurityDetails} from '../common/SecurityDetails.js';
+import {Deferred} from '../util/Deferred.js';
+
+import type {CdpHTTPRequest} from './HTTPRequest.js';
+
+/**
+ * @internal
+ */
+export class CdpHTTPResponse extends HTTPResponse {
+ #client: CDPSession;
+ #request: CdpHTTPRequest;
+ #contentPromise: Promise<Buffer> | null = null;
+ #bodyLoadedDeferred = Deferred.create<void, Error>();
+ #remoteAddress: RemoteAddress;
+ #status: number;
+ #statusText: string;
+ #url: string;
+ #fromDiskCache: boolean;
+ #fromServiceWorker: boolean;
+ #headers: Record<string, string> = {};
+ #securityDetails: SecurityDetails | null;
+ #timing: Protocol.Network.ResourceTiming | null;
+
+ constructor(
+ client: CDPSession,
+ request: CdpHTTPRequest,
+ responsePayload: Protocol.Network.Response,
+ extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
+ ) {
+ super();
+ this.#client = client;
+ this.#request = request;
+
+ this.#remoteAddress = {
+ ip: responsePayload.remoteIPAddress,
+ port: responsePayload.remotePort,
+ };
+ this.#statusText =
+ this.#parseStatusTextFromExtraInfo(extraInfo) ||
+ responsePayload.statusText;
+ this.#url = request.url();
+ this.#fromDiskCache = !!responsePayload.fromDiskCache;
+ this.#fromServiceWorker = !!responsePayload.fromServiceWorker;
+
+ this.#status = extraInfo ? extraInfo.statusCode : responsePayload.status;
+ const headers = extraInfo ? extraInfo.headers : responsePayload.headers;
+ for (const [key, value] of Object.entries(headers)) {
+ this.#headers[key.toLowerCase()] = value;
+ }
+
+ this.#securityDetails = responsePayload.securityDetails
+ ? new SecurityDetails(responsePayload.securityDetails)
+ : null;
+ this.#timing = responsePayload.timing || null;
+ }
+
+ #parseStatusTextFromExtraInfo(
+ extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
+ ): string | undefined {
+ if (!extraInfo || !extraInfo.headersText) {
+ return;
+ }
+ const firstLine = extraInfo.headersText.split('\r', 1)[0];
+ if (!firstLine) {
+ return;
+ }
+ const match = firstLine.match(/[^ ]* [^ ]* (.*)/);
+ if (!match) {
+ return;
+ }
+ const statusText = match[1];
+ if (!statusText) {
+ return;
+ }
+ return statusText;
+ }
+
+ _resolveBody(err?: Error): void {
+ if (err) {
+ return this.#bodyLoadedDeferred.reject(err);
+ }
+ return this.#bodyLoadedDeferred.resolve();
+ }
+
+ override remoteAddress(): RemoteAddress {
+ return this.#remoteAddress;
+ }
+
+ override url(): string {
+ return this.#url;
+ }
+
+ override status(): number {
+ return this.#status;
+ }
+
+ override statusText(): string {
+ return this.#statusText;
+ }
+
+ override headers(): Record<string, string> {
+ return this.#headers;
+ }
+
+ override securityDetails(): SecurityDetails | null {
+ return this.#securityDetails;
+ }
+
+ override timing(): Protocol.Network.ResourceTiming | null {
+ return this.#timing;
+ }
+
+ override buffer(): Promise<Buffer> {
+ if (!this.#contentPromise) {
+ this.#contentPromise = this.#bodyLoadedDeferred
+ .valueOrThrow()
+ .then(async () => {
+ try {
+ const response = await this.#client.send(
+ 'Network.getResponseBody',
+ {
+ requestId: this.#request._requestId,
+ }
+ );
+ return Buffer.from(
+ response.body,
+ response.base64Encoded ? 'base64' : 'utf8'
+ );
+ } catch (error) {
+ if (
+ error instanceof ProtocolError &&
+ error.originalMessage ===
+ 'No resource with given identifier found'
+ ) {
+ throw new ProtocolError(
+ 'Could not load body for this request. This might happen if the request is a preflight request.'
+ );
+ }
+
+ throw error;
+ }
+ });
+ }
+ return this.#contentPromise;
+ }
+
+ override request(): CdpHTTPRequest {
+ return this.#request;
+ }
+
+ override fromCache(): boolean {
+ return this.#fromDiskCache || this.#request._fromMemoryCache;
+ }
+
+ override fromServiceWorker(): boolean {
+ return this.#fromServiceWorker;
+ }
+
+ override frame(): Frame | null {
+ return this.#request.frame();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts
new file mode 100644
index 0000000000..9bfafddcf3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Input.ts
@@ -0,0 +1,604 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {Point} from '../api/ElementHandle.js';
+import {
+ Keyboard,
+ type KeyDownOptions,
+ type KeyPressOptions,
+ Mouse,
+ MouseButton,
+ type MouseClickOptions,
+ type MouseMoveOptions,
+ type MouseOptions,
+ type MouseWheelOptions,
+ Touchscreen,
+ type KeyboardTypeOptions,
+} from '../api/Input.js';
+import {
+ _keyDefinitions,
+ type KeyDefinition,
+ type KeyInput,
+} from '../common/USKeyboardLayout.js';
+import {assert} from '../util/assert.js';
+
+type KeyDescription = Required<
+ Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>
+>;
+
+/**
+ * @internal
+ */
+export class CdpKeyboard extends Keyboard {
+ #client: CDPSession;
+ #pressedKeys = new Set<string>();
+
+ _modifiers = 0;
+
+ constructor(client: CDPSession) {
+ super();
+ this.#client = client;
+ }
+
+ updateClient(client: CDPSession): void {
+ this.#client = client;
+ }
+
+ override async down(
+ key: KeyInput,
+ options: Readonly<KeyDownOptions> = {
+ text: undefined,
+ commands: [],
+ }
+ ): Promise<void> {
+ const description = this.#keyDescriptionForString(key);
+
+ const autoRepeat = this.#pressedKeys.has(description.code);
+ this.#pressedKeys.add(description.code);
+ this._modifiers |= this.#modifierBit(description.key);
+
+ const text = options.text === undefined ? description.text : options.text;
+ await this.#client.send('Input.dispatchKeyEvent', {
+ type: text ? 'keyDown' : 'rawKeyDown',
+ modifiers: this._modifiers,
+ windowsVirtualKeyCode: description.keyCode,
+ code: description.code,
+ key: description.key,
+ text: text,
+ unmodifiedText: text,
+ autoRepeat,
+ location: description.location,
+ isKeypad: description.location === 3,
+ commands: options.commands,
+ });
+ }
+
+ #modifierBit(key: string): number {
+ if (key === 'Alt') {
+ return 1;
+ }
+ if (key === 'Control') {
+ return 2;
+ }
+ if (key === 'Meta') {
+ return 4;
+ }
+ if (key === 'Shift') {
+ return 8;
+ }
+ return 0;
+ }
+
+ #keyDescriptionForString(keyString: KeyInput): KeyDescription {
+ const shift = this._modifiers & 8;
+ const description = {
+ key: '',
+ keyCode: 0,
+ code: '',
+ text: '',
+ location: 0,
+ };
+
+ const definition = _keyDefinitions[keyString];
+ assert(definition, `Unknown key: "${keyString}"`);
+
+ if (definition.key) {
+ description.key = definition.key;
+ }
+ if (shift && definition.shiftKey) {
+ description.key = definition.shiftKey;
+ }
+
+ if (definition.keyCode) {
+ description.keyCode = definition.keyCode;
+ }
+ if (shift && definition.shiftKeyCode) {
+ description.keyCode = definition.shiftKeyCode;
+ }
+
+ if (definition.code) {
+ description.code = definition.code;
+ }
+
+ if (definition.location) {
+ description.location = definition.location;
+ }
+
+ if (description.key.length === 1) {
+ description.text = description.key;
+ }
+
+ if (definition.text) {
+ description.text = definition.text;
+ }
+ if (shift && definition.shiftText) {
+ description.text = definition.shiftText;
+ }
+
+ // if any modifiers besides shift are pressed, no text should be sent
+ if (this._modifiers & ~8) {
+ description.text = '';
+ }
+
+ return description;
+ }
+
+ override async up(key: KeyInput): Promise<void> {
+ const description = this.#keyDescriptionForString(key);
+
+ this._modifiers &= ~this.#modifierBit(description.key);
+ this.#pressedKeys.delete(description.code);
+ await this.#client.send('Input.dispatchKeyEvent', {
+ type: 'keyUp',
+ modifiers: this._modifiers,
+ key: description.key,
+ windowsVirtualKeyCode: description.keyCode,
+ code: description.code,
+ location: description.location,
+ });
+ }
+
+ override async sendCharacter(char: string): Promise<void> {
+ await this.#client.send('Input.insertText', {text: char});
+ }
+
+ private charIsKey(char: string): char is KeyInput {
+ return !!_keyDefinitions[char as KeyInput];
+ }
+
+ override async type(
+ text: string,
+ options: Readonly<KeyboardTypeOptions> = {}
+ ): Promise<void> {
+ const delay = options.delay || undefined;
+ for (const char of text) {
+ if (this.charIsKey(char)) {
+ await this.press(char, {delay});
+ } else {
+ if (delay) {
+ await new Promise(f => {
+ return setTimeout(f, delay);
+ });
+ }
+ await this.sendCharacter(char);
+ }
+ }
+ }
+
+ override async press(
+ key: KeyInput,
+ options: Readonly<KeyPressOptions> = {}
+ ): Promise<void> {
+ const {delay = null} = options;
+ await this.down(key, options);
+ if (delay) {
+ await new Promise(f => {
+ return setTimeout(f, options.delay);
+ });
+ }
+ await this.up(key);
+ }
+}
+
+/**
+ * This must follow {@link Protocol.Input.DispatchMouseEventRequest.buttons}.
+ */
+const enum MouseButtonFlag {
+ None = 0,
+ Left = 1,
+ Right = 1 << 1,
+ Middle = 1 << 2,
+ Back = 1 << 3,
+ Forward = 1 << 4,
+}
+
+const getFlag = (button: MouseButton): MouseButtonFlag => {
+ switch (button) {
+ case MouseButton.Left:
+ return MouseButtonFlag.Left;
+ case MouseButton.Right:
+ return MouseButtonFlag.Right;
+ case MouseButton.Middle:
+ return MouseButtonFlag.Middle;
+ case MouseButton.Back:
+ return MouseButtonFlag.Back;
+ case MouseButton.Forward:
+ return MouseButtonFlag.Forward;
+ }
+};
+
+/**
+ * This should match
+ * https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/browser/renderer_host/input/web_input_event_builders_mac.mm;drc=a61b95c63b0b75c1cfe872d9c8cdf927c226046e;bpv=1;bpt=1;l=221.
+ */
+const getButtonFromPressedButtons = (
+ buttons: number
+): Protocol.Input.MouseButton => {
+ if (buttons & MouseButtonFlag.Left) {
+ return MouseButton.Left;
+ } else if (buttons & MouseButtonFlag.Right) {
+ return MouseButton.Right;
+ } else if (buttons & MouseButtonFlag.Middle) {
+ return MouseButton.Middle;
+ } else if (buttons & MouseButtonFlag.Back) {
+ return MouseButton.Back;
+ } else if (buttons & MouseButtonFlag.Forward) {
+ return MouseButton.Forward;
+ }
+ return 'none';
+};
+
+interface MouseState {
+ /**
+ * The current position of the mouse.
+ */
+ position: Point;
+ /**
+ * The buttons that are currently being pressed.
+ */
+ buttons: number;
+}
+
+/**
+ * @internal
+ */
+export class CdpMouse extends Mouse {
+ #client: CDPSession;
+ #keyboard: CdpKeyboard;
+
+ constructor(client: CDPSession, keyboard: CdpKeyboard) {
+ super();
+ this.#client = client;
+ this.#keyboard = keyboard;
+ }
+
+ updateClient(client: CDPSession): void {
+ this.#client = client;
+ }
+
+ #_state: Readonly<MouseState> = {
+ position: {x: 0, y: 0},
+ buttons: MouseButtonFlag.None,
+ };
+ get #state(): MouseState {
+ return Object.assign({...this.#_state}, ...this.#transactions);
+ }
+
+ // Transactions can run in parallel, so we store each of thme in this array.
+ #transactions: Array<Partial<MouseState>> = [];
+ #createTransaction(): {
+ update: (updates: Partial<MouseState>) => void;
+ commit: () => void;
+ rollback: () => void;
+ } {
+ const transaction: Partial<MouseState> = {};
+ this.#transactions.push(transaction);
+ const popTransaction = () => {
+ this.#transactions.splice(this.#transactions.indexOf(transaction), 1);
+ };
+ return {
+ update: (updates: Partial<MouseState>) => {
+ Object.assign(transaction, updates);
+ },
+ commit: () => {
+ this.#_state = {...this.#_state, ...transaction};
+ popTransaction();
+ },
+ rollback: popTransaction,
+ };
+ }
+
+ /**
+ * This is a shortcut for a typical update, commit/rollback lifecycle based on
+ * the error of the action.
+ */
+ async #withTransaction(
+ action: (update: (updates: Partial<MouseState>) => void) => Promise<unknown>
+ ): Promise<void> {
+ const {update, commit, rollback} = this.#createTransaction();
+ try {
+ await action(update);
+ commit();
+ } catch (error) {
+ rollback();
+ throw error;
+ }
+ }
+
+ override async reset(): Promise<void> {
+ const actions = [];
+ for (const [flag, button] of [
+ [MouseButtonFlag.Left, MouseButton.Left],
+ [MouseButtonFlag.Middle, MouseButton.Middle],
+ [MouseButtonFlag.Right, MouseButton.Right],
+ [MouseButtonFlag.Forward, MouseButton.Forward],
+ [MouseButtonFlag.Back, MouseButton.Back],
+ ] as const) {
+ if (this.#state.buttons & flag) {
+ actions.push(this.up({button: button}));
+ }
+ }
+ if (this.#state.position.x !== 0 || this.#state.position.y !== 0) {
+ actions.push(this.move(0, 0));
+ }
+ await Promise.all(actions);
+ }
+
+ override async move(
+ x: number,
+ y: number,
+ options: Readonly<MouseMoveOptions> = {}
+ ): Promise<void> {
+ const {steps = 1} = options;
+ const from = this.#state.position;
+ const to = {x, y};
+ for (let i = 1; i <= steps; i++) {
+ await this.#withTransaction(updateState => {
+ updateState({
+ position: {
+ x: from.x + (to.x - from.x) * (i / steps),
+ y: from.y + (to.y - from.y) * (i / steps),
+ },
+ });
+ const {buttons, position} = this.#state;
+ return this.#client.send('Input.dispatchMouseEvent', {
+ type: 'mouseMoved',
+ modifiers: this.#keyboard._modifiers,
+ buttons,
+ button: getButtonFromPressedButtons(buttons),
+ ...position,
+ });
+ });
+ }
+ }
+
+ override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
+ const {button = MouseButton.Left, clickCount = 1} = options;
+ const flag = getFlag(button);
+ if (!flag) {
+ throw new Error(`Unsupported mouse button: ${button}`);
+ }
+ if (this.#state.buttons & flag) {
+ throw new Error(`'${button}' is already pressed.`);
+ }
+ await this.#withTransaction(updateState => {
+ updateState({
+ buttons: this.#state.buttons | flag,
+ });
+ const {buttons, position} = this.#state;
+ return this.#client.send('Input.dispatchMouseEvent', {
+ type: 'mousePressed',
+ modifiers: this.#keyboard._modifiers,
+ clickCount,
+ buttons,
+ button,
+ ...position,
+ });
+ });
+ }
+
+ override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
+ const {button = MouseButton.Left, clickCount = 1} = options;
+ const flag = getFlag(button);
+ if (!flag) {
+ throw new Error(`Unsupported mouse button: ${button}`);
+ }
+ if (!(this.#state.buttons & flag)) {
+ throw new Error(`'${button}' is not pressed.`);
+ }
+ await this.#withTransaction(updateState => {
+ updateState({
+ buttons: this.#state.buttons & ~flag,
+ });
+ const {buttons, position} = this.#state;
+ return this.#client.send('Input.dispatchMouseEvent', {
+ type: 'mouseReleased',
+ modifiers: this.#keyboard._modifiers,
+ clickCount,
+ buttons,
+ button,
+ ...position,
+ });
+ });
+ }
+
+ override async click(
+ x: number,
+ y: number,
+ options: Readonly<MouseClickOptions> = {}
+ ): Promise<void> {
+ const {delay, count = 1, clickCount = count} = options;
+ if (count < 1) {
+ throw new Error('Click must occur a positive number of times.');
+ }
+ const actions: Array<Promise<void>> = [this.move(x, y)];
+ if (clickCount === count) {
+ for (let i = 1; i < count; ++i) {
+ actions.push(
+ this.down({...options, clickCount: i}),
+ this.up({...options, clickCount: i})
+ );
+ }
+ }
+ actions.push(this.down({...options, clickCount}));
+ if (typeof delay === 'number') {
+ await Promise.all(actions);
+ actions.length = 0;
+ await new Promise(resolve => {
+ setTimeout(resolve, delay);
+ });
+ }
+ actions.push(this.up({...options, clickCount}));
+ await Promise.all(actions);
+ }
+
+ override async wheel(
+ options: Readonly<MouseWheelOptions> = {}
+ ): Promise<void> {
+ const {deltaX = 0, deltaY = 0} = options;
+ const {position, buttons} = this.#state;
+ await this.#client.send('Input.dispatchMouseEvent', {
+ type: 'mouseWheel',
+ pointerType: 'mouse',
+ modifiers: this.#keyboard._modifiers,
+ deltaY,
+ deltaX,
+ buttons,
+ ...position,
+ });
+ }
+
+ override async drag(
+ start: Point,
+ target: Point
+ ): Promise<Protocol.Input.DragData> {
+ const promise = new Promise<Protocol.Input.DragData>(resolve => {
+ this.#client.once('Input.dragIntercepted', event => {
+ return resolve(event.data);
+ });
+ });
+ await this.move(start.x, start.y);
+ await this.down();
+ await this.move(target.x, target.y);
+ return await promise;
+ }
+
+ override async dragEnter(
+ target: Point,
+ data: Protocol.Input.DragData
+ ): Promise<void> {
+ await this.#client.send('Input.dispatchDragEvent', {
+ type: 'dragEnter',
+ x: target.x,
+ y: target.y,
+ modifiers: this.#keyboard._modifiers,
+ data,
+ });
+ }
+
+ override async dragOver(
+ target: Point,
+ data: Protocol.Input.DragData
+ ): Promise<void> {
+ await this.#client.send('Input.dispatchDragEvent', {
+ type: 'dragOver',
+ x: target.x,
+ y: target.y,
+ modifiers: this.#keyboard._modifiers,
+ data,
+ });
+ }
+
+ override async drop(
+ target: Point,
+ data: Protocol.Input.DragData
+ ): Promise<void> {
+ await this.#client.send('Input.dispatchDragEvent', {
+ type: 'drop',
+ x: target.x,
+ y: target.y,
+ modifiers: this.#keyboard._modifiers,
+ data,
+ });
+ }
+
+ override async dragAndDrop(
+ start: Point,
+ target: Point,
+ options: {delay?: number} = {}
+ ): Promise<void> {
+ const {delay = null} = options;
+ const data = await this.drag(start, target);
+ await this.dragEnter(target, data);
+ await this.dragOver(target, data);
+ if (delay) {
+ await new Promise(resolve => {
+ return setTimeout(resolve, delay);
+ });
+ }
+ await this.drop(target, data);
+ await this.up();
+ }
+}
+
+/**
+ * @internal
+ */
+export class CdpTouchscreen extends Touchscreen {
+ #client: CDPSession;
+ #keyboard: CdpKeyboard;
+
+ constructor(client: CDPSession, keyboard: CdpKeyboard) {
+ super();
+ this.#client = client;
+ this.#keyboard = keyboard;
+ }
+
+ updateClient(client: CDPSession): void {
+ this.#client = client;
+ }
+
+ override async touchStart(x: number, y: number): Promise<void> {
+ await this.#client.send('Input.dispatchTouchEvent', {
+ type: 'touchStart',
+ touchPoints: [
+ {
+ x: Math.round(x),
+ y: Math.round(y),
+ radiusX: 0.5,
+ radiusY: 0.5,
+ },
+ ],
+ modifiers: this.#keyboard._modifiers,
+ });
+ }
+
+ override async touchMove(x: number, y: number): Promise<void> {
+ await this.#client.send('Input.dispatchTouchEvent', {
+ type: 'touchMove',
+ touchPoints: [
+ {
+ x: Math.round(x),
+ y: Math.round(y),
+ radiusX: 0.5,
+ radiusY: 0.5,
+ },
+ ],
+ modifiers: this.#keyboard._modifiers,
+ });
+ }
+
+ override async touchEnd(): Promise<void> {
+ await this.#client.send('Input.dispatchTouchEvent', {
+ type: 'touchEnd',
+ touchPoints: [],
+ modifiers: this.#keyboard._modifiers,
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/IsolatedWorld.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/IsolatedWorld.ts
new file mode 100644
index 0000000000..5846ef3652
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/IsolatedWorld.ts
@@ -0,0 +1,273 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {JSHandle} from '../api/JSHandle.js';
+import {Realm} from '../api/Realm.js';
+import type {TimeoutSettings} from '../common/TimeoutSettings.js';
+import type {BindingPayload, EvaluateFunc, HandleFor} from '../common/types.js';
+import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js';
+import {Deferred} from '../util/Deferred.js';
+import {disposeSymbol} from '../util/disposable.js';
+import {Mutex} from '../util/Mutex.js';
+
+import type {Binding} from './Binding.js';
+import {ExecutionContext, createCdpHandle} from './ExecutionContext.js';
+import type {CdpFrame} from './Frame.js';
+import type {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
+import {addPageBinding} from './utils.js';
+import type {CdpWebWorker} from './WebWorker.js';
+
+/**
+ * @internal
+ */
+export interface PageBinding {
+ name: string;
+ pptrFunction: Function;
+}
+
+/**
+ * @internal
+ */
+export interface IsolatedWorldChart {
+ [key: string]: IsolatedWorld;
+ [MAIN_WORLD]: IsolatedWorld;
+ [PUPPETEER_WORLD]: IsolatedWorld;
+}
+
+/**
+ * @internal
+ */
+export class IsolatedWorld extends Realm {
+ #context = Deferred.create<ExecutionContext>();
+
+ // Set of bindings that have been registered in the current context.
+ #contextBindings = new Set<string>();
+
+ // Contains mapping from functions that should be bound to Puppeteer functions.
+ #bindings = new Map<string, Binding>();
+
+ get _bindings(): Map<string, Binding> {
+ return this.#bindings;
+ }
+
+ readonly #frameOrWorker: CdpFrame | CdpWebWorker;
+
+ constructor(
+ frameOrWorker: CdpFrame | CdpWebWorker,
+ timeoutSettings: TimeoutSettings
+ ) {
+ super(timeoutSettings);
+ this.#frameOrWorker = frameOrWorker;
+ this.frameUpdated();
+ }
+
+ get environment(): CdpFrame | CdpWebWorker {
+ return this.#frameOrWorker;
+ }
+
+ frameUpdated(): void {
+ this.client.on('Runtime.bindingCalled', this.#onBindingCalled);
+ }
+
+ get client(): CDPSession {
+ return this.#frameOrWorker.client;
+ }
+
+ clearContext(): void {
+ // The message has to match the CDP message expected by the WaitTask class.
+ this.#context?.reject(new Error('Execution context was destroyed'));
+ this.#context = Deferred.create();
+ if ('clearDocumentHandle' in this.#frameOrWorker) {
+ this.#frameOrWorker.clearDocumentHandle();
+ }
+ }
+
+ setContext(context: ExecutionContext): void {
+ this.#contextBindings.clear();
+ this.#context.resolve(context);
+ void this.taskManager.rerunAll();
+ }
+
+ hasContext(): boolean {
+ return this.#context.resolved();
+ }
+
+ #executionContext(): Promise<ExecutionContext> {
+ if (this.disposed) {
+ throw new Error(
+ `Execution context is not available in detached frame "${this.environment.url()}" (are you trying to evaluate?)`
+ );
+ }
+ if (this.#context === null) {
+ throw new Error(`Execution content promise is missing`);
+ }
+ return this.#context.valueOrThrow();
+ }
+
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluateHandle.name,
+ pageFunction
+ );
+ const context = await this.#executionContext();
+ return await context.evaluateHandle(pageFunction, ...args);
+ }
+
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ pageFunction = withSourcePuppeteerURLIfNone(
+ this.evaluate.name,
+ pageFunction
+ );
+ let context = this.#context.value();
+ if (!context || !(context instanceof ExecutionContext)) {
+ context = await this.#executionContext();
+ }
+ return await context.evaluate(pageFunction, ...args);
+ }
+
+ // If multiple waitFor are set up asynchronously, we need to wait for the
+ // first one to set up the binding in the page before running the others.
+ #mutex = new Mutex();
+ async _addBindingToContext(
+ context: ExecutionContext,
+ name: string
+ ): Promise<void> {
+ if (this.#contextBindings.has(name)) {
+ return;
+ }
+
+ using _ = await this.#mutex.acquire();
+ try {
+ await context._client.send(
+ 'Runtime.addBinding',
+ context._contextName
+ ? {
+ name,
+ executionContextName: context._contextName,
+ }
+ : {
+ name,
+ executionContextId: context._contextId,
+ }
+ );
+
+ await context.evaluate(addPageBinding, 'internal', name);
+
+ this.#contextBindings.add(name);
+ } catch (error) {
+ // We could have tried to evaluate in a context which was already
+ // destroyed. This happens, for example, if the page is navigated while
+ // we are trying to add the binding
+ if (error instanceof Error) {
+ // Destroyed context.
+ if (error.message.includes('Execution context was destroyed')) {
+ return;
+ }
+ // Missing context.
+ if (error.message.includes('Cannot find context with specified id')) {
+ return;
+ }
+ }
+
+ debugError(error);
+ }
+ }
+
+ #onBindingCalled = async (
+ event: Protocol.Runtime.BindingCalledEvent
+ ): Promise<void> => {
+ let payload: BindingPayload;
+ try {
+ payload = JSON.parse(event.payload);
+ } catch {
+ // The binding was either called by something in the page or it was
+ // called before our wrapper was initialized.
+ return;
+ }
+ const {type, name, seq, args, isTrivial} = payload;
+ if (type !== 'internal') {
+ return;
+ }
+ if (!this.#contextBindings.has(name)) {
+ return;
+ }
+
+ try {
+ const context = await this.#context.valueOrThrow();
+ if (event.executionContextId !== context._contextId) {
+ return;
+ }
+
+ const binding = this._bindings.get(name);
+ await binding?.run(context, seq, args, isTrivial);
+ } catch (err) {
+ debugError(err);
+ }
+ };
+
+ override async adoptBackendNode(
+ backendNodeId?: Protocol.DOM.BackendNodeId
+ ): Promise<JSHandle<Node>> {
+ const executionContext = await this.#executionContext();
+ const {object} = await this.client.send('DOM.resolveNode', {
+ backendNodeId: backendNodeId,
+ executionContextId: executionContext._contextId,
+ });
+ return createCdpHandle(this, object) as JSHandle<Node>;
+ }
+
+ async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
+ if (handle.realm === this) {
+ // If the context has already adopted this handle, clone it so downstream
+ // disposal doesn't become an issue.
+ return (await handle.evaluateHandle(value => {
+ return value;
+ })) as unknown as T;
+ }
+ const nodeInfo = await this.client.send('DOM.describeNode', {
+ objectId: handle.id,
+ });
+ return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T;
+ }
+
+ async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
+ if (handle.realm === this) {
+ return handle;
+ }
+ // Implies it's a primitive value, probably.
+ if (handle.remoteObject().objectId === undefined) {
+ return handle;
+ }
+ const info = await this.client.send('DOM.describeNode', {
+ objectId: handle.remoteObject().objectId,
+ });
+ const newHandle = (await this.adoptBackendNode(
+ info.node.backendNodeId
+ )) as T;
+ await handle.dispose();
+ return newHandle;
+ }
+
+ [disposeSymbol](): void {
+ super[disposeSymbol]();
+ this.client.off('Runtime.bindingCalled', this.#onBindingCalled);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/IsolatedWorlds.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/IsolatedWorlds.ts
new file mode 100644
index 0000000000..ddb6c2381d
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/IsolatedWorlds.ts
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * A unique key for {@link IsolatedWorldChart} to denote the default world.
+ * Execution contexts are automatically created in the default world.
+ *
+ * @internal
+ */
+export const MAIN_WORLD = Symbol('mainWorld');
+/**
+ * A unique key for {@link IsolatedWorldChart} to denote the puppeteer world.
+ * This world contains all puppeteer-internal bindings/code.
+ *
+ * @internal
+ */
+export const PUPPETEER_WORLD = Symbol('puppeteerWorld');
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/JSHandle.ts
new file mode 100644
index 0000000000..bba5f96b5d
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/JSHandle.ts
@@ -0,0 +1,109 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import {JSHandle} from '../api/JSHandle.js';
+import {debugError} from '../common/util.js';
+
+import type {CdpElementHandle} from './ElementHandle.js';
+import type {IsolatedWorld} from './IsolatedWorld.js';
+import {valueFromRemoteObject} from './utils.js';
+
+/**
+ * @internal
+ */
+export class CdpJSHandle<T = unknown> extends JSHandle<T> {
+ #disposed = false;
+ readonly #remoteObject: Protocol.Runtime.RemoteObject;
+ readonly #world: IsolatedWorld;
+
+ constructor(
+ world: IsolatedWorld,
+ remoteObject: Protocol.Runtime.RemoteObject
+ ) {
+ super();
+ this.#world = world;
+ this.#remoteObject = remoteObject;
+ }
+
+ override get disposed(): boolean {
+ return this.#disposed;
+ }
+
+ override get realm(): IsolatedWorld {
+ return this.#world;
+ }
+
+ get client(): CDPSession {
+ return this.realm.environment.client;
+ }
+
+ override async jsonValue(): Promise<T> {
+ if (!this.#remoteObject.objectId) {
+ return valueFromRemoteObject(this.#remoteObject);
+ }
+ const value = await this.evaluate(object => {
+ return object;
+ });
+ if (value === undefined) {
+ throw new Error('Could not serialize referenced object');
+ }
+ return value;
+ }
+
+ /**
+ * Either `null` or the handle itself if the handle is an
+ * instance of {@link ElementHandle}.
+ */
+ override asElement(): CdpElementHandle<Node> | null {
+ return null;
+ }
+
+ override async dispose(): Promise<void> {
+ if (this.#disposed) {
+ return;
+ }
+ this.#disposed = true;
+ await releaseObject(this.client, this.#remoteObject);
+ }
+
+ override toString(): string {
+ if (!this.#remoteObject.objectId) {
+ return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject);
+ }
+ const type = this.#remoteObject.subtype || this.#remoteObject.type;
+ return 'JSHandle@' + type;
+ }
+
+ override get id(): string | undefined {
+ return this.#remoteObject.objectId;
+ }
+
+ override remoteObject(): Protocol.Runtime.RemoteObject {
+ return this.#remoteObject;
+ }
+}
+
+/**
+ * @internal
+ */
+export async function releaseObject(
+ client: CDPSession,
+ remoteObject: Protocol.Runtime.RemoteObject
+): Promise<void> {
+ if (!remoteObject.objectId) {
+ return;
+ }
+ await client
+ .send('Runtime.releaseObject', {objectId: remoteObject.objectId})
+ .catch(error => {
+ // Exceptions might happen in case of a page been navigated or closed.
+ // Swallow these since they are harmless and we don't leak anything in this case.
+ debugError(error);
+ });
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts
new file mode 100644
index 0000000000..a4f5aaa468
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/LifecycleWatcher.ts
@@ -0,0 +1,298 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type Protocol from 'devtools-protocol';
+
+import {type Frame, FrameEvent} from '../api/Frame.js';
+import type {HTTPRequest} from '../api/HTTPRequest.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+import type {TimeoutError} from '../common/Errors.js';
+import {EventSubscription} from '../common/EventEmitter.js';
+import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+import {DisposableStack} from '../util/disposable.js';
+
+import type {CdpFrame} from './Frame.js';
+import {FrameManagerEvent} from './FrameManagerEvents.js';
+import type {NetworkManager} from './NetworkManager.js';
+
+/**
+ * @public
+ */
+export type PuppeteerLifeCycleEvent =
+ /**
+ * Waits for the 'load' event.
+ */
+ | 'load'
+ /**
+ * Waits for the 'DOMContentLoaded' event.
+ */
+ | 'domcontentloaded'
+ /**
+ * Waits till there are no more than 0 network connections for at least `500`
+ * ms.
+ */
+ | 'networkidle0'
+ /**
+ * Waits till there are no more than 2 network connections for at least `500`
+ * ms.
+ */
+ | 'networkidle2';
+
+/**
+ * @public
+ */
+export type ProtocolLifeCycleEvent =
+ | 'load'
+ | 'DOMContentLoaded'
+ | 'networkIdle'
+ | 'networkAlmostIdle';
+
+const puppeteerToProtocolLifecycle = new Map<
+ PuppeteerLifeCycleEvent,
+ ProtocolLifeCycleEvent
+>([
+ ['load', 'load'],
+ ['domcontentloaded', 'DOMContentLoaded'],
+ ['networkidle0', 'networkIdle'],
+ ['networkidle2', 'networkAlmostIdle'],
+]);
+
+/**
+ * @internal
+ */
+export class LifecycleWatcher {
+ #expectedLifecycle: ProtocolLifeCycleEvent[];
+ #frame: CdpFrame;
+ #timeout: number;
+ #navigationRequest: HTTPRequest | null = null;
+ #subscriptions = new DisposableStack();
+ #initialLoaderId: string;
+
+ #terminationDeferred: Deferred<Error>;
+ #sameDocumentNavigationDeferred = Deferred.create<undefined>();
+ #lifecycleDeferred = Deferred.create<void>();
+ #newDocumentNavigationDeferred = Deferred.create<undefined>();
+
+ #hasSameDocumentNavigation?: boolean;
+ #swapped?: boolean;
+
+ #navigationResponseReceived?: Deferred<void>;
+
+ constructor(
+ networkManager: NetworkManager,
+ frame: CdpFrame,
+ waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[],
+ timeout: number
+ ) {
+ if (Array.isArray(waitUntil)) {
+ waitUntil = waitUntil.slice();
+ } else if (typeof waitUntil === 'string') {
+ waitUntil = [waitUntil];
+ }
+ this.#initialLoaderId = frame._loaderId;
+ this.#expectedLifecycle = waitUntil.map(value => {
+ const protocolEvent = puppeteerToProtocolLifecycle.get(value);
+ assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
+ return protocolEvent as ProtocolLifeCycleEvent;
+ });
+
+ this.#frame = frame;
+ this.#timeout = timeout;
+ this.#subscriptions.use(
+ // Revert if TODO #1 is done
+ new EventSubscription(
+ frame._frameManager,
+ FrameManagerEvent.LifecycleEvent,
+ this.#checkLifecycleComplete.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ frame,
+ FrameEvent.FrameNavigatedWithinDocument,
+ this.#navigatedWithinDocument.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ frame,
+ FrameEvent.FrameNavigated,
+ this.#navigated.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ frame,
+ FrameEvent.FrameSwapped,
+ this.#frameSwapped.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ frame,
+ FrameEvent.FrameSwappedByActivation,
+ this.#frameSwapped.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ frame,
+ FrameEvent.FrameDetached,
+ this.#onFrameDetached.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ networkManager,
+ NetworkManagerEvent.Request,
+ this.#onRequest.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ networkManager,
+ NetworkManagerEvent.Response,
+ this.#onResponse.bind(this)
+ )
+ );
+ this.#subscriptions.use(
+ new EventSubscription(
+ networkManager,
+ NetworkManagerEvent.RequestFailed,
+ this.#onRequestFailed.bind(this)
+ )
+ );
+ this.#terminationDeferred = Deferred.create<Error>({
+ timeout: this.#timeout,
+ message: `Navigation timeout of ${this.#timeout} ms exceeded`,
+ });
+
+ this.#checkLifecycleComplete();
+ }
+
+ #onRequest(request: HTTPRequest): void {
+ if (request.frame() !== this.#frame || !request.isNavigationRequest()) {
+ return;
+ }
+ this.#navigationRequest = request;
+ // Resolve previous navigation response in case there are multiple
+ // navigation requests reported by the backend. This generally should not
+ // happen by it looks like it's possible.
+ this.#navigationResponseReceived?.resolve();
+ this.#navigationResponseReceived = Deferred.create();
+ if (request.response() !== null) {
+ this.#navigationResponseReceived?.resolve();
+ }
+ }
+
+ #onRequestFailed(request: HTTPRequest): void {
+ if (this.#navigationRequest?._requestId !== request._requestId) {
+ return;
+ }
+ this.#navigationResponseReceived?.resolve();
+ }
+
+ #onResponse(response: HTTPResponse): void {
+ if (this.#navigationRequest?._requestId !== response.request()._requestId) {
+ return;
+ }
+ this.#navigationResponseReceived?.resolve();
+ }
+
+ #onFrameDetached(frame: Frame): void {
+ if (this.#frame === frame) {
+ this.#terminationDeferred.resolve(
+ new Error('Navigating frame was detached')
+ );
+ return;
+ }
+ this.#checkLifecycleComplete();
+ }
+
+ async navigationResponse(): Promise<HTTPResponse | null> {
+ // Continue with a possibly null response.
+ await this.#navigationResponseReceived?.valueOrThrow();
+ return this.#navigationRequest ? this.#navigationRequest.response() : null;
+ }
+
+ sameDocumentNavigationPromise(): Promise<Error | undefined> {
+ return this.#sameDocumentNavigationDeferred.valueOrThrow();
+ }
+
+ newDocumentNavigationPromise(): Promise<Error | undefined> {
+ return this.#newDocumentNavigationDeferred.valueOrThrow();
+ }
+
+ lifecyclePromise(): Promise<void> {
+ return this.#lifecycleDeferred.valueOrThrow();
+ }
+
+ terminationPromise(): Promise<Error | TimeoutError | undefined> {
+ return this.#terminationDeferred.valueOrThrow();
+ }
+
+ #navigatedWithinDocument(): void {
+ this.#hasSameDocumentNavigation = true;
+ this.#checkLifecycleComplete();
+ }
+
+ #navigated(navigationType: Protocol.Page.NavigationType): void {
+ if (navigationType === 'BackForwardCacheRestore') {
+ return this.#frameSwapped();
+ }
+ this.#checkLifecycleComplete();
+ }
+
+ #frameSwapped(): void {
+ this.#swapped = true;
+ this.#checkLifecycleComplete();
+ }
+
+ #checkLifecycleComplete(): void {
+ // We expect navigation to commit.
+ if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) {
+ return;
+ }
+ this.#lifecycleDeferred.resolve();
+ if (this.#hasSameDocumentNavigation) {
+ this.#sameDocumentNavigationDeferred.resolve(undefined);
+ }
+ if (this.#swapped || this.#frame._loaderId !== this.#initialLoaderId) {
+ this.#newDocumentNavigationDeferred.resolve(undefined);
+ }
+
+ function checkLifecycle(
+ frame: CdpFrame,
+ expectedLifecycle: ProtocolLifeCycleEvent[]
+ ): boolean {
+ for (const event of expectedLifecycle) {
+ if (!frame._lifecycleEvents.has(event)) {
+ return false;
+ }
+ }
+ // TODO(#1): Its possible we don't need this check
+ // CDP provided the correct order for Loading Events
+ // And NetworkIdle is a global state
+ // Consider removing
+ for (const child of frame.childFrames()) {
+ if (
+ child._hasStartedLoading &&
+ !checkLifecycle(child, expectedLifecycle)
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ dispose(): void {
+ this.#subscriptions.dispose();
+ this.#terminationDeferred.resolve(new Error('LifecycleWatcher disposed'));
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkEventManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkEventManager.ts
new file mode 100644
index 0000000000..2aadd21d25
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkEventManager.ts
@@ -0,0 +1,217 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CdpHTTPRequest} from './HTTPRequest.js';
+
+/**
+ * @internal
+ */
+export interface QueuedEventGroup {
+ responseReceivedEvent: Protocol.Network.ResponseReceivedEvent;
+ loadingFinishedEvent?: Protocol.Network.LoadingFinishedEvent;
+ loadingFailedEvent?: Protocol.Network.LoadingFailedEvent;
+}
+
+/**
+ * @internal
+ */
+export type FetchRequestId = string;
+
+/**
+ * @internal
+ */
+export interface RedirectInfo {
+ event: Protocol.Network.RequestWillBeSentEvent;
+ fetchRequestId?: FetchRequestId;
+}
+type RedirectInfoList = RedirectInfo[];
+
+/**
+ * @internal
+ */
+export type NetworkRequestId = string;
+
+/**
+ * Helper class to track network events by request ID
+ *
+ * @internal
+ */
+export class NetworkEventManager {
+ /**
+ * There are four possible orders of events:
+ * A. `_onRequestWillBeSent`
+ * B. `_onRequestWillBeSent`, `_onRequestPaused`
+ * C. `_onRequestPaused`, `_onRequestWillBeSent`
+ * D. `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`,
+ * `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`
+ * (see crbug.com/1196004)
+ *
+ * For `_onRequest` we need the event from `_onRequestWillBeSent` and
+ * optionally the `interceptionId` from `_onRequestPaused`.
+ *
+ * If request interception is disabled, call `_onRequest` once per call to
+ * `_onRequestWillBeSent`.
+ * If request interception is enabled, call `_onRequest` once per call to
+ * `_onRequestPaused` (once per `interceptionId`).
+ *
+ * Events are stored to allow for subsequent events to call `_onRequest`.
+ *
+ * Note that (chains of) redirect requests have the same `requestId` (!) as
+ * the original request. We have to anticipate series of events like these:
+ * A. `_onRequestWillBeSent`,
+ * `_onRequestWillBeSent`, ...
+ * B. `_onRequestWillBeSent`, `_onRequestPaused`,
+ * `_onRequestWillBeSent`, `_onRequestPaused`, ...
+ * C. `_onRequestWillBeSent`, `_onRequestPaused`,
+ * `_onRequestPaused`, `_onRequestWillBeSent`, ...
+ * D. `_onRequestPaused`, `_onRequestWillBeSent`,
+ * `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`,
+ * `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`, ...
+ * (see crbug.com/1196004)
+ */
+ #requestWillBeSentMap = new Map<
+ NetworkRequestId,
+ Protocol.Network.RequestWillBeSentEvent
+ >();
+ #requestPausedMap = new Map<
+ NetworkRequestId,
+ Protocol.Fetch.RequestPausedEvent
+ >();
+ #httpRequestsMap = new Map<NetworkRequestId, CdpHTTPRequest>();
+
+ /*
+ * The below maps are used to reconcile Network.responseReceivedExtraInfo
+ * events with their corresponding request. Each response and redirect
+ * response gets an ExtraInfo event, and we don't know which will come first.
+ * This means that we have to store a Response or an ExtraInfo for each
+ * response, and emit the event when we get both of them. In addition, to
+ * handle redirects, we have to make them Arrays to represent the chain of
+ * events.
+ */
+ #responseReceivedExtraInfoMap = new Map<
+ NetworkRequestId,
+ Protocol.Network.ResponseReceivedExtraInfoEvent[]
+ >();
+ #queuedRedirectInfoMap = new Map<NetworkRequestId, RedirectInfoList>();
+ #queuedEventGroupMap = new Map<NetworkRequestId, QueuedEventGroup>();
+
+ forget(networkRequestId: NetworkRequestId): void {
+ this.#requestWillBeSentMap.delete(networkRequestId);
+ this.#requestPausedMap.delete(networkRequestId);
+ this.#queuedEventGroupMap.delete(networkRequestId);
+ this.#queuedRedirectInfoMap.delete(networkRequestId);
+ this.#responseReceivedExtraInfoMap.delete(networkRequestId);
+ }
+
+ responseExtraInfo(
+ networkRequestId: NetworkRequestId
+ ): Protocol.Network.ResponseReceivedExtraInfoEvent[] {
+ if (!this.#responseReceivedExtraInfoMap.has(networkRequestId)) {
+ this.#responseReceivedExtraInfoMap.set(networkRequestId, []);
+ }
+ return this.#responseReceivedExtraInfoMap.get(
+ networkRequestId
+ ) as Protocol.Network.ResponseReceivedExtraInfoEvent[];
+ }
+
+ private queuedRedirectInfo(fetchRequestId: FetchRequestId): RedirectInfoList {
+ if (!this.#queuedRedirectInfoMap.has(fetchRequestId)) {
+ this.#queuedRedirectInfoMap.set(fetchRequestId, []);
+ }
+ return this.#queuedRedirectInfoMap.get(fetchRequestId) as RedirectInfoList;
+ }
+
+ queueRedirectInfo(
+ fetchRequestId: FetchRequestId,
+ redirectInfo: RedirectInfo
+ ): void {
+ this.queuedRedirectInfo(fetchRequestId).push(redirectInfo);
+ }
+
+ takeQueuedRedirectInfo(
+ fetchRequestId: FetchRequestId
+ ): RedirectInfo | undefined {
+ return this.queuedRedirectInfo(fetchRequestId).shift();
+ }
+
+ inFlightRequestsCount(): number {
+ let inFlightRequestCounter = 0;
+ for (const request of this.#httpRequestsMap.values()) {
+ if (!request.response()) {
+ inFlightRequestCounter++;
+ }
+ }
+ return inFlightRequestCounter;
+ }
+
+ storeRequestWillBeSent(
+ networkRequestId: NetworkRequestId,
+ event: Protocol.Network.RequestWillBeSentEvent
+ ): void {
+ this.#requestWillBeSentMap.set(networkRequestId, event);
+ }
+
+ getRequestWillBeSent(
+ networkRequestId: NetworkRequestId
+ ): Protocol.Network.RequestWillBeSentEvent | undefined {
+ return this.#requestWillBeSentMap.get(networkRequestId);
+ }
+
+ forgetRequestWillBeSent(networkRequestId: NetworkRequestId): void {
+ this.#requestWillBeSentMap.delete(networkRequestId);
+ }
+
+ getRequestPaused(
+ networkRequestId: NetworkRequestId
+ ): Protocol.Fetch.RequestPausedEvent | undefined {
+ return this.#requestPausedMap.get(networkRequestId);
+ }
+
+ forgetRequestPaused(networkRequestId: NetworkRequestId): void {
+ this.#requestPausedMap.delete(networkRequestId);
+ }
+
+ storeRequestPaused(
+ networkRequestId: NetworkRequestId,
+ event: Protocol.Fetch.RequestPausedEvent
+ ): void {
+ this.#requestPausedMap.set(networkRequestId, event);
+ }
+
+ getRequest(networkRequestId: NetworkRequestId): CdpHTTPRequest | undefined {
+ return this.#httpRequestsMap.get(networkRequestId);
+ }
+
+ storeRequest(
+ networkRequestId: NetworkRequestId,
+ request: CdpHTTPRequest
+ ): void {
+ this.#httpRequestsMap.set(networkRequestId, request);
+ }
+
+ forgetRequest(networkRequestId: NetworkRequestId): void {
+ this.#httpRequestsMap.delete(networkRequestId);
+ }
+
+ getQueuedEventGroup(
+ networkRequestId: NetworkRequestId
+ ): QueuedEventGroup | undefined {
+ return this.#queuedEventGroupMap.get(networkRequestId);
+ }
+
+ queueEventGroup(
+ networkRequestId: NetworkRequestId,
+ event: QueuedEventGroup
+ ): void {
+ this.#queuedEventGroupMap.set(networkRequestId, event);
+ }
+
+ forgetQueuedEventGroup(networkRequestId: NetworkRequestId): void {
+ this.#queuedEventGroupMap.delete(networkRequestId);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts
new file mode 100644
index 0000000000..c3e9a8f609
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.test.ts
@@ -0,0 +1,1531 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import type {CDPSessionEvents} from '../api/CDPSession.js';
+import type {HTTPRequest} from '../api/HTTPRequest.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+import {EventEmitter} from '../common/EventEmitter.js';
+import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
+
+import type {CdpFrame} from './Frame.js';
+import {NetworkManager} from './NetworkManager.js';
+
+// TODO: develop a helper to generate fake network events for attributes that
+// are not relevant for the network manager to make tests shorter.
+
+class MockCDPSession extends EventEmitter<CDPSessionEvents> {
+ async send(): Promise<any> {}
+ connection() {
+ return undefined;
+ }
+ async detach() {}
+ id() {
+ return '1';
+ }
+ parentSession() {
+ return undefined;
+ }
+}
+
+describe('NetworkManager', () => {
+ it('should process extra info on multiple redirects', async () => {
+ const mockCDPSession = new MockCDPSession();
+ const manager = new NetworkManager(true, {
+ frame(): CdpFrame | null {
+ return null;
+ },
+ });
+ await manager.addClient(mockCDPSession);
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ loaderId: '7760711DEFCFA23132D98ABA6B4E175C',
+ documentURL: 'http://localhost:8907/redirect/1.html',
+ request: {
+ url: 'http://localhost:8907/redirect/1.html',
+ method: 'GET',
+ headers: {
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 2111.55635,
+ wallTime: 1637315638.473634,
+ initiator: {type: 'other'},
+ redirectHasExtraInfo: false,
+ type: 'Document',
+ frameId: '099A5216AF03AAFEC988F214B024DF08',
+ hasUserGesture: false,
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ associatedCookies: [],
+ headers: {
+ Host: 'localhost:8907',
+ Connection: 'keep-alive',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36',
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+ 'Sec-Fetch-Site': 'none',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-User': '?1',
+ 'Sec-Fetch-Dest': 'document',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ },
+ connectTiming: {requestTime: 2111.557593},
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ blockedCookies: [],
+ headers: {
+ location: '/redirect/2.html',
+ Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=5',
+ 'Transfer-Encoding': 'chunked',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 302,
+ headersText:
+ 'HTTP/1.1 302 Found\r\nlocation: /redirect/2.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n',
+ });
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ loaderId: '7760711DEFCFA23132D98ABA6B4E175C',
+ documentURL: 'http://localhost:8907/redirect/2.html',
+ request: {
+ url: 'http://localhost:8907/redirect/2.html',
+ method: 'GET',
+ headers: {
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 2111.559124,
+ wallTime: 1637315638.47642,
+ initiator: {type: 'other'},
+ redirectHasExtraInfo: true,
+ redirectResponse: {
+ url: 'http://localhost:8907/redirect/1.html',
+ status: 302,
+ statusText: 'Found',
+ headers: {
+ location: '/redirect/2.html',
+ Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=5',
+ 'Transfer-Encoding': 'chunked',
+ },
+ mimeType: '',
+ connectionReused: false,
+ connectionId: 322,
+ remoteIPAddress: '[::1]',
+ remotePort: 8907,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 162,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 2111.557593,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: 0.241,
+ dnsEnd: 0.251,
+ connectStart: 0.251,
+ connectEnd: 0.47,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.537,
+ sendEnd: 0.611,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 0.939,
+ },
+ responseTime: 1.637315638475744e12,
+ protocol: 'http/1.1',
+ securityState: 'secure',
+ },
+ type: 'Document',
+ frameId: '099A5216AF03AAFEC988F214B024DF08',
+ hasUserGesture: false,
+ });
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ associatedCookies: [],
+ headers: {
+ Host: 'localhost:8907',
+ Connection: 'keep-alive',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36',
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+ 'Sec-Fetch-Site': 'none',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-User': '?1',
+ 'Sec-Fetch-Dest': 'document',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ },
+ connectTiming: {requestTime: 2111.559346},
+ });
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ loaderId: '7760711DEFCFA23132D98ABA6B4E175C',
+ documentURL: 'http://localhost:8907/redirect/3.html',
+ request: {
+ url: 'http://localhost:8907/redirect/3.html',
+ method: 'GET',
+ headers: {
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 2111.560249,
+ wallTime: 1637315638.477543,
+ initiator: {type: 'other'},
+ redirectHasExtraInfo: true,
+ redirectResponse: {
+ url: 'http://localhost:8907/redirect/2.html',
+ status: 302,
+ statusText: 'Found',
+ headers: {
+ location: '/redirect/3.html',
+ Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=5',
+ 'Transfer-Encoding': 'chunked',
+ },
+ mimeType: '',
+ connectionReused: true,
+ connectionId: 322,
+ remoteIPAddress: '[::1]',
+ remotePort: 8907,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 162,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 2111.559346,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.15,
+ sendEnd: 0.196,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 0.507,
+ },
+ responseTime: 1.637315638477063e12,
+ protocol: 'http/1.1',
+ securityState: 'secure',
+ },
+ type: 'Document',
+ frameId: '099A5216AF03AAFEC988F214B024DF08',
+ hasUserGesture: false,
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ blockedCookies: [],
+ headers: {
+ location: '/redirect/3.html',
+ Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=5',
+ 'Transfer-Encoding': 'chunked',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 302,
+ headersText:
+ 'HTTP/1.1 302 Found\r\nlocation: /redirect/3.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n',
+ });
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ associatedCookies: [],
+ headers: {
+ Host: 'localhost:8907',
+ Connection: 'keep-alive',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36',
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+ 'Sec-Fetch-Site': 'none',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-User': '?1',
+ 'Sec-Fetch-Dest': 'document',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ },
+ connectTiming: {requestTime: 2111.560482},
+ });
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ loaderId: '7760711DEFCFA23132D98ABA6B4E175C',
+ documentURL: 'http://localhost:8907/empty.html',
+ request: {
+ url: 'http://localhost:8907/empty.html',
+ method: 'GET',
+ headers: {
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 2111.561542,
+ wallTime: 1637315638.478837,
+ initiator: {type: 'other'},
+ redirectHasExtraInfo: true,
+ redirectResponse: {
+ url: 'http://localhost:8907/redirect/3.html',
+ status: 302,
+ statusText: 'Found',
+ headers: {
+ location: 'http://localhost:8907/empty.html',
+ Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=5',
+ 'Transfer-Encoding': 'chunked',
+ },
+ mimeType: '',
+ connectionReused: true,
+ connectionId: 322,
+ remoteIPAddress: '[::1]',
+ remotePort: 8907,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 178,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 2111.560482,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.149,
+ sendEnd: 0.198,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 0.478,
+ },
+ responseTime: 1.637315638478184e12,
+ protocol: 'http/1.1',
+ securityState: 'secure',
+ },
+ type: 'Document',
+ frameId: '099A5216AF03AAFEC988F214B024DF08',
+ hasUserGesture: false,
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ blockedCookies: [],
+ headers: {
+ location: 'http://localhost:8907/empty.html',
+ Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=5',
+ 'Transfer-Encoding': 'chunked',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 302,
+ headersText:
+ 'HTTP/1.1 302 Found\r\nlocation: http://localhost:8907/empty.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n',
+ });
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ associatedCookies: [],
+ headers: {
+ Host: 'localhost:8907',
+ Connection: 'keep-alive',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36',
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+ 'Sec-Fetch-Site': 'none',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-User': '?1',
+ 'Sec-Fetch-Dest': 'document',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ },
+ connectTiming: {requestTime: 2111.561759},
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ blockedCookies: [],
+ headers: {
+ 'Cache-Control': 'no-cache, no-store',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=5',
+ 'Content-Length': '0',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 200,
+ headersText:
+ 'HTTP/1.1 200 OK\r\nCache-Control: no-cache, no-store\r\nContent-Type: text/html; charset=utf-8\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nContent-Length: 0\r\n\r\n',
+ });
+ mockCDPSession.emit('Network.responseReceived', {
+ requestId: '7760711DEFCFA23132D98ABA6B4E175C',
+ loaderId: '7760711DEFCFA23132D98ABA6B4E175C',
+ timestamp: 2111.563565,
+ type: 'Document',
+ response: {
+ url: 'http://localhost:8907/empty.html',
+ status: 200,
+ statusText: 'OK',
+ headers: {
+ 'Cache-Control': 'no-cache, no-store',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Fri, 19 Nov 2021 09:53:58 GMT',
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=5',
+ 'Content-Length': '0',
+ },
+ mimeType: 'text/html',
+ connectionReused: true,
+ connectionId: 322,
+ remoteIPAddress: '[::1]',
+ remotePort: 8907,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 197,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 2111.561759,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.148,
+ sendEnd: 0.19,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 0.925,
+ },
+ responseTime: 1.637315638479928e12,
+ protocol: 'http/1.1',
+ securityState: 'secure',
+ },
+ hasExtraInfo: true,
+ frameId: '099A5216AF03AAFEC988F214B024DF08',
+ });
+ });
+ it(`should handle "double pause" (crbug.com/1196004) Fetch.requestPaused events for the same Network.requestWillBeSent event`, async () => {
+ const mockCDPSession = new MockCDPSession();
+ const manager = new NetworkManager(true, {
+ frame(): CdpFrame | null {
+ return null;
+ },
+ });
+ await manager.addClient(mockCDPSession);
+ await manager.setRequestInterception(true);
+
+ const requests: HTTPRequest[] = [];
+ manager.on(NetworkManagerEvent.Request, async (request: HTTPRequest) => {
+ requests.push(request);
+ await request.continue();
+ });
+
+ /**
+ * This sequence was taken from an actual CDP session produced by the following
+ * test script:
+ *
+ * ```ts
+ * const browser = await puppeteer.launch({headless: false});
+ * const page = await browser.newPage();
+ * await page.setCacheEnabled(false);
+ *
+ * await page.setRequestInterception(true);
+ * page.on('request', interceptedRequest => {
+ * interceptedRequest.continue();
+ * });
+ *
+ * await page.goto('https://www.google.com');
+ * await browser.close();
+ * ```
+ */
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '11ACE9783588040D644B905E8B55285B',
+ loaderId: '11ACE9783588040D644B905E8B55285B',
+ documentURL: 'https://www.google.com/',
+ request: {
+ url: 'https://www.google.com/',
+ method: 'GET',
+ headers: {},
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 224604.980827,
+ wallTime: 1637955746.786191,
+ initiator: {type: 'other'},
+ redirectHasExtraInfo: false,
+ type: 'Document',
+ frameId: '84AC261A351B86932B775B76D1DD79F8',
+ hasUserGesture: false,
+ });
+ mockCDPSession.emit('Fetch.requestPaused', {
+ requestId: 'interception-job-1.0',
+ request: {
+ url: 'https://www.google.com/',
+ method: 'GET',
+ headers: {},
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ },
+ frameId: '84AC261A351B86932B775B76D1DD79F8',
+ resourceType: 'Document',
+ networkId: '11ACE9783588040D644B905E8B55285B',
+ });
+ mockCDPSession.emit('Fetch.requestPaused', {
+ requestId: 'interception-job-2.0',
+ request: {
+ url: 'https://www.google.com/',
+ method: 'GET',
+ headers: {},
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ },
+ frameId: '84AC261A351B86932B775B76D1DD79F8',
+ resourceType: 'Document',
+ networkId: '11ACE9783588040D644B905E8B55285B',
+ });
+
+ expect(requests).toHaveLength(2);
+ });
+ it(`should handle Network.responseReceivedExtraInfo event after Network.responseReceived event (github.com/puppeteer/puppeteer/issues/8234)`, async () => {
+ const mockCDPSession = new MockCDPSession();
+ const manager = new NetworkManager(true, {
+ frame(): CdpFrame | null {
+ return null;
+ },
+ });
+ await manager.addClient(mockCDPSession);
+
+ const requests: HTTPRequest[] = [];
+ manager.on(NetworkManagerEvent.RequestFinished, (request: HTTPRequest) => {
+ requests.push(request);
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '1360.2',
+ loaderId: '9E86B0282CC98B77FB0ABD49156DDFDD',
+ documentURL: 'http://this.is.the.start.page.com/',
+ request: {
+ url: 'http://this.is.a.test.com:1080/test.js',
+ method: 'GET',
+ headers: {
+ 'Accept-Language': 'en-US,en;q=0.9',
+ Referer: 'http://this.is.the.start.page.com/',
+ 'User-Agent':
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'High',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: false,
+ },
+ timestamp: 10959.020087,
+ wallTime: 1649712607.861365,
+ initiator: {
+ type: 'parser',
+ url: 'http://this.is.the.start.page.com/',
+ lineNumber: 9,
+ columnNumber: 80,
+ },
+ redirectHasExtraInfo: false,
+ type: 'Script',
+ frameId: '60E6C35E7E519F28E646056820095498',
+ hasUserGesture: false,
+ });
+ mockCDPSession.emit('Network.responseReceived', {
+ requestId: '1360.2',
+ loaderId: '9E86B0282CC98B77FB0ABD49156DDFDD',
+ timestamp: 10959.042529,
+ type: 'Script',
+ response: {
+ url: 'http://this.is.a.test.com:1080',
+ status: 200,
+ statusText: 'OK',
+ headers: {
+ connection: 'keep-alive',
+ 'content-length': '85862',
+ },
+ mimeType: 'text/plain',
+ connectionReused: false,
+ connectionId: 119,
+ remoteIPAddress: '127.0.0.1',
+ remotePort: 1080,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 66,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 10959.023904,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: 0.328,
+ dnsEnd: 2.183,
+ connectStart: 2.183,
+ connectEnd: 2.798,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 2.982,
+ sendEnd: 3.757,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 16.373,
+ },
+ responseTime: 1649712607880.971,
+ protocol: 'http/1.1',
+ securityState: 'insecure',
+ },
+ hasExtraInfo: true,
+ frameId: '60E6C35E7E519F28E646056820095498',
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '1360.2',
+ blockedCookies: [],
+ headers: {
+ connection: 'keep-alive',
+ 'content-length': '85862',
+ },
+ resourceIPAddressSpace: 'Private',
+ statusCode: 200,
+ headersText:
+ 'HTTP/1.1 200 OK\r\nconnection: keep-alive\r\ncontent-length: 85862\r\n\r\n',
+ });
+ mockCDPSession.emit('Network.loadingFinished', {
+ requestId: '1360.2',
+ timestamp: 10959.060708,
+ encodedDataLength: 85928,
+ });
+
+ expect(requests).toHaveLength(1);
+ });
+
+ it(`should resolve the response once the late responseReceivedExtraInfo event arrives`, async () => {
+ const mockCDPSession = new MockCDPSession();
+ const manager = new NetworkManager(true, {
+ frame(): CdpFrame | null {
+ return null;
+ },
+ });
+ await manager.addClient(mockCDPSession);
+
+ const finishedRequests: HTTPRequest[] = [];
+ const pendingRequests: HTTPRequest[] = [];
+ manager.on(NetworkManagerEvent.RequestFinished, (request: HTTPRequest) => {
+ finishedRequests.push(request);
+ });
+
+ manager.on(NetworkManagerEvent.Request, (request: HTTPRequest) => {
+ pendingRequests.push(request);
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: 'LOADERID',
+ loaderId: 'LOADERID',
+ documentURL: 'http://10.1.0.39:42915/empty.html',
+ request: {
+ url: 'http://10.1.0.39:42915/empty.html',
+ method: 'GET',
+ headers: {
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 671.229856,
+ wallTime: 1660121157.913774,
+ initiator: {type: 'other'},
+ redirectHasExtraInfo: false,
+ type: 'Document',
+ frameId: 'FRAMEID',
+ hasUserGesture: false,
+ });
+
+ mockCDPSession.emit('Network.responseReceived', {
+ requestId: 'LOADERID',
+ loaderId: 'LOADERID',
+ timestamp: 671.236025,
+ type: 'Document',
+ response: {
+ url: 'http://10.1.0.39:42915/empty.html',
+ status: 200,
+ statusText: 'OK',
+ headers: {
+ 'Cache-Control': 'no-cache, no-store',
+ Connection: 'keep-alive',
+ 'Content-Length': '0',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 10 Aug 2022 08:45:57 GMT',
+ 'Keep-Alive': 'timeout=5',
+ },
+ mimeType: 'text/html',
+ connectionReused: true,
+ connectionId: 18,
+ remoteIPAddress: '10.1.0.39',
+ remotePort: 42915,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 197,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 671.232585,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.308,
+ sendEnd: 0.364,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 1.554,
+ },
+ responseTime: 1.660121157917951e12,
+ protocol: 'http/1.1',
+ securityState: 'insecure',
+ },
+ hasExtraInfo: true,
+ frameId: 'FRAMEID',
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: 'LOADERID',
+ associatedCookies: [],
+ headers: {
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+ 'Accept-Encoding': 'gzip, deflate',
+ 'Accept-Language': 'en-US,en;q=0.9',
+ Connection: 'keep-alive',
+ Host: '10.1.0.39:42915',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36',
+ },
+ connectTiming: {requestTime: 671.232585},
+ });
+
+ mockCDPSession.emit('Network.loadingFinished', {
+ requestId: 'LOADERID',
+ timestamp: 671.234448,
+ encodedDataLength: 197,
+ });
+
+ expect(pendingRequests).toHaveLength(1);
+ expect(finishedRequests).toHaveLength(0);
+ expect(pendingRequests[0]!.response()).toEqual(null);
+
+ // The extra info might arrive late.
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: 'LOADERID',
+ blockedCookies: [],
+ headers: {
+ 'Cache-Control': 'no-cache, no-store',
+ Connection: 'keep-alive',
+ 'Content-Length': '0',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 10 Aug 2022 09:04:39 GMT',
+ 'Keep-Alive': 'timeout=5',
+ },
+ resourceIPAddressSpace: 'Private',
+ statusCode: 200,
+ headersText:
+ 'HTTP/1.1 200 OK\\r\\nCache-Control: no-cache, no-store\\r\\nContent-Type: text/html; charset=utf-8\\r\\nDate: Wed, 10 Aug 2022 09:04:39 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nContent-Length: 0\\r\\n\\r\\n',
+ });
+
+ expect(pendingRequests).toHaveLength(1);
+ expect(finishedRequests).toHaveLength(1);
+ expect(pendingRequests[0]!.response()).not.toEqual(null);
+ });
+
+ it(`should send responses for iframe that don't receive loadingFinished event`, async () => {
+ const mockCDPSession = new MockCDPSession();
+ const manager = new NetworkManager(true, {
+ frame(): CdpFrame | null {
+ return null;
+ },
+ });
+ await manager.addClient(mockCDPSession);
+
+ const responses: HTTPResponse[] = [];
+ const requests: HTTPRequest[] = [];
+ manager.on(NetworkManagerEvent.Response, (response: HTTPResponse) => {
+ responses.push(response);
+ });
+
+ manager.on(NetworkManagerEvent.Request, (request: HTTPRequest) => {
+ requests.push(request);
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '94051D839ACF29E53A3D1273FB20B4C4',
+ loaderId: '94051D839ACF29E53A3D1273FB20B4C4',
+ documentURL: 'http://127.0.0.1:54590/empty.html',
+ request: {
+ url: 'http://127.0.0.1:54590/empty.html',
+ method: 'GET',
+ headers: {
+ Referer: 'http://localhost:54590/',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/105.0.5173.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: false,
+ },
+ timestamp: 504903.99901,
+ wallTime: 1660125092.026021,
+ initiator: {
+ type: 'script',
+ stack: {
+ callFrames: [
+ {
+ functionName: 'navigateFrame',
+ scriptId: '8',
+ url: 'pptr://__puppeteer_evaluation_script__',
+ lineNumber: 2,
+ columnNumber: 18,
+ },
+ ],
+ },
+ },
+ redirectHasExtraInfo: false,
+ type: 'Document',
+ frameId: '07D18B8630A8161C72B6079B74123D60',
+ hasUserGesture: true,
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: '94051D839ACF29E53A3D1273FB20B4C4',
+ associatedCookies: [],
+ headers: {
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ Connection: 'keep-alive',
+ Host: '127.0.0.1:54590',
+ Referer: 'http://localhost:54590/',
+ 'Sec-Fetch-Dest': 'iframe',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-Site': 'cross-site',
+ 'Sec-Fetch-User': '?1',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/105.0.5173.0 Safari/537.36',
+ },
+ connectTiming: {requestTime: 504904.000422},
+ clientSecurityState: {
+ initiatorIsSecureContext: true,
+ initiatorIPAddressSpace: 'Local',
+ privateNetworkRequestPolicy: 'Allow',
+ },
+ });
+
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '94051D839ACF29E53A3D1273FB20B4C4',
+ blockedCookies: [],
+ headers: {
+ 'Cache-Control': 'no-cache, no-store',
+ Connection: 'keep-alive',
+ 'Content-Length': '0',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 10 Aug 2022 09:51:32 GMT',
+ 'Keep-Alive': 'timeout=5',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 200,
+ headersText:
+ 'HTTP/1.1 200 OK\r\nCache-Control: no-cache, no-store\r\nContent-Type: text/html; charset=utf-8\r\nDate: Wed, 10 Aug 2022 09:51:32 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nContent-Length: 0\r\n\r\n',
+ });
+
+ mockCDPSession.emit('Network.responseReceived', {
+ requestId: '94051D839ACF29E53A3D1273FB20B4C4',
+ loaderId: '94051D839ACF29E53A3D1273FB20B4C4',
+ timestamp: 504904.00338,
+ type: 'Document',
+ response: {
+ url: 'http://127.0.0.1:54590/empty.html',
+ status: 200,
+ statusText: 'OK',
+ headers: {
+ 'Cache-Control': 'no-cache, no-store',
+ Connection: 'keep-alive',
+ 'Content-Length': '0',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 10 Aug 2022 09:51:32 GMT',
+ 'Keep-Alive': 'timeout=5',
+ },
+ mimeType: 'text/html',
+ connectionReused: true,
+ connectionId: 13,
+ remoteIPAddress: '127.0.0.1',
+ remotePort: 54590,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 197,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 504904.000422,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.338,
+ sendEnd: 0.413,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 1.877,
+ },
+ responseTime: 1.660125092029241e12,
+ protocol: 'http/1.1',
+ securityState: 'secure',
+ },
+ hasExtraInfo: true,
+ frameId: '07D18B8630A8161C72B6079B74123D60',
+ });
+
+ expect(requests).toHaveLength(1);
+ expect(responses).toHaveLength(1);
+ expect(requests[0]!.response()).not.toEqual(null);
+ });
+
+ it(`should send responses for iframe that don't receive loadingFinished event`, async () => {
+ const mockCDPSession = new MockCDPSession();
+ const manager = new NetworkManager(true, {
+ frame(): CdpFrame | null {
+ return null;
+ },
+ });
+ await manager.addClient(mockCDPSession);
+
+ const responses: HTTPResponse[] = [];
+ const requests: HTTPRequest[] = [];
+ manager.on(NetworkManagerEvent.Response, (response: HTTPResponse) => {
+ responses.push(response);
+ });
+
+ manager.on(NetworkManagerEvent.Request, (request: HTTPRequest) => {
+ requests.push(request);
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: 'E18BEB94B486CA8771F9AFA2030FEA37',
+ loaderId: 'E18BEB94B486CA8771F9AFA2030FEA37',
+ documentURL: 'http://localhost:56295/empty.html',
+ request: {
+ url: 'http://localhost:56295/empty.html',
+ method: 'GET',
+ headers: {
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/105.0.5173.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 510294.105656,
+ wallTime: 1660130482.230591,
+ initiator: {type: 'other'},
+ redirectHasExtraInfo: false,
+ type: 'Document',
+ frameId: 'F9C89A517341F1EFFE63310141630189',
+ hasUserGesture: false,
+ });
+ mockCDPSession.emit('Network.responseReceived', {
+ requestId: 'E18BEB94B486CA8771F9AFA2030FEA37',
+ loaderId: 'E18BEB94B486CA8771F9AFA2030FEA37',
+ timestamp: 510294.119816,
+ type: 'Document',
+ response: {
+ url: 'http://localhost:56295/empty.html',
+ status: 200,
+ statusText: 'OK',
+ headers: {
+ 'Cache-Control': 'no-cache, no-store',
+ Connection: 'keep-alive',
+ 'Content-Length': '0',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 10 Aug 2022 11:21:22 GMT',
+ 'Keep-Alive': 'timeout=5',
+ },
+ mimeType: 'text/html',
+ connectionReused: true,
+ connectionId: 13,
+ remoteIPAddress: '[::1]',
+ remotePort: 56295,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 197,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 510294.106734,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 2.195,
+ sendEnd: 2.29,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 6.493,
+ },
+ responseTime: 1.660130482238109e12,
+ protocol: 'http/1.1',
+ securityState: 'secure',
+ },
+ hasExtraInfo: true,
+ frameId: 'F9C89A517341F1EFFE63310141630189',
+ });
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: 'E18BEB94B486CA8771F9AFA2030FEA37',
+ associatedCookies: [],
+ headers: {
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ Connection: 'keep-alive',
+ Host: 'localhost:56295',
+ 'Sec-Fetch-Dest': 'document',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-Site': 'none',
+ 'Sec-Fetch-User': '?1',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/105.0.5173.0 Safari/537.36',
+ },
+ connectTiming: {requestTime: 510294.106734},
+ });
+ mockCDPSession.emit('Network.loadingFinished', {
+ requestId: 'E18BEB94B486CA8771F9AFA2030FEA37',
+ timestamp: 510294.113383,
+ encodedDataLength: 197,
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: 'E18BEB94B486CA8771F9AFA2030FEA37',
+ blockedCookies: [],
+ headers: {
+ 'Cache-Control': 'no-cache, no-store',
+ Connection: 'keep-alive',
+ 'Content-Length': '0',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 10 Aug 2022 11:21:22 GMT',
+ 'Keep-Alive': 'timeout=5',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 200,
+ headersText:
+ 'HTTP/1.1 200 OK\r\nCache-Control: no-cache, no-store\r\nContent-Type: text/html; charset=utf-8\r\nDate: Wed, 10 Aug 2022 11:21:22 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nContent-Length: 0\r\n\r\n',
+ });
+
+ expect(requests).toHaveLength(1);
+ expect(responses).toHaveLength(1);
+ expect(requests[0]!.response()).not.toEqual(null);
+ });
+
+ it(`should handle cached redirects`, async () => {
+ const mockCDPSession = new MockCDPSession();
+ const manager = new NetworkManager(true, {
+ frame(): CdpFrame | null {
+ return null;
+ },
+ });
+ await manager.addClient(mockCDPSession);
+
+ const responses: HTTPResponse[] = [];
+ const requests: HTTPRequest[] = [];
+ manager.on(NetworkManagerEvent.Response, (response: HTTPResponse) => {
+ responses.push(response);
+ });
+
+ manager.on(NetworkManagerEvent.Request, (request: HTTPRequest) => {
+ requests.push(request);
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '6D76C8ACAECE880C722FA515AD380015',
+ loaderId: '6D76C8ACAECE880C722FA515AD380015',
+ documentURL: 'http://localhost:3000/',
+ request: {
+ url: 'http://localhost:3000/',
+ method: 'GET',
+ headers: {
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 31949.95878,
+ wallTime: 1680698353.570949,
+ initiator: {type: 'other'},
+ redirectHasExtraInfo: false,
+ type: 'Document',
+ frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4',
+ hasUserGesture: false,
+ });
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: '6D76C8ACAECE880C722FA515AD380015',
+ associatedCookies: [],
+ headers: {
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
+ Connection: 'keep-alive',
+ Host: 'localhost:3000',
+ 'Sec-Fetch-Dest': 'document',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-Site': 'none',
+ 'Sec-Fetch-User': '?1',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
+ 'sec-ch-ua-mobile': '?0',
+ },
+ connectTiming: {requestTime: 31949.959838},
+ siteHasCookieInOtherPartition: false,
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '6D76C8ACAECE880C722FA515AD380015',
+ blockedCookies: [],
+ headers: {
+ 'Cache-Control': 'max-age=5',
+ Connection: 'keep-alive',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 05 Apr 2023 12:39:13 GMT',
+ 'Keep-Alive': 'timeout=5',
+ 'Transfer-Encoding': 'chunked',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 200,
+ headersText:
+ 'HTTP/1.1 200 OK\\r\\nContent-Type: text/html; charset=utf-8\\r\\nCache-Control: max-age=5\\r\\nDate: Wed, 05 Apr 2023 12:39:13 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n',
+ cookiePartitionKey: 'http://localhost',
+ cookiePartitionKeyOpaque: false,
+ });
+
+ mockCDPSession.emit('Network.responseReceived', {
+ requestId: '6D76C8ACAECE880C722FA515AD380015',
+ loaderId: '6D76C8ACAECE880C722FA515AD380015',
+ timestamp: 31949.965149,
+ type: 'Document',
+ response: {
+ url: 'http://localhost:3000/',
+ status: 200,
+ statusText: 'OK',
+ headers: {
+ 'Cache-Control': 'max-age=5',
+ Connection: 'keep-alive',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 05 Apr 2023 12:39:13 GMT',
+ 'Keep-Alive': 'timeout=5',
+ 'Transfer-Encoding': 'chunked',
+ },
+ mimeType: 'text/html',
+ connectionReused: true,
+ connectionId: 34,
+ remoteIPAddress: '127.0.0.1',
+ remotePort: 3000,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 197,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 31949.959838,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.613,
+ sendEnd: 0.665,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 3.619,
+ },
+ responseTime: 1.680698353573552e12,
+ protocol: 'http/1.1',
+ alternateProtocolUsage: 'unspecifiedReason',
+ securityState: 'secure',
+ },
+ hasExtraInfo: true,
+ frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4',
+ });
+ mockCDPSession.emit('Network.loadingFinished', {
+ requestId: '6D76C8ACAECE880C722FA515AD380015',
+ timestamp: 31949.963861,
+ encodedDataLength: 847,
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ loaderId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ documentURL: 'http://localhost:3000/redirect',
+ request: {
+ url: 'http://localhost:3000/redirect',
+ method: 'GET',
+ headers: {
+ Referer: 'http://localhost:3000/',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
+ 'sec-ch-ua-mobile': '?0',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 31949.982895,
+ wallTime: 1680698353.595079,
+ initiator: {
+ type: 'script',
+ stack: {
+ callFrames: [
+ {
+ functionName: '',
+ scriptId: '5',
+ url: 'http://localhost:3000/',
+ lineNumber: 8,
+ columnNumber: 32,
+ },
+ ],
+ },
+ },
+ redirectHasExtraInfo: false,
+ type: 'Document',
+ frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4',
+ hasUserGesture: false,
+ });
+
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ associatedCookies: [],
+ headers: {
+ Accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
+ Connection: 'keep-alive',
+ Host: 'localhost:3000',
+ Referer: 'http://localhost:3000/',
+ 'Sec-Fetch-Dest': 'document',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-Site': 'same-origin',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
+ 'sec-ch-ua-mobile': '?0',
+ },
+ connectTiming: {requestTime: 31949.983605},
+ siteHasCookieInOtherPartition: false,
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ blockedCookies: [],
+ headers: {
+ Connection: 'keep-alive',
+ Date: 'Wed, 05 Apr 2023 12:39:13 GMT',
+ 'Keep-Alive': 'timeout=5',
+ Location: 'http://localhost:3000/#from-redirect',
+ 'Transfer-Encoding': 'chunked',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 302,
+ headersText:
+ 'HTTP/1.1 302 Found\\r\\nLocation: http://localhost:3000/#from-redirect\\r\\nDate: Wed, 05 Apr 2023 12:39:13 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n',
+ cookiePartitionKey: 'http://localhost',
+ cookiePartitionKeyOpaque: false,
+ });
+ mockCDPSession.emit('Network.requestWillBeSent', {
+ requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ loaderId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ documentURL: 'http://localhost:3000/',
+ request: {
+ url: 'http://localhost:3000/',
+ urlFragment: '#from-redirect',
+ method: 'GET',
+ headers: {
+ Referer: 'http://localhost:3000/',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
+ 'sec-ch-ua-mobile': '?0',
+ },
+ mixedContentType: 'none',
+ initialPriority: 'VeryHigh',
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ isSameSite: true,
+ },
+ timestamp: 31949.988506,
+ wallTime: 1680698353.60069,
+ initiator: {
+ type: 'script',
+ stack: {
+ callFrames: [
+ {
+ functionName: '',
+ scriptId: '5',
+ url: 'http://localhost:3000/',
+ lineNumber: 8,
+ columnNumber: 32,
+ },
+ ],
+ },
+ },
+ redirectHasExtraInfo: true,
+ redirectResponse: {
+ url: 'http://localhost:3000/redirect',
+ status: 302,
+ statusText: 'Found',
+ headers: {
+ Connection: 'keep-alive',
+ Date: 'Wed, 05 Apr 2023 12:39:13 GMT',
+ 'Keep-Alive': 'timeout=5',
+ Location: 'http://localhost:3000/#from-redirect',
+ 'Transfer-Encoding': 'chunked',
+ },
+ mimeType: '',
+ connectionReused: true,
+ connectionId: 34,
+ remoteIPAddress: '127.0.0.1',
+ remotePort: 3000,
+ fromDiskCache: false,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 182,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 31949.983605,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.364,
+ sendEnd: 0.401,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 4.085,
+ },
+ responseTime: 1.680698353596548e12,
+ protocol: 'http/1.1',
+ alternateProtocolUsage: 'unspecifiedReason',
+ securityState: 'secure',
+ },
+ type: 'Document',
+ frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4',
+ hasUserGesture: false,
+ });
+ mockCDPSession.emit('Network.requestWillBeSentExtraInfo', {
+ requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ associatedCookies: [],
+ headers: {},
+ connectTiming: {requestTime: 31949.988855},
+ siteHasCookieInOtherPartition: false,
+ });
+
+ mockCDPSession.emit('Network.responseReceived', {
+ requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ loaderId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ timestamp: 31949.991319,
+ type: 'Document',
+ response: {
+ url: 'http://localhost:3000/',
+ status: 200,
+ statusText: 'OK',
+ headers: {
+ 'Cache-Control': 'max-age=5',
+ 'Content-Type': 'text/html; charset=utf-8',
+ Date: 'Wed, 05 Apr 2023 12:39:13 GMT',
+ },
+ mimeType: 'text/html',
+ connectionReused: false,
+ connectionId: 0,
+ remoteIPAddress: '127.0.0.1',
+ remotePort: 3000,
+ fromDiskCache: true,
+ fromServiceWorker: false,
+ fromPrefetchCache: false,
+ encodedDataLength: 0,
+ timing: {
+ receiveHeadersStart: 0,
+ requestTime: 31949.988855,
+ proxyStart: -1,
+ proxyEnd: -1,
+ dnsStart: -1,
+ dnsEnd: -1,
+ connectStart: -1,
+ connectEnd: -1,
+ sslStart: -1,
+ sslEnd: -1,
+ workerStart: -1,
+ workerReady: -1,
+ workerFetchStart: -1,
+ workerRespondWithSettled: -1,
+ sendStart: 0.069,
+ sendEnd: 0.069,
+ pushStart: 0,
+ pushEnd: 0,
+ receiveHeadersEnd: 0.321,
+ },
+ responseTime: 1.680698353573552e12,
+ protocol: 'http/1.1',
+ alternateProtocolUsage: 'unspecifiedReason',
+ securityState: 'secure',
+ },
+ hasExtraInfo: true,
+ frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4',
+ });
+ mockCDPSession.emit('Network.responseReceivedExtraInfo', {
+ requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ blockedCookies: [],
+ headers: {
+ Connection: 'keep-alive',
+ Date: 'Wed, 05 Apr 2023 12:39:13 GMT',
+ 'Keep-Alive': 'timeout=5',
+ Location: 'http://localhost:3000/#from-redirect',
+ 'Transfer-Encoding': 'chunked',
+ },
+ resourceIPAddressSpace: 'Local',
+ statusCode: 302,
+ headersText:
+ 'HTTP/1.1 302 Found\\r\\nLocation: http://localhost:3000/#from-redirect\\r\\nDate: Wed, 05 Apr 2023 12:39:13 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n',
+ cookiePartitionKey: 'http://localhost',
+ cookiePartitionKeyOpaque: false,
+ });
+ mockCDPSession.emit('Network.loadingFinished', {
+ requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105',
+ timestamp: 31949.989412,
+ encodedDataLength: 0,
+ });
+ expect(
+ responses.map(r => {
+ return r.status();
+ })
+ ).toEqual([200, 302, 200]);
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts
new file mode 100644
index 0000000000..8b24b9a748
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/NetworkManager.ts
@@ -0,0 +1,710 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
+import type {Frame} from '../api/Frame.js';
+import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
+import {
+ NetworkManagerEvent,
+ type NetworkManagerEvents,
+} from '../common/NetworkManagerEvents.js';
+import {debugError, isString} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {DisposableStack} from '../util/disposable.js';
+
+import {CdpHTTPRequest} from './HTTPRequest.js';
+import {CdpHTTPResponse} from './HTTPResponse.js';
+import {
+ NetworkEventManager,
+ type FetchRequestId,
+} from './NetworkEventManager.js';
+
+/**
+ * @public
+ */
+export interface Credentials {
+ username: string;
+ password: string;
+}
+
+/**
+ * @public
+ */
+export interface NetworkConditions {
+ // Download speed (bytes/s)
+ download: number;
+ // Upload speed (bytes/s)
+ upload: number;
+ // Latency (ms)
+ latency: number;
+}
+
+/**
+ * @public
+ */
+export interface InternalNetworkConditions extends NetworkConditions {
+ offline: boolean;
+}
+
+/**
+ * @internal
+ */
+export interface FrameProvider {
+ frame(id: string): Frame | null;
+}
+
+/**
+ * @internal
+ */
+export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
+ #ignoreHTTPSErrors: boolean;
+ #frameManager: FrameProvider;
+ #networkEventManager = new NetworkEventManager();
+ #extraHTTPHeaders?: Record<string, string>;
+ #credentials?: Credentials;
+ #attemptedAuthentications = new Set<string>();
+ #userRequestInterceptionEnabled = false;
+ #protocolRequestInterceptionEnabled = false;
+ #userCacheDisabled?: boolean;
+ #emulatedNetworkConditions?: InternalNetworkConditions;
+ #userAgent?: string;
+ #userAgentMetadata?: Protocol.Emulation.UserAgentMetadata;
+
+ readonly #handlers = [
+ ['Fetch.requestPaused', this.#onRequestPaused],
+ ['Fetch.authRequired', this.#onAuthRequired],
+ ['Network.requestWillBeSent', this.#onRequestWillBeSent],
+ ['Network.requestServedFromCache', this.#onRequestServedFromCache],
+ ['Network.responseReceived', this.#onResponseReceived],
+ ['Network.loadingFinished', this.#onLoadingFinished],
+ ['Network.loadingFailed', this.#onLoadingFailed],
+ ['Network.responseReceivedExtraInfo', this.#onResponseReceivedExtraInfo],
+ [CDPSessionEvent.Disconnected, this.#removeClient],
+ ] as const;
+
+ #clients = new Map<CDPSession, DisposableStack>();
+
+ constructor(ignoreHTTPSErrors: boolean, frameManager: FrameProvider) {
+ super();
+ this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
+ this.#frameManager = frameManager;
+ }
+
+ async addClient(client: CDPSession): Promise<void> {
+ if (this.#clients.has(client)) {
+ return;
+ }
+ const subscriptions = new DisposableStack();
+ this.#clients.set(client, subscriptions);
+ for (const [event, handler] of this.#handlers) {
+ subscriptions.use(
+ // TODO: Remove any here.
+ new EventSubscription(client, event, (arg: any) => {
+ return handler.bind(this)(client, arg);
+ })
+ );
+ }
+ await Promise.all([
+ this.#ignoreHTTPSErrors
+ ? client.send('Security.setIgnoreCertificateErrors', {
+ ignore: true,
+ })
+ : null,
+ client.send('Network.enable'),
+ this.#applyExtraHTTPHeaders(client),
+ this.#applyNetworkConditions(client),
+ this.#applyProtocolCacheDisabled(client),
+ this.#applyProtocolRequestInterception(client),
+ this.#applyUserAgent(client),
+ ]);
+ }
+
+ async #removeClient(client: CDPSession) {
+ this.#clients.get(client)?.dispose();
+ this.#clients.delete(client);
+ }
+
+ async authenticate(credentials?: Credentials): Promise<void> {
+ this.#credentials = credentials;
+ const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
+ if (enabled === this.#protocolRequestInterceptionEnabled) {
+ return;
+ }
+ this.#protocolRequestInterceptionEnabled = enabled;
+ await this.#applyToAllClients(
+ this.#applyProtocolRequestInterception.bind(this)
+ );
+ }
+
+ async setExtraHTTPHeaders(
+ extraHTTPHeaders: Record<string, string>
+ ): Promise<void> {
+ this.#extraHTTPHeaders = {};
+ for (const key of Object.keys(extraHTTPHeaders)) {
+ const value = extraHTTPHeaders[key];
+ assert(
+ isString(value),
+ `Expected value of header "${key}" to be String, but "${typeof value}" is found.`
+ );
+ this.#extraHTTPHeaders[key.toLowerCase()] = value;
+ }
+
+ await this.#applyToAllClients(this.#applyExtraHTTPHeaders.bind(this));
+ }
+
+ async #applyExtraHTTPHeaders(client: CDPSession) {
+ if (this.#extraHTTPHeaders === undefined) {
+ return;
+ }
+ await client.send('Network.setExtraHTTPHeaders', {
+ headers: this.#extraHTTPHeaders,
+ });
+ }
+
+ extraHTTPHeaders(): Record<string, string> {
+ return Object.assign({}, this.#extraHTTPHeaders);
+ }
+
+ inFlightRequestsCount(): number {
+ return this.#networkEventManager.inFlightRequestsCount();
+ }
+
+ async setOfflineMode(value: boolean): Promise<void> {
+ if (!this.#emulatedNetworkConditions) {
+ this.#emulatedNetworkConditions = {
+ offline: false,
+ upload: -1,
+ download: -1,
+ latency: 0,
+ };
+ }
+ this.#emulatedNetworkConditions.offline = value;
+ await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
+ }
+
+ async emulateNetworkConditions(
+ networkConditions: NetworkConditions | null
+ ): Promise<void> {
+ if (!this.#emulatedNetworkConditions) {
+ this.#emulatedNetworkConditions = {
+ offline: false,
+ upload: -1,
+ download: -1,
+ latency: 0,
+ };
+ }
+ this.#emulatedNetworkConditions.upload = networkConditions
+ ? networkConditions.upload
+ : -1;
+ this.#emulatedNetworkConditions.download = networkConditions
+ ? networkConditions.download
+ : -1;
+ this.#emulatedNetworkConditions.latency = networkConditions
+ ? networkConditions.latency
+ : 0;
+
+ await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
+ }
+
+ async #applyToAllClients(fn: (client: CDPSession) => Promise<unknown>) {
+ await Promise.all(
+ Array.from(this.#clients.keys()).map(client => {
+ return fn(client);
+ })
+ );
+ }
+
+ async #applyNetworkConditions(client: CDPSession): Promise<void> {
+ if (this.#emulatedNetworkConditions === undefined) {
+ return;
+ }
+ await client.send('Network.emulateNetworkConditions', {
+ offline: this.#emulatedNetworkConditions.offline,
+ latency: this.#emulatedNetworkConditions.latency,
+ uploadThroughput: this.#emulatedNetworkConditions.upload,
+ downloadThroughput: this.#emulatedNetworkConditions.download,
+ });
+ }
+
+ async setUserAgent(
+ userAgent: string,
+ userAgentMetadata?: Protocol.Emulation.UserAgentMetadata
+ ): Promise<void> {
+ this.#userAgent = userAgent;
+ this.#userAgentMetadata = userAgentMetadata;
+ await this.#applyToAllClients(this.#applyUserAgent.bind(this));
+ }
+
+ async #applyUserAgent(client: CDPSession) {
+ if (this.#userAgent === undefined) {
+ return;
+ }
+ await client.send('Network.setUserAgentOverride', {
+ userAgent: this.#userAgent,
+ userAgentMetadata: this.#userAgentMetadata,
+ });
+ }
+
+ async setCacheEnabled(enabled: boolean): Promise<void> {
+ this.#userCacheDisabled = !enabled;
+ await this.#applyToAllClients(this.#applyProtocolCacheDisabled.bind(this));
+ }
+
+ async setRequestInterception(value: boolean): Promise<void> {
+ this.#userRequestInterceptionEnabled = value;
+ const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
+ if (enabled === this.#protocolRequestInterceptionEnabled) {
+ return;
+ }
+ this.#protocolRequestInterceptionEnabled = enabled;
+ await this.#applyToAllClients(
+ this.#applyProtocolRequestInterception.bind(this)
+ );
+ }
+
+ async #applyProtocolRequestInterception(client: CDPSession): Promise<void> {
+ if (this.#userCacheDisabled === undefined) {
+ this.#userCacheDisabled = false;
+ }
+ if (this.#protocolRequestInterceptionEnabled) {
+ await Promise.all([
+ this.#applyProtocolCacheDisabled(client),
+ client.send('Fetch.enable', {
+ handleAuthRequests: true,
+ patterns: [{urlPattern: '*'}],
+ }),
+ ]);
+ } else {
+ await Promise.all([
+ this.#applyProtocolCacheDisabled(client),
+ client.send('Fetch.disable'),
+ ]);
+ }
+ }
+
+ async #applyProtocolCacheDisabled(client: CDPSession): Promise<void> {
+ if (this.#userCacheDisabled === undefined) {
+ return;
+ }
+ await client.send('Network.setCacheDisabled', {
+ cacheDisabled: this.#userCacheDisabled,
+ });
+ }
+
+ #onRequestWillBeSent(
+ client: CDPSession,
+ event: Protocol.Network.RequestWillBeSentEvent
+ ): void {
+ // Request interception doesn't happen for data URLs with Network Service.
+ if (
+ this.#userRequestInterceptionEnabled &&
+ !event.request.url.startsWith('data:')
+ ) {
+ const {requestId: networkRequestId} = event;
+
+ this.#networkEventManager.storeRequestWillBeSent(networkRequestId, event);
+
+ /**
+ * CDP may have sent a Fetch.requestPaused event already. Check for it.
+ */
+ const requestPausedEvent =
+ this.#networkEventManager.getRequestPaused(networkRequestId);
+ if (requestPausedEvent) {
+ const {requestId: fetchRequestId} = requestPausedEvent;
+ this.#patchRequestEventHeaders(event, requestPausedEvent);
+ this.#onRequest(client, event, fetchRequestId);
+ this.#networkEventManager.forgetRequestPaused(networkRequestId);
+ }
+
+ return;
+ }
+ this.#onRequest(client, event, undefined);
+ }
+
+ #onAuthRequired(
+ client: CDPSession,
+ event: Protocol.Fetch.AuthRequiredEvent
+ ): void {
+ let response: Protocol.Fetch.AuthChallengeResponse['response'] = 'Default';
+ if (this.#attemptedAuthentications.has(event.requestId)) {
+ response = 'CancelAuth';
+ } else if (this.#credentials) {
+ response = 'ProvideCredentials';
+ this.#attemptedAuthentications.add(event.requestId);
+ }
+ const {username, password} = this.#credentials || {
+ username: undefined,
+ password: undefined,
+ };
+ client
+ .send('Fetch.continueWithAuth', {
+ requestId: event.requestId,
+ authChallengeResponse: {response, username, password},
+ })
+ .catch(debugError);
+ }
+
+ /**
+ * CDP may send a Fetch.requestPaused without or before a
+ * Network.requestWillBeSent
+ *
+ * CDP may send multiple Fetch.requestPaused
+ * for the same Network.requestWillBeSent.
+ */
+ #onRequestPaused(
+ client: CDPSession,
+ event: Protocol.Fetch.RequestPausedEvent
+ ): void {
+ if (
+ !this.#userRequestInterceptionEnabled &&
+ this.#protocolRequestInterceptionEnabled
+ ) {
+ client
+ .send('Fetch.continueRequest', {
+ requestId: event.requestId,
+ })
+ .catch(debugError);
+ }
+
+ const {networkId: networkRequestId, requestId: fetchRequestId} = event;
+
+ if (!networkRequestId) {
+ this.#onRequestWithoutNetworkInstrumentation(client, event);
+ return;
+ }
+
+ const requestWillBeSentEvent = (() => {
+ const requestWillBeSentEvent =
+ this.#networkEventManager.getRequestWillBeSent(networkRequestId);
+
+ // redirect requests have the same `requestId`,
+ if (
+ requestWillBeSentEvent &&
+ (requestWillBeSentEvent.request.url !== event.request.url ||
+ requestWillBeSentEvent.request.method !== event.request.method)
+ ) {
+ this.#networkEventManager.forgetRequestWillBeSent(networkRequestId);
+ return;
+ }
+ return requestWillBeSentEvent;
+ })();
+
+ if (requestWillBeSentEvent) {
+ this.#patchRequestEventHeaders(requestWillBeSentEvent, event);
+ this.#onRequest(client, requestWillBeSentEvent, fetchRequestId);
+ } else {
+ this.#networkEventManager.storeRequestPaused(networkRequestId, event);
+ }
+ }
+
+ #patchRequestEventHeaders(
+ requestWillBeSentEvent: Protocol.Network.RequestWillBeSentEvent,
+ requestPausedEvent: Protocol.Fetch.RequestPausedEvent
+ ): void {
+ requestWillBeSentEvent.request.headers = {
+ ...requestWillBeSentEvent.request.headers,
+ // includes extra headers, like: Accept, Origin
+ ...requestPausedEvent.request.headers,
+ };
+ }
+
+ #onRequestWithoutNetworkInstrumentation(
+ client: CDPSession,
+ event: Protocol.Fetch.RequestPausedEvent
+ ): void {
+ // If an event has no networkId it should not have any network events. We
+ // still want to dispatch it for the interception by the user.
+ const frame = event.frameId
+ ? this.#frameManager.frame(event.frameId)
+ : null;
+
+ const request = new CdpHTTPRequest(
+ client,
+ frame,
+ event.requestId,
+ this.#userRequestInterceptionEnabled,
+ event,
+ []
+ );
+ this.emit(NetworkManagerEvent.Request, request);
+ void request.finalizeInterceptions();
+ }
+
+ #onRequest(
+ client: CDPSession,
+ event: Protocol.Network.RequestWillBeSentEvent,
+ fetchRequestId?: FetchRequestId
+ ): void {
+ let redirectChain: CdpHTTPRequest[] = [];
+ if (event.redirectResponse) {
+ // We want to emit a response and requestfinished for the
+ // redirectResponse, but we can't do so unless we have a
+ // responseExtraInfo ready to pair it up with. If we don't have any
+ // responseExtraInfos saved in our queue, they we have to wait until
+ // the next one to emit response and requestfinished, *and* we should
+ // also wait to emit this Request too because it should come after the
+ // response/requestfinished.
+ let redirectResponseExtraInfo = null;
+ if (event.redirectHasExtraInfo) {
+ redirectResponseExtraInfo = this.#networkEventManager
+ .responseExtraInfo(event.requestId)
+ .shift();
+ if (!redirectResponseExtraInfo) {
+ this.#networkEventManager.queueRedirectInfo(event.requestId, {
+ event,
+ fetchRequestId,
+ });
+ return;
+ }
+ }
+
+ const request = this.#networkEventManager.getRequest(event.requestId);
+ // If we connect late to the target, we could have missed the
+ // requestWillBeSent event.
+ if (request) {
+ this.#handleRequestRedirect(
+ client,
+ request,
+ event.redirectResponse,
+ redirectResponseExtraInfo
+ );
+ redirectChain = request._redirectChain;
+ }
+ }
+ const frame = event.frameId
+ ? this.#frameManager.frame(event.frameId)
+ : null;
+
+ const request = new CdpHTTPRequest(
+ client,
+ frame,
+ fetchRequestId,
+ this.#userRequestInterceptionEnabled,
+ event,
+ redirectChain
+ );
+ this.#networkEventManager.storeRequest(event.requestId, request);
+ this.emit(NetworkManagerEvent.Request, request);
+ void request.finalizeInterceptions();
+ }
+
+ #onRequestServedFromCache(
+ _client: CDPSession,
+ event: Protocol.Network.RequestServedFromCacheEvent
+ ): void {
+ const request = this.#networkEventManager.getRequest(event.requestId);
+ if (request) {
+ request._fromMemoryCache = true;
+ }
+ this.emit(NetworkManagerEvent.RequestServedFromCache, request);
+ }
+
+ #handleRequestRedirect(
+ client: CDPSession,
+ request: CdpHTTPRequest,
+ responsePayload: Protocol.Network.Response,
+ extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
+ ): void {
+ const response = new CdpHTTPResponse(
+ client,
+ request,
+ responsePayload,
+ extraInfo
+ );
+ request._response = response;
+ request._redirectChain.push(request);
+ response._resolveBody(
+ new Error('Response body is unavailable for redirect responses')
+ );
+ this.#forgetRequest(request, false);
+ this.emit(NetworkManagerEvent.Response, response);
+ this.emit(NetworkManagerEvent.RequestFinished, request);
+ }
+
+ #emitResponseEvent(
+ client: CDPSession,
+ responseReceived: Protocol.Network.ResponseReceivedEvent,
+ extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
+ ): void {
+ const request = this.#networkEventManager.getRequest(
+ responseReceived.requestId
+ );
+ // FileUpload sends a response without a matching request.
+ if (!request) {
+ return;
+ }
+
+ const extraInfos = this.#networkEventManager.responseExtraInfo(
+ responseReceived.requestId
+ );
+ if (extraInfos.length) {
+ debugError(
+ new Error(
+ 'Unexpected extraInfo events for request ' +
+ responseReceived.requestId
+ )
+ );
+ }
+
+ // Chromium sends wrong extraInfo events for responses served from cache.
+ // See https://github.com/puppeteer/puppeteer/issues/9965 and
+ // https://crbug.com/1340398.
+ if (responseReceived.response.fromDiskCache) {
+ extraInfo = null;
+ }
+
+ const response = new CdpHTTPResponse(
+ client,
+ request,
+ responseReceived.response,
+ extraInfo
+ );
+ request._response = response;
+ this.emit(NetworkManagerEvent.Response, response);
+ }
+
+ #onResponseReceived(
+ client: CDPSession,
+ event: Protocol.Network.ResponseReceivedEvent
+ ): void {
+ const request = this.#networkEventManager.getRequest(event.requestId);
+ let extraInfo = null;
+ if (request && !request._fromMemoryCache && event.hasExtraInfo) {
+ extraInfo = this.#networkEventManager
+ .responseExtraInfo(event.requestId)
+ .shift();
+ if (!extraInfo) {
+ // Wait until we get the corresponding ExtraInfo event.
+ this.#networkEventManager.queueEventGroup(event.requestId, {
+ responseReceivedEvent: event,
+ });
+ return;
+ }
+ }
+ this.#emitResponseEvent(client, event, extraInfo);
+ }
+
+ #onResponseReceivedExtraInfo(
+ client: CDPSession,
+ event: Protocol.Network.ResponseReceivedExtraInfoEvent
+ ): void {
+ // We may have skipped a redirect response/request pair due to waiting for
+ // this ExtraInfo event. If so, continue that work now that we have the
+ // request.
+ const redirectInfo = this.#networkEventManager.takeQueuedRedirectInfo(
+ event.requestId
+ );
+ if (redirectInfo) {
+ this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
+ this.#onRequest(client, redirectInfo.event, redirectInfo.fetchRequestId);
+ return;
+ }
+
+ // We may have skipped response and loading events because we didn't have
+ // this ExtraInfo event yet. If so, emit those events now.
+ const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
+ event.requestId
+ );
+ if (queuedEvents) {
+ this.#networkEventManager.forgetQueuedEventGroup(event.requestId);
+ this.#emitResponseEvent(
+ client,
+ queuedEvents.responseReceivedEvent,
+ event
+ );
+ if (queuedEvents.loadingFinishedEvent) {
+ this.#emitLoadingFinished(queuedEvents.loadingFinishedEvent);
+ }
+ if (queuedEvents.loadingFailedEvent) {
+ this.#emitLoadingFailed(queuedEvents.loadingFailedEvent);
+ }
+ return;
+ }
+
+ // Wait until we get another event that can use this ExtraInfo event.
+ this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
+ }
+
+ #forgetRequest(request: CdpHTTPRequest, events: boolean): void {
+ const requestId = request._requestId;
+ const interceptionId = request._interceptionId;
+
+ this.#networkEventManager.forgetRequest(requestId);
+ interceptionId !== undefined &&
+ this.#attemptedAuthentications.delete(interceptionId);
+
+ if (events) {
+ this.#networkEventManager.forget(requestId);
+ }
+ }
+
+ #onLoadingFinished(
+ _client: CDPSession,
+ event: Protocol.Network.LoadingFinishedEvent
+ ): void {
+ // If the response event for this request is still waiting on a
+ // corresponding ExtraInfo event, then wait to emit this event too.
+ const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
+ event.requestId
+ );
+ if (queuedEvents) {
+ queuedEvents.loadingFinishedEvent = event;
+ } else {
+ this.#emitLoadingFinished(event);
+ }
+ }
+
+ #emitLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
+ const request = this.#networkEventManager.getRequest(event.requestId);
+ // For certain requestIds we never receive requestWillBeSent event.
+ // @see https://crbug.com/750469
+ if (!request) {
+ return;
+ }
+
+ // Under certain conditions we never get the Network.responseReceived
+ // event from protocol. @see https://crbug.com/883475
+ if (request.response()) {
+ request.response()?._resolveBody();
+ }
+ this.#forgetRequest(request, true);
+ this.emit(NetworkManagerEvent.RequestFinished, request);
+ }
+
+ #onLoadingFailed(
+ _client: CDPSession,
+ event: Protocol.Network.LoadingFailedEvent
+ ): void {
+ // If the response event for this request is still waiting on a
+ // corresponding ExtraInfo event, then wait to emit this event too.
+ const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
+ event.requestId
+ );
+ if (queuedEvents) {
+ queuedEvents.loadingFailedEvent = event;
+ } else {
+ this.#emitLoadingFailed(event);
+ }
+ }
+
+ #emitLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
+ const request = this.#networkEventManager.getRequest(event.requestId);
+ // For certain requestIds we never receive requestWillBeSent event.
+ // @see https://crbug.com/750469
+ if (!request) {
+ return;
+ }
+ request._failureText = event.errorText;
+ const response = request.response();
+ if (response) {
+ response._resolveBody();
+ }
+ this.#forgetRequest(request, true);
+ this.emit(NetworkManagerEvent.RequestFailed, request);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts
new file mode 100644
index 0000000000..491637f0ea
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Page.ts
@@ -0,0 +1,1249 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Readable} from 'stream';
+
+import type {Protocol} from 'devtools-protocol';
+
+import {firstValueFrom, from, raceWith} from '../../third_party/rxjs/rxjs.js';
+import type {Browser} from '../api/Browser.js';
+import type {BrowserContext} from '../api/BrowserContext.js';
+import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
+import type {ElementHandle} from '../api/ElementHandle.js';
+import type {Frame, WaitForOptions} from '../api/Frame.js';
+import type {HTTPRequest} from '../api/HTTPRequest.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+import type {JSHandle} from '../api/JSHandle.js';
+import {
+ Page,
+ PageEvent,
+ type GeolocationOptions,
+ type MediaFeature,
+ type Metrics,
+ type NewDocumentScriptEvaluation,
+ type ScreenshotClip,
+ type ScreenshotOptions,
+ type WaitTimeoutOptions,
+} from '../api/Page.js';
+import {
+ ConsoleMessage,
+ type ConsoleMessageType,
+} from '../common/ConsoleMessage.js';
+import {TargetCloseError} from '../common/Errors.js';
+import {FileChooser} from '../common/FileChooser.js';
+import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
+import type {PDFOptions} from '../common/PDFOptions.js';
+import type {BindingPayload, HandleFor} from '../common/types.js';
+import {
+ debugError,
+ evaluationString,
+ getReadableAsBuffer,
+ getReadableFromProtocolStream,
+ parsePDFOptions,
+ timeout,
+ validateDialogType,
+} from '../common/util.js';
+import type {Viewport} from '../common/Viewport.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+import {AsyncDisposableStack} from '../util/disposable.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import {Accessibility} from './Accessibility.js';
+import {Binding} from './Binding.js';
+import {CdpCDPSession} from './CDPSession.js';
+import {isTargetClosedError} from './Connection.js';
+import {Coverage} from './Coverage.js';
+import type {DeviceRequestPrompt} from './DeviceRequestPrompt.js';
+import {CdpDialog} from './Dialog.js';
+import {EmulationManager} from './EmulationManager.js';
+import {createCdpHandle} from './ExecutionContext.js';
+import {FirefoxTargetManager} from './FirefoxTargetManager.js';
+import type {CdpFrame} from './Frame.js';
+import {FrameManager} from './FrameManager.js';
+import {FrameManagerEvent} from './FrameManagerEvents.js';
+import {CdpKeyboard, CdpMouse, CdpTouchscreen} from './Input.js';
+import {MAIN_WORLD} from './IsolatedWorlds.js';
+import {releaseObject} from './JSHandle.js';
+import type {Credentials, NetworkConditions} from './NetworkManager.js';
+import type {CdpTarget} from './Target.js';
+import type {TargetManager} from './TargetManager.js';
+import {TargetManagerEvent} from './TargetManager.js';
+import {Tracing} from './Tracing.js';
+import {
+ createClientError,
+ pageBindingInitString,
+ valueFromRemoteObject,
+} from './utils.js';
+import {CdpWebWorker} from './WebWorker.js';
+
+/**
+ * @internal
+ */
+export class CdpPage extends Page {
+ static async _create(
+ client: CDPSession,
+ target: CdpTarget,
+ ignoreHTTPSErrors: boolean,
+ defaultViewport: Viewport | null
+ ): Promise<CdpPage> {
+ const page = new CdpPage(client, target, ignoreHTTPSErrors);
+ await page.#initialize();
+ if (defaultViewport) {
+ try {
+ await page.setViewport(defaultViewport);
+ } catch (err) {
+ if (isErrorLike(err) && isTargetClosedError(err)) {
+ debugError(err);
+ } else {
+ throw err;
+ }
+ }
+ }
+ return page;
+ }
+
+ #closed = false;
+ readonly #targetManager: TargetManager;
+
+ #primaryTargetClient: CDPSession;
+ #primaryTarget: CdpTarget;
+ #tabTargetClient: CDPSession;
+ #tabTarget: CdpTarget;
+ #keyboard: CdpKeyboard;
+ #mouse: CdpMouse;
+ #touchscreen: CdpTouchscreen;
+ #accessibility: Accessibility;
+ #frameManager: FrameManager;
+ #emulationManager: EmulationManager;
+ #tracing: Tracing;
+ #bindings = new Map<string, Binding>();
+ #exposedFunctions = new Map<string, string>();
+ #coverage: Coverage;
+ #viewport: Viewport | null;
+ #workers = new Map<string, CdpWebWorker>();
+ #fileChooserDeferreds = new Set<Deferred<FileChooser>>();
+ #sessionCloseDeferred = Deferred.create<never, TargetCloseError>();
+ #serviceWorkerBypassed = false;
+ #userDragInterceptionEnabled = false;
+
+ readonly #frameManagerHandlers = [
+ [
+ FrameManagerEvent.FrameAttached,
+ (frame: CdpFrame) => {
+ this.emit(PageEvent.FrameAttached, frame);
+ },
+ ],
+ [
+ FrameManagerEvent.FrameDetached,
+ (frame: CdpFrame) => {
+ this.emit(PageEvent.FrameDetached, frame);
+ },
+ ],
+ [
+ FrameManagerEvent.FrameNavigated,
+ (frame: CdpFrame) => {
+ this.emit(PageEvent.FrameNavigated, frame);
+ },
+ ],
+ ] as const;
+
+ readonly #networkManagerHandlers = [
+ [
+ NetworkManagerEvent.Request,
+ (request: HTTPRequest) => {
+ this.emit(PageEvent.Request, request);
+ },
+ ],
+ [
+ NetworkManagerEvent.RequestServedFromCache,
+ (request: HTTPRequest) => {
+ this.emit(PageEvent.RequestServedFromCache, request);
+ },
+ ],
+ [
+ NetworkManagerEvent.Response,
+ (response: HTTPResponse) => {
+ this.emit(PageEvent.Response, response);
+ },
+ ],
+ [
+ NetworkManagerEvent.RequestFailed,
+ (request: HTTPRequest) => {
+ this.emit(PageEvent.RequestFailed, request);
+ },
+ ],
+ [
+ NetworkManagerEvent.RequestFinished,
+ (request: HTTPRequest) => {
+ this.emit(PageEvent.RequestFinished, request);
+ },
+ ],
+ ] as const;
+
+ readonly #sessionHandlers = [
+ [
+ CDPSessionEvent.Disconnected,
+ () => {
+ this.#sessionCloseDeferred.reject(
+ new TargetCloseError('Target closed')
+ );
+ },
+ ],
+ [
+ 'Page.domContentEventFired',
+ () => {
+ return this.emit(PageEvent.DOMContentLoaded, undefined);
+ },
+ ],
+ [
+ 'Page.loadEventFired',
+ () => {
+ return this.emit(PageEvent.Load, undefined);
+ },
+ ],
+ ['Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this)],
+ ['Runtime.bindingCalled', this.#onBindingCalled.bind(this)],
+ ['Page.javascriptDialogOpening', this.#onDialog.bind(this)],
+ ['Runtime.exceptionThrown', this.#handleException.bind(this)],
+ ['Inspector.targetCrashed', this.#onTargetCrashed.bind(this)],
+ ['Performance.metrics', this.#emitMetrics.bind(this)],
+ ['Log.entryAdded', this.#onLogEntryAdded.bind(this)],
+ ['Page.fileChooserOpened', this.#onFileChooser.bind(this)],
+ ] as const;
+
+ constructor(
+ client: CDPSession,
+ target: CdpTarget,
+ ignoreHTTPSErrors: boolean
+ ) {
+ super();
+ this.#primaryTargetClient = client;
+ this.#tabTargetClient = client.parentSession()!;
+ assert(this.#tabTargetClient, 'Tab target session is not defined.');
+ this.#tabTarget = (this.#tabTargetClient as CdpCDPSession)._target();
+ assert(this.#tabTarget, 'Tab target is not defined.');
+ this.#primaryTarget = target;
+ this.#targetManager = target._targetManager();
+ this.#keyboard = new CdpKeyboard(client);
+ this.#mouse = new CdpMouse(client, this.#keyboard);
+ this.#touchscreen = new CdpTouchscreen(client, this.#keyboard);
+ this.#accessibility = new Accessibility(client);
+ this.#frameManager = new FrameManager(
+ client,
+ this,
+ ignoreHTTPSErrors,
+ this._timeoutSettings
+ );
+ this.#emulationManager = new EmulationManager(client);
+ this.#tracing = new Tracing(client);
+ this.#coverage = new Coverage(client);
+ this.#viewport = null;
+
+ for (const [eventName, handler] of this.#frameManagerHandlers) {
+ this.#frameManager.on(eventName, handler);
+ }
+
+ for (const [eventName, handler] of this.#networkManagerHandlers) {
+ // TODO: Remove any.
+ this.#frameManager.networkManager.on(eventName, handler as any);
+ }
+
+ this.#tabTargetClient.on(
+ CDPSessionEvent.Swapped,
+ this.#onActivation.bind(this)
+ );
+
+ this.#tabTargetClient.on(
+ CDPSessionEvent.Ready,
+ this.#onSecondaryTarget.bind(this)
+ );
+
+ this.#targetManager.on(
+ TargetManagerEvent.TargetGone,
+ this.#onDetachedFromTarget
+ );
+
+ this.#tabTarget._isClosedDeferred
+ .valueOrThrow()
+ .then(() => {
+ this.#targetManager.off(
+ TargetManagerEvent.TargetGone,
+ this.#onDetachedFromTarget
+ );
+
+ this.emit(PageEvent.Close, undefined);
+ this.#closed = true;
+ })
+ .catch(debugError);
+
+ this.#setupPrimaryTargetListeners();
+ }
+
+ async #onActivation(newSession: CDPSession): Promise<void> {
+ this.#primaryTargetClient = newSession;
+ assert(
+ this.#primaryTargetClient instanceof CdpCDPSession,
+ 'CDPSession is not instance of CDPSessionImpl'
+ );
+ this.#primaryTarget = this.#primaryTargetClient._target();
+ assert(this.#primaryTarget, 'Missing target on swap');
+ this.#keyboard.updateClient(newSession);
+ this.#mouse.updateClient(newSession);
+ this.#touchscreen.updateClient(newSession);
+ this.#accessibility.updateClient(newSession);
+ this.#emulationManager.updateClient(newSession);
+ this.#tracing.updateClient(newSession);
+ this.#coverage.updateClient(newSession);
+ await this.#frameManager.swapFrameTree(newSession);
+ this.#setupPrimaryTargetListeners();
+ }
+
+ async #onSecondaryTarget(session: CDPSession): Promise<void> {
+ assert(session instanceof CdpCDPSession);
+ if (session._target()._subtype() !== 'prerender') {
+ return;
+ }
+ this.#frameManager.registerSpeculativeSession(session).catch(debugError);
+ this.#emulationManager
+ .registerSpeculativeSession(session)
+ .catch(debugError);
+ }
+
+ /**
+ * Sets up listeners for the primary target. The primary target can change
+ * during a navigation to a prerended page.
+ */
+ #setupPrimaryTargetListeners() {
+ this.#primaryTargetClient.on(
+ CDPSessionEvent.Ready,
+ this.#onAttachedToTarget
+ );
+
+ for (const [eventName, handler] of this.#sessionHandlers) {
+ // TODO: Remove any.
+ this.#primaryTargetClient.on(eventName, handler as any);
+ }
+ }
+
+ #onDetachedFromTarget = (target: CdpTarget) => {
+ const sessionId = target._session()?.id();
+ const worker = this.#workers.get(sessionId!);
+ if (!worker) {
+ return;
+ }
+ this.#workers.delete(sessionId!);
+ this.emit(PageEvent.WorkerDestroyed, worker);
+ };
+
+ #onAttachedToTarget = (session: CDPSession) => {
+ assert(session instanceof CdpCDPSession);
+ this.#frameManager.onAttachedToTarget(session._target());
+ if (session._target()._getTargetInfo().type === 'worker') {
+ const worker = new CdpWebWorker(
+ session,
+ session._target().url(),
+ this.#addConsoleMessage.bind(this),
+ this.#handleException.bind(this)
+ );
+ this.#workers.set(session.id(), worker);
+ this.emit(PageEvent.WorkerCreated, worker);
+ }
+ session.on(CDPSessionEvent.Ready, this.#onAttachedToTarget);
+ };
+
+ async #initialize(): Promise<void> {
+ try {
+ await Promise.all([
+ this.#frameManager.initialize(this.#primaryTargetClient),
+ this.#primaryTargetClient.send('Performance.enable'),
+ this.#primaryTargetClient.send('Log.enable'),
+ ]);
+ } catch (err) {
+ if (isErrorLike(err) && isTargetClosedError(err)) {
+ debugError(err);
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ async #onFileChooser(
+ event: Protocol.Page.FileChooserOpenedEvent
+ ): Promise<void> {
+ if (!this.#fileChooserDeferreds.size) {
+ return;
+ }
+
+ const frame = this.#frameManager.frame(event.frameId);
+ assert(frame, 'This should never happen.');
+
+ // This is guaranteed to be an HTMLInputElement handle by the event.
+ using handle = (await frame.worlds[MAIN_WORLD].adoptBackendNode(
+ event.backendNodeId
+ )) as ElementHandle<HTMLInputElement>;
+
+ const fileChooser = new FileChooser(handle.move(), event);
+ for (const promise of this.#fileChooserDeferreds) {
+ promise.resolve(fileChooser);
+ }
+ this.#fileChooserDeferreds.clear();
+ }
+
+ _client(): CDPSession {
+ return this.#primaryTargetClient;
+ }
+
+ override isServiceWorkerBypassed(): boolean {
+ return this.#serviceWorkerBypassed;
+ }
+
+ override isDragInterceptionEnabled(): boolean {
+ return this.#userDragInterceptionEnabled;
+ }
+
+ override isJavaScriptEnabled(): boolean {
+ return this.#emulationManager.javascriptEnabled;
+ }
+
+ override async waitForFileChooser(
+ options: WaitTimeoutOptions = {}
+ ): Promise<FileChooser> {
+ const needsEnable = this.#fileChooserDeferreds.size === 0;
+ const {timeout = this._timeoutSettings.timeout()} = options;
+ const deferred = Deferred.create<FileChooser>({
+ message: `Waiting for \`FileChooser\` failed: ${timeout}ms exceeded`,
+ timeout,
+ });
+ this.#fileChooserDeferreds.add(deferred);
+ let enablePromise: Promise<void> | undefined;
+ if (needsEnable) {
+ enablePromise = this.#primaryTargetClient.send(
+ 'Page.setInterceptFileChooserDialog',
+ {
+ enabled: true,
+ }
+ );
+ }
+ try {
+ const [result] = await Promise.all([
+ deferred.valueOrThrow(),
+ enablePromise,
+ ]);
+ return result;
+ } catch (error) {
+ this.#fileChooserDeferreds.delete(deferred);
+ throw error;
+ }
+ }
+
+ override async setGeolocation(options: GeolocationOptions): Promise<void> {
+ return await this.#emulationManager.setGeolocation(options);
+ }
+
+ override target(): CdpTarget {
+ return this.#primaryTarget;
+ }
+
+ override browser(): Browser {
+ return this.#primaryTarget.browser();
+ }
+
+ override browserContext(): BrowserContext {
+ return this.#primaryTarget.browserContext();
+ }
+
+ #onTargetCrashed(): void {
+ this.emit(PageEvent.Error, new Error('Page crashed!'));
+ }
+
+ #onLogEntryAdded(event: Protocol.Log.EntryAddedEvent): void {
+ const {level, text, args, source, url, lineNumber} = event.entry;
+ if (args) {
+ args.map(arg => {
+ void releaseObject(this.#primaryTargetClient, arg);
+ });
+ }
+ if (source !== 'worker') {
+ this.emit(
+ PageEvent.Console,
+ new ConsoleMessage(level, text, [], [{url, lineNumber}])
+ );
+ }
+ }
+
+ override mainFrame(): CdpFrame {
+ return this.#frameManager.mainFrame();
+ }
+
+ override get keyboard(): CdpKeyboard {
+ return this.#keyboard;
+ }
+
+ override get touchscreen(): CdpTouchscreen {
+ return this.#touchscreen;
+ }
+
+ override get coverage(): Coverage {
+ return this.#coverage;
+ }
+
+ override get tracing(): Tracing {
+ return this.#tracing;
+ }
+
+ override get accessibility(): Accessibility {
+ return this.#accessibility;
+ }
+
+ override frames(): Frame[] {
+ return this.#frameManager.frames();
+ }
+
+ override workers(): CdpWebWorker[] {
+ return Array.from(this.#workers.values());
+ }
+
+ override async setRequestInterception(value: boolean): Promise<void> {
+ return await this.#frameManager.networkManager.setRequestInterception(
+ value
+ );
+ }
+
+ override async setBypassServiceWorker(bypass: boolean): Promise<void> {
+ this.#serviceWorkerBypassed = bypass;
+ return await this.#primaryTargetClient.send(
+ 'Network.setBypassServiceWorker',
+ {bypass}
+ );
+ }
+
+ override async setDragInterception(enabled: boolean): Promise<void> {
+ this.#userDragInterceptionEnabled = enabled;
+ return await this.#primaryTargetClient.send('Input.setInterceptDrags', {
+ enabled,
+ });
+ }
+
+ override async setOfflineMode(enabled: boolean): Promise<void> {
+ return await this.#frameManager.networkManager.setOfflineMode(enabled);
+ }
+
+ override async emulateNetworkConditions(
+ networkConditions: NetworkConditions | null
+ ): Promise<void> {
+ return await this.#frameManager.networkManager.emulateNetworkConditions(
+ networkConditions
+ );
+ }
+
+ override setDefaultNavigationTimeout(timeout: number): void {
+ this._timeoutSettings.setDefaultNavigationTimeout(timeout);
+ }
+
+ override setDefaultTimeout(timeout: number): void {
+ this._timeoutSettings.setDefaultTimeout(timeout);
+ }
+
+ override getDefaultTimeout(): number {
+ return this._timeoutSettings.timeout();
+ }
+
+ override async queryObjects<Prototype>(
+ prototypeHandle: JSHandle<Prototype>
+ ): Promise<JSHandle<Prototype[]>> {
+ assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
+ assert(
+ prototypeHandle.id,
+ 'Prototype JSHandle must not be referencing primitive value'
+ );
+ const response = await this.mainFrame().client.send(
+ 'Runtime.queryObjects',
+ {
+ prototypeObjectId: prototypeHandle.id,
+ }
+ );
+ return createCdpHandle(
+ this.mainFrame().mainRealm(),
+ response.objects
+ ) as HandleFor<Prototype[]>;
+ }
+
+ override async cookies(
+ ...urls: string[]
+ ): Promise<Protocol.Network.Cookie[]> {
+ const originalCookies = (
+ await this.#primaryTargetClient.send('Network.getCookies', {
+ urls: urls.length ? urls : [this.url()],
+ })
+ ).cookies;
+
+ const unsupportedCookieAttributes = ['priority'];
+ const filterUnsupportedAttributes = (
+ cookie: Protocol.Network.Cookie
+ ): Protocol.Network.Cookie => {
+ for (const attr of unsupportedCookieAttributes) {
+ delete (cookie as unknown as Record<string, unknown>)[attr];
+ }
+ return cookie;
+ };
+ return originalCookies.map(filterUnsupportedAttributes);
+ }
+
+ override async deleteCookie(
+ ...cookies: Protocol.Network.DeleteCookiesRequest[]
+ ): Promise<void> {
+ const pageURL = this.url();
+ for (const cookie of cookies) {
+ const item = Object.assign({}, cookie);
+ if (!cookie.url && pageURL.startsWith('http')) {
+ item.url = pageURL;
+ }
+ await this.#primaryTargetClient.send('Network.deleteCookies', item);
+ }
+ }
+
+ override async setCookie(
+ ...cookies: Protocol.Network.CookieParam[]
+ ): Promise<void> {
+ const pageURL = this.url();
+ const startsWithHTTP = pageURL.startsWith('http');
+ const items = cookies.map(cookie => {
+ const item = Object.assign({}, cookie);
+ if (!item.url && startsWithHTTP) {
+ item.url = pageURL;
+ }
+ assert(
+ item.url !== 'about:blank',
+ `Blank page can not have cookie "${item.name}"`
+ );
+ assert(
+ !String.prototype.startsWith.call(item.url || '', 'data:'),
+ `Data URL page can not have cookie "${item.name}"`
+ );
+ return item;
+ });
+ await this.deleteCookie(...items);
+ if (items.length) {
+ await this.#primaryTargetClient.send('Network.setCookies', {
+ cookies: items,
+ });
+ }
+ }
+
+ override async exposeFunction(
+ name: string,
+ pptrFunction: Function | {default: Function}
+ ): Promise<void> {
+ if (this.#bindings.has(name)) {
+ throw new Error(
+ `Failed to add page binding with name ${name}: window['${name}'] already exists!`
+ );
+ }
+
+ let binding: Binding;
+ switch (typeof pptrFunction) {
+ case 'function':
+ binding = new Binding(
+ name,
+ pptrFunction as (...args: unknown[]) => unknown
+ );
+ break;
+ default:
+ binding = new Binding(
+ name,
+ pptrFunction.default as (...args: unknown[]) => unknown
+ );
+ break;
+ }
+
+ this.#bindings.set(name, binding);
+
+ const expression = pageBindingInitString('exposedFun', name);
+ await this.#primaryTargetClient.send('Runtime.addBinding', {name});
+ // TODO: investigate this as it appears to only apply to the main frame and
+ // local subframes instead of the entire frame tree (including future
+ // frame).
+ const {identifier} = await this.#primaryTargetClient.send(
+ 'Page.addScriptToEvaluateOnNewDocument',
+ {
+ source: expression,
+ }
+ );
+
+ this.#exposedFunctions.set(name, identifier);
+
+ await Promise.all(
+ this.frames().map(frame => {
+ // If a frame has not started loading, it might never start. Rely on
+ // addScriptToEvaluateOnNewDocument in that case.
+ if (frame !== this.mainFrame() && !frame._hasStartedLoading) {
+ return;
+ }
+ return frame.evaluate(expression).catch(debugError);
+ })
+ );
+ }
+
+ override async removeExposedFunction(name: string): Promise<void> {
+ const exposedFun = this.#exposedFunctions.get(name);
+ if (!exposedFun) {
+ throw new Error(
+ `Failed to remove page binding with name ${name}: window['${name}'] does not exists!`
+ );
+ }
+
+ await this.#primaryTargetClient.send('Runtime.removeBinding', {name});
+ await this.removeScriptToEvaluateOnNewDocument(exposedFun);
+
+ await Promise.all(
+ this.frames().map(frame => {
+ // If a frame has not started loading, it might never start. Rely on
+ // addScriptToEvaluateOnNewDocument in that case.
+ if (frame !== this.mainFrame() && !frame._hasStartedLoading) {
+ return;
+ }
+ return frame
+ .evaluate(name => {
+ // Removes the dangling Puppeteer binding wrapper.
+ // @ts-expect-error: In a different context.
+ globalThis[name] = undefined;
+ }, name)
+ .catch(debugError);
+ })
+ );
+
+ this.#exposedFunctions.delete(name);
+ this.#bindings.delete(name);
+ }
+
+ override async authenticate(credentials: Credentials): Promise<void> {
+ return await this.#frameManager.networkManager.authenticate(credentials);
+ }
+
+ override async setExtraHTTPHeaders(
+ headers: Record<string, string>
+ ): Promise<void> {
+ return await this.#frameManager.networkManager.setExtraHTTPHeaders(headers);
+ }
+
+ override async setUserAgent(
+ userAgent: string,
+ userAgentMetadata?: Protocol.Emulation.UserAgentMetadata
+ ): Promise<void> {
+ return await this.#frameManager.networkManager.setUserAgent(
+ userAgent,
+ userAgentMetadata
+ );
+ }
+
+ override async metrics(): Promise<Metrics> {
+ const response = await this.#primaryTargetClient.send(
+ 'Performance.getMetrics'
+ );
+ return this.#buildMetricsObject(response.metrics);
+ }
+
+ #emitMetrics(event: Protocol.Performance.MetricsEvent): void {
+ this.emit(PageEvent.Metrics, {
+ title: event.title,
+ metrics: this.#buildMetricsObject(event.metrics),
+ });
+ }
+
+ #buildMetricsObject(metrics?: Protocol.Performance.Metric[]): Metrics {
+ const result: Record<
+ Protocol.Performance.Metric['name'],
+ Protocol.Performance.Metric['value']
+ > = {};
+ for (const metric of metrics || []) {
+ if (supportedMetrics.has(metric.name)) {
+ result[metric.name] = metric.value;
+ }
+ }
+ return result;
+ }
+
+ #handleException(exception: Protocol.Runtime.ExceptionThrownEvent): void {
+ this.emit(
+ PageEvent.PageError,
+ createClientError(exception.exceptionDetails)
+ );
+ }
+
+ async #onConsoleAPI(
+ event: Protocol.Runtime.ConsoleAPICalledEvent
+ ): Promise<void> {
+ if (event.executionContextId === 0) {
+ // DevTools protocol stores the last 1000 console messages. These
+ // messages are always reported even for removed execution contexts. In
+ // this case, they are marked with executionContextId = 0 and are
+ // reported upon enabling Runtime agent.
+ //
+ // Ignore these messages since:
+ // - there's no execution context we can use to operate with message
+ // arguments
+ // - these messages are reported before Puppeteer clients can subscribe
+ // to the 'console'
+ // page event.
+ //
+ // @see https://github.com/puppeteer/puppeteer/issues/3865
+ return;
+ }
+ const context = this.#frameManager.getExecutionContextById(
+ event.executionContextId,
+ this.#primaryTargetClient
+ );
+ if (!context) {
+ debugError(
+ new Error(
+ `ExecutionContext not found for a console message: ${JSON.stringify(
+ event
+ )}`
+ )
+ );
+ return;
+ }
+ const values = event.args.map(arg => {
+ return createCdpHandle(context._world, arg);
+ });
+ this.#addConsoleMessage(event.type, values, event.stackTrace);
+ }
+
+ async #onBindingCalled(
+ event: Protocol.Runtime.BindingCalledEvent
+ ): Promise<void> {
+ let payload: BindingPayload;
+ try {
+ payload = JSON.parse(event.payload);
+ } catch {
+ // The binding was either called by something in the page or it was
+ // called before our wrapper was initialized.
+ return;
+ }
+ const {type, name, seq, args, isTrivial} = payload;
+ if (type !== 'exposedFun') {
+ return;
+ }
+
+ const context = this.#frameManager.executionContextById(
+ event.executionContextId,
+ this.#primaryTargetClient
+ );
+ if (!context) {
+ return;
+ }
+
+ const binding = this.#bindings.get(name);
+ await binding?.run(context, seq, args, isTrivial);
+ }
+
+ #addConsoleMessage(
+ eventType: ConsoleMessageType,
+ args: JSHandle[],
+ stackTrace?: Protocol.Runtime.StackTrace
+ ): void {
+ if (!this.listenerCount(PageEvent.Console)) {
+ args.forEach(arg => {
+ return arg.dispose();
+ });
+ return;
+ }
+ const textTokens = [];
+ // eslint-disable-next-line max-len -- The comment is long.
+ // eslint-disable-next-line rulesdir/use-using -- These are not owned by this function.
+ for (const arg of args) {
+ const remoteObject = arg.remoteObject();
+ if (remoteObject.objectId) {
+ textTokens.push(arg.toString());
+ } else {
+ textTokens.push(valueFromRemoteObject(remoteObject));
+ }
+ }
+ const stackTraceLocations = [];
+ if (stackTrace) {
+ for (const callFrame of stackTrace.callFrames) {
+ stackTraceLocations.push({
+ url: callFrame.url,
+ lineNumber: callFrame.lineNumber,
+ columnNumber: callFrame.columnNumber,
+ });
+ }
+ }
+ const message = new ConsoleMessage(
+ eventType,
+ textTokens.join(' '),
+ args,
+ stackTraceLocations
+ );
+ this.emit(PageEvent.Console, message);
+ }
+
+ #onDialog(event: Protocol.Page.JavascriptDialogOpeningEvent): void {
+ const type = validateDialogType(event.type);
+ const dialog = new CdpDialog(
+ this.#primaryTargetClient,
+ type,
+ event.message,
+ event.defaultPrompt
+ );
+ this.emit(PageEvent.Dialog, dialog);
+ }
+
+ override async reload(
+ options?: WaitForOptions
+ ): Promise<HTTPResponse | null> {
+ const [result] = await Promise.all([
+ this.waitForNavigation(options),
+ this.#primaryTargetClient.send('Page.reload'),
+ ]);
+
+ return result;
+ }
+
+ override async createCDPSession(): Promise<CDPSession> {
+ return await this.target().createCDPSession();
+ }
+
+ override async goBack(
+ options: WaitForOptions = {}
+ ): Promise<HTTPResponse | null> {
+ return await this.#go(-1, options);
+ }
+
+ override async goForward(
+ options: WaitForOptions = {}
+ ): Promise<HTTPResponse | null> {
+ return await this.#go(+1, options);
+ }
+
+ async #go(
+ delta: number,
+ options: WaitForOptions
+ ): Promise<HTTPResponse | null> {
+ const history = await this.#primaryTargetClient.send(
+ 'Page.getNavigationHistory'
+ );
+ const entry = history.entries[history.currentIndex + delta];
+ if (!entry) {
+ return null;
+ }
+ const result = await Promise.all([
+ this.waitForNavigation(options),
+ this.#primaryTargetClient.send('Page.navigateToHistoryEntry', {
+ entryId: entry.id,
+ }),
+ ]);
+ return result[0];
+ }
+
+ override async bringToFront(): Promise<void> {
+ await this.#primaryTargetClient.send('Page.bringToFront');
+ }
+
+ override async setJavaScriptEnabled(enabled: boolean): Promise<void> {
+ return await this.#emulationManager.setJavaScriptEnabled(enabled);
+ }
+
+ override async setBypassCSP(enabled: boolean): Promise<void> {
+ await this.#primaryTargetClient.send('Page.setBypassCSP', {enabled});
+ }
+
+ override async emulateMediaType(type?: string): Promise<void> {
+ return await this.#emulationManager.emulateMediaType(type);
+ }
+
+ override async emulateCPUThrottling(factor: number | null): Promise<void> {
+ return await this.#emulationManager.emulateCPUThrottling(factor);
+ }
+
+ override async emulateMediaFeatures(
+ features?: MediaFeature[]
+ ): Promise<void> {
+ return await this.#emulationManager.emulateMediaFeatures(features);
+ }
+
+ override async emulateTimezone(timezoneId?: string): Promise<void> {
+ return await this.#emulationManager.emulateTimezone(timezoneId);
+ }
+
+ override async emulateIdleState(overrides?: {
+ isUserActive: boolean;
+ isScreenUnlocked: boolean;
+ }): Promise<void> {
+ return await this.#emulationManager.emulateIdleState(overrides);
+ }
+
+ override async emulateVisionDeficiency(
+ type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
+ ): Promise<void> {
+ return await this.#emulationManager.emulateVisionDeficiency(type);
+ }
+
+ override async setViewport(viewport: Viewport): Promise<void> {
+ const needsReload = await this.#emulationManager.emulateViewport(viewport);
+ this.#viewport = viewport;
+ if (needsReload) {
+ await this.reload();
+ }
+ }
+
+ override viewport(): Viewport | null {
+ return this.#viewport;
+ }
+
+ override async evaluateOnNewDocument<
+ Params extends unknown[],
+ Func extends (...args: Params) => unknown = (...args: Params) => unknown,
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<NewDocumentScriptEvaluation> {
+ const source = evaluationString(pageFunction, ...args);
+ const {identifier} = await this.#primaryTargetClient.send(
+ 'Page.addScriptToEvaluateOnNewDocument',
+ {
+ source,
+ }
+ );
+
+ return {identifier};
+ }
+
+ override async removeScriptToEvaluateOnNewDocument(
+ identifier: string
+ ): Promise<void> {
+ await this.#primaryTargetClient.send(
+ 'Page.removeScriptToEvaluateOnNewDocument',
+ {
+ identifier,
+ }
+ );
+ }
+
+ override async setCacheEnabled(enabled = true): Promise<void> {
+ await this.#frameManager.networkManager.setCacheEnabled(enabled);
+ }
+
+ override async _screenshot(
+ options: Readonly<ScreenshotOptions>
+ ): Promise<string> {
+ const {
+ fromSurface,
+ omitBackground,
+ optimizeForSpeed,
+ quality,
+ clip: userClip,
+ type,
+ captureBeyondViewport,
+ } = options;
+
+ const isFirefox =
+ this.target()._targetManager() instanceof FirefoxTargetManager;
+
+ await using stack = new AsyncDisposableStack();
+ // Firefox omits background by default; it's not configurable.
+ if (!isFirefox && omitBackground && (type === 'png' || type === 'webp')) {
+ await this.#emulationManager.setTransparentBackgroundColor();
+ stack.defer(async () => {
+ await this.#emulationManager
+ .resetDefaultBackgroundColor()
+ .catch(debugError);
+ });
+ }
+
+ let clip = userClip;
+ if (clip && !captureBeyondViewport) {
+ const viewport = await this.mainFrame()
+ .isolatedRealm()
+ .evaluate(() => {
+ const {
+ height,
+ pageLeft: x,
+ pageTop: y,
+ width,
+ } = window.visualViewport!;
+ return {x, y, height, width};
+ });
+ clip = getIntersectionRect(clip, viewport);
+ }
+
+ // We need to do these spreads because Firefox doesn't allow unknown options.
+ const {data} = await this.#primaryTargetClient.send(
+ 'Page.captureScreenshot',
+ {
+ format: type,
+ ...(optimizeForSpeed ? {optimizeForSpeed} : {}),
+ ...(quality !== undefined ? {quality: Math.round(quality)} : {}),
+ ...(clip ? {clip: {...clip, scale: clip.scale ?? 1}} : {}),
+ ...(!fromSurface ? {fromSurface} : {}),
+ captureBeyondViewport,
+ }
+ );
+ return data;
+ }
+
+ override async createPDFStream(options: PDFOptions = {}): Promise<Readable> {
+ const {timeout: ms = this._timeoutSettings.timeout()} = options;
+ const {
+ landscape,
+ displayHeaderFooter,
+ headerTemplate,
+ footerTemplate,
+ printBackground,
+ scale,
+ width: paperWidth,
+ height: paperHeight,
+ margin,
+ pageRanges,
+ preferCSSPageSize,
+ omitBackground,
+ tagged: generateTaggedPDF,
+ } = parsePDFOptions(options);
+
+ if (omitBackground) {
+ await this.#emulationManager.setTransparentBackgroundColor();
+ }
+
+ const printCommandPromise = this.#primaryTargetClient.send(
+ 'Page.printToPDF',
+ {
+ transferMode: 'ReturnAsStream',
+ landscape,
+ displayHeaderFooter,
+ headerTemplate,
+ footerTemplate,
+ printBackground,
+ scale,
+ paperWidth,
+ paperHeight,
+ marginTop: margin.top,
+ marginBottom: margin.bottom,
+ marginLeft: margin.left,
+ marginRight: margin.right,
+ pageRanges,
+ preferCSSPageSize,
+ generateTaggedPDF,
+ }
+ );
+
+ const result = await firstValueFrom(
+ from(printCommandPromise).pipe(raceWith(timeout(ms)))
+ );
+
+ if (omitBackground) {
+ await this.#emulationManager.resetDefaultBackgroundColor();
+ }
+
+ assert(result.stream, '`stream` is missing from `Page.printToPDF');
+ return await getReadableFromProtocolStream(
+ this.#primaryTargetClient,
+ result.stream
+ );
+ }
+
+ override async pdf(options: PDFOptions = {}): Promise<Buffer> {
+ const {path = undefined} = options;
+ const readable = await this.createPDFStream(options);
+ const buffer = await getReadableAsBuffer(readable, path);
+ assert(buffer, 'Could not create buffer');
+ return buffer;
+ }
+
+ override async close(
+ options: {runBeforeUnload?: boolean} = {runBeforeUnload: undefined}
+ ): Promise<void> {
+ const connection = this.#primaryTargetClient.connection();
+ assert(
+ connection,
+ 'Protocol error: Connection closed. Most likely the page has been closed.'
+ );
+ const runBeforeUnload = !!options.runBeforeUnload;
+ if (runBeforeUnload) {
+ await this.#primaryTargetClient.send('Page.close');
+ } else {
+ await connection.send('Target.closeTarget', {
+ targetId: this.#primaryTarget._targetId,
+ });
+ await this.#tabTarget._isClosedDeferred.valueOrThrow();
+ }
+ }
+
+ override isClosed(): boolean {
+ return this.#closed;
+ }
+
+ override get mouse(): CdpMouse {
+ return this.#mouse;
+ }
+
+ /**
+ * This method is typically coupled with an action that triggers a device
+ * request from an api such as WebBluetooth.
+ *
+ * :::caution
+ *
+ * This must be called before the device request is made. It will not return a
+ * currently active device prompt.
+ *
+ * :::
+ *
+ * @example
+ *
+ * ```ts
+ * const [devicePrompt] = Promise.all([
+ * page.waitForDevicePrompt(),
+ * page.click('#connect-bluetooth'),
+ * ]);
+ * await devicePrompt.select(
+ * await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
+ * );
+ * ```
+ */
+ override async waitForDevicePrompt(
+ options: WaitTimeoutOptions = {}
+ ): Promise<DeviceRequestPrompt> {
+ return await this.mainFrame().waitForDevicePrompt(options);
+ }
+}
+
+const supportedMetrics = new Set<string>([
+ 'Timestamp',
+ 'Documents',
+ 'Frames',
+ 'JSEventListeners',
+ 'Nodes',
+ 'LayoutCount',
+ 'RecalcStyleCount',
+ 'LayoutDuration',
+ 'RecalcStyleDuration',
+ 'ScriptDuration',
+ 'TaskDuration',
+ 'JSHeapUsedSize',
+ 'JSHeapTotalSize',
+]);
+
+/** @see https://w3c.github.io/webdriver-bidi/#rectangle-intersection */
+function getIntersectionRect(
+ clip: Readonly<ScreenshotClip>,
+ viewport: Readonly<Protocol.DOM.Rect>
+): ScreenshotClip {
+ // Note these will already be normalized.
+ const x = Math.max(clip.x, viewport.x);
+ const y = Math.max(clip.y, viewport.y);
+ return {
+ x,
+ y,
+ width: Math.max(
+ Math.min(clip.x + clip.width, viewport.x + viewport.width) - x,
+ 0
+ ),
+ height: Math.max(
+ Math.min(clip.y + clip.height, viewport.y + viewport.height) - y,
+ 0
+ ),
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
new file mode 100644
index 0000000000..df035ae52b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2021 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {NetworkConditions} from './NetworkManager.js';
+
+/**
+ * A list of network conditions to be used with
+ * {@link Page.emulateNetworkConditions}.
+ *
+ * @example
+ *
+ * ```ts
+ * import {PredefinedNetworkConditions} from 'puppeteer';
+ * const slow3G = PredefinedNetworkConditions['Slow 3G'];
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.emulateNetworkConditions(slow3G);
+ * await page.goto('https://www.google.com');
+ * // other actions...
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @public
+ */
+export const PredefinedNetworkConditions = Object.freeze({
+ 'Slow 3G': {
+ download: ((500 * 1000) / 8) * 0.8,
+ upload: ((500 * 1000) / 8) * 0.8,
+ latency: 400 * 5,
+ } as NetworkConditions,
+ 'Fast 3G': {
+ download: ((1.6 * 1000 * 1000) / 8) * 0.9,
+ upload: ((750 * 1000) / 8) * 0.9,
+ latency: 150 * 3.75,
+ } as NetworkConditions,
+});
+
+/**
+ * @deprecated Import {@link PredefinedNetworkConditions}.
+ *
+ * @public
+ */
+export const networkConditions = PredefinedNetworkConditions;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts
new file mode 100644
index 0000000000..b3e9ea83ec
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Target.ts
@@ -0,0 +1,305 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {Browser} from '../api/Browser.js';
+import type {BrowserContext} from '../api/BrowserContext.js';
+import type {CDPSession} from '../api/CDPSession.js';
+import {PageEvent, type Page} from '../api/Page.js';
+import {Target, TargetType} from '../api/Target.js';
+import {debugError} from '../common/util.js';
+import type {Viewport} from '../common/Viewport.js';
+import {Deferred} from '../util/Deferred.js';
+
+import {CdpCDPSession} from './CDPSession.js';
+import {CdpPage} from './Page.js';
+import type {TargetManager} from './TargetManager.js';
+import {CdpWebWorker} from './WebWorker.js';
+
+/**
+ * @internal
+ */
+export enum InitializationStatus {
+ SUCCESS = 'success',
+ ABORTED = 'aborted',
+}
+
+/**
+ * @internal
+ */
+export class CdpTarget extends Target {
+ #browserContext?: BrowserContext;
+ #session?: CDPSession;
+ #targetInfo: Protocol.Target.TargetInfo;
+ #targetManager?: TargetManager;
+ #sessionFactory:
+ | ((isAutoAttachEmulated: boolean) => Promise<CDPSession>)
+ | undefined;
+
+ _initializedDeferred = Deferred.create<InitializationStatus>();
+ _isClosedDeferred = Deferred.create<void>();
+ _targetId: string;
+
+ /**
+ * To initialize the target for use, call initialize.
+ *
+ * @internal
+ */
+ constructor(
+ targetInfo: Protocol.Target.TargetInfo,
+ session: CDPSession | undefined,
+ browserContext: BrowserContext | undefined,
+ targetManager: TargetManager | undefined,
+ sessionFactory:
+ | ((isAutoAttachEmulated: boolean) => Promise<CDPSession>)
+ | undefined
+ ) {
+ super();
+ this.#session = session;
+ this.#targetManager = targetManager;
+ this.#targetInfo = targetInfo;
+ this.#browserContext = browserContext;
+ this._targetId = targetInfo.targetId;
+ this.#sessionFactory = sessionFactory;
+ if (this.#session && this.#session instanceof CdpCDPSession) {
+ this.#session._setTarget(this);
+ }
+ }
+
+ override async asPage(): Promise<Page> {
+ const session = this._session();
+ if (!session) {
+ return await this.createCDPSession().then(client => {
+ return CdpPage._create(client, this, false, null);
+ });
+ }
+ return await CdpPage._create(session, this, false, null);
+ }
+
+ _subtype(): string | undefined {
+ return this.#targetInfo.subtype;
+ }
+
+ _session(): CDPSession | undefined {
+ return this.#session;
+ }
+
+ protected _sessionFactory(): (
+ isAutoAttachEmulated: boolean
+ ) => Promise<CDPSession> {
+ if (!this.#sessionFactory) {
+ throw new Error('sessionFactory is not initialized');
+ }
+ return this.#sessionFactory;
+ }
+
+ override createCDPSession(): Promise<CDPSession> {
+ if (!this.#sessionFactory) {
+ throw new Error('sessionFactory is not initialized');
+ }
+ return this.#sessionFactory(false).then(session => {
+ (session as CdpCDPSession)._setTarget(this);
+ return session;
+ });
+ }
+
+ override url(): string {
+ return this.#targetInfo.url;
+ }
+
+ override type(): TargetType {
+ const type = this.#targetInfo.type;
+ switch (type) {
+ case 'page':
+ return TargetType.PAGE;
+ case 'background_page':
+ return TargetType.BACKGROUND_PAGE;
+ case 'service_worker':
+ return TargetType.SERVICE_WORKER;
+ case 'shared_worker':
+ return TargetType.SHARED_WORKER;
+ case 'browser':
+ return TargetType.BROWSER;
+ case 'webview':
+ return TargetType.WEBVIEW;
+ case 'tab':
+ return TargetType.TAB;
+ default:
+ return TargetType.OTHER;
+ }
+ }
+
+ _targetManager(): TargetManager {
+ if (!this.#targetManager) {
+ throw new Error('targetManager is not initialized');
+ }
+ return this.#targetManager;
+ }
+
+ _getTargetInfo(): Protocol.Target.TargetInfo {
+ return this.#targetInfo;
+ }
+
+ override browser(): Browser {
+ if (!this.#browserContext) {
+ throw new Error('browserContext is not initialized');
+ }
+ return this.#browserContext.browser();
+ }
+
+ override browserContext(): BrowserContext {
+ if (!this.#browserContext) {
+ throw new Error('browserContext is not initialized');
+ }
+ return this.#browserContext;
+ }
+
+ override opener(): Target | undefined {
+ const {openerId} = this.#targetInfo;
+ if (!openerId) {
+ return;
+ }
+ return this.browser()
+ .targets()
+ .find(target => {
+ return (target as CdpTarget)._targetId === openerId;
+ });
+ }
+
+ _targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void {
+ this.#targetInfo = targetInfo;
+ this._checkIfInitialized();
+ }
+
+ _initialize(): void {
+ this._initializedDeferred.resolve(InitializationStatus.SUCCESS);
+ }
+
+ _isTargetExposed(): boolean {
+ return this.type() !== TargetType.TAB && !this._subtype();
+ }
+
+ protected _checkIfInitialized(): void {
+ if (!this._initializedDeferred.resolved()) {
+ this._initializedDeferred.resolve(InitializationStatus.SUCCESS);
+ }
+ }
+}
+
+/**
+ * @internal
+ */
+export class PageTarget extends CdpTarget {
+ #defaultViewport?: Viewport;
+ protected pagePromise?: Promise<Page>;
+ #ignoreHTTPSErrors: boolean;
+
+ constructor(
+ targetInfo: Protocol.Target.TargetInfo,
+ session: CDPSession | undefined,
+ browserContext: BrowserContext,
+ targetManager: TargetManager,
+ sessionFactory: (isAutoAttachEmulated: boolean) => Promise<CDPSession>,
+ ignoreHTTPSErrors: boolean,
+ defaultViewport: Viewport | null
+ ) {
+ super(targetInfo, session, browserContext, targetManager, sessionFactory);
+ this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
+ this.#defaultViewport = defaultViewport ?? undefined;
+ }
+
+ override _initialize(): void {
+ this._initializedDeferred
+ .valueOrThrow()
+ .then(async result => {
+ if (result === InitializationStatus.ABORTED) {
+ return;
+ }
+ const opener = this.opener();
+ if (!(opener instanceof PageTarget)) {
+ return;
+ }
+ if (!opener || !opener.pagePromise || this.type() !== 'page') {
+ return true;
+ }
+ const openerPage = await opener.pagePromise;
+ if (!openerPage.listenerCount(PageEvent.Popup)) {
+ return true;
+ }
+ const popupPage = await this.page();
+ openerPage.emit(PageEvent.Popup, popupPage);
+ return true;
+ })
+ .catch(debugError);
+ this._checkIfInitialized();
+ }
+
+ override async page(): Promise<Page | null> {
+ if (!this.pagePromise) {
+ const session = this._session();
+ this.pagePromise = (
+ session
+ ? Promise.resolve(session)
+ : this._sessionFactory()(/* isAutoAttachEmulated=*/ false)
+ ).then(client => {
+ return CdpPage._create(
+ client,
+ this,
+ this.#ignoreHTTPSErrors,
+ this.#defaultViewport ?? null
+ );
+ });
+ }
+ return (await this.pagePromise) ?? null;
+ }
+
+ override _checkIfInitialized(): void {
+ if (this._initializedDeferred.resolved()) {
+ return;
+ }
+ if (this._getTargetInfo().url !== '') {
+ this._initializedDeferred.resolve(InitializationStatus.SUCCESS);
+ }
+ }
+}
+
+/**
+ * @internal
+ */
+export class DevToolsTarget extends PageTarget {}
+
+/**
+ * @internal
+ */
+export class WorkerTarget extends CdpTarget {
+ #workerPromise?: Promise<CdpWebWorker>;
+
+ override async worker(): Promise<CdpWebWorker | null> {
+ if (!this.#workerPromise) {
+ const session = this._session();
+ // TODO(einbinder): Make workers send their console logs.
+ this.#workerPromise = (
+ session
+ ? Promise.resolve(session)
+ : this._sessionFactory()(/* isAutoAttachEmulated=*/ false)
+ ).then(client => {
+ return new CdpWebWorker(
+ client,
+ this._getTargetInfo().url,
+ () => {} /* consoleAPICalled */,
+ () => {} /* exceptionThrown */
+ );
+ });
+ }
+ return await this.#workerPromise;
+ }
+}
+
+/**
+ * @internal
+ */
+export class OtherTarget extends CdpTarget {}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/TargetManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/TargetManager.ts
new file mode 100644
index 0000000000..248f63539d
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/TargetManager.ts
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {EventEmitter, EventType} from '../common/EventEmitter.js';
+
+import type {CdpTarget} from './Target.js';
+
+/**
+ * @internal
+ */
+export type TargetFactory = (
+ targetInfo: Protocol.Target.TargetInfo,
+ session?: CDPSession,
+ parentSession?: CDPSession
+) => CdpTarget;
+
+/**
+ * @internal
+ */
+export const enum TargetManagerEvent {
+ TargetDiscovered = 'targetDiscovered',
+ TargetAvailable = 'targetAvailable',
+ TargetGone = 'targetGone',
+ /**
+ * Emitted after a target has been initialized and whenever its URL changes.
+ */
+ TargetChanged = 'targetChanged',
+}
+
+/**
+ * @internal
+ */
+export interface TargetManagerEvents extends Record<EventType, unknown> {
+ [TargetManagerEvent.TargetAvailable]: CdpTarget;
+ [TargetManagerEvent.TargetDiscovered]: Protocol.Target.TargetInfo;
+ [TargetManagerEvent.TargetGone]: CdpTarget;
+ [TargetManagerEvent.TargetChanged]: {
+ target: CdpTarget;
+ wasInitialized: true;
+ previousURL: string;
+ };
+}
+
+/**
+ * TargetManager encapsulates all interactions with CDP targets and is
+ * responsible for coordinating the configuration of targets with the rest of
+ * Puppeteer. Code outside of this class should not subscribe `Target.*` events
+ * and only use the TargetManager events.
+ *
+ * There are two implementations: one for Chrome that uses CDP's auto-attach
+ * mechanism and one for Firefox because Firefox does not support auto-attach.
+ *
+ * @internal
+ */
+export interface TargetManager extends EventEmitter<TargetManagerEvents> {
+ getAvailableTargets(): ReadonlyMap<string, CdpTarget>;
+ initialize(): Promise<void>;
+ dispose(): void;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Tracing.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Tracing.ts
new file mode 100644
index 0000000000..22eae9a5d4
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Tracing.ts
@@ -0,0 +1,140 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {CDPSession} from '../api/CDPSession.js';
+import {
+ getReadableAsBuffer,
+ getReadableFromProtocolStream,
+} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+/**
+ * @public
+ */
+export interface TracingOptions {
+ path?: string;
+ screenshots?: boolean;
+ categories?: string[];
+}
+
+/**
+ * The Tracing class exposes the tracing audit interface.
+ * @remarks
+ * You can use `tracing.start` and `tracing.stop` to create a trace file
+ * which can be opened in Chrome DevTools or {@link https://chromedevtools.github.io/timeline-viewer/ | timeline viewer}.
+ *
+ * @example
+ *
+ * ```ts
+ * await page.tracing.start({path: 'trace.json'});
+ * await page.goto('https://www.google.com');
+ * await page.tracing.stop();
+ * ```
+ *
+ * @public
+ */
+export class Tracing {
+ #client: CDPSession;
+ #recording = false;
+ #path?: string;
+
+ /**
+ * @internal
+ */
+ constructor(client: CDPSession) {
+ this.#client = client;
+ }
+
+ /**
+ * @internal
+ */
+ updateClient(client: CDPSession): void {
+ this.#client = client;
+ }
+
+ /**
+ * Starts a trace for the current page.
+ * @remarks
+ * Only one trace can be active at a time per browser.
+ *
+ * @param options - Optional `TracingOptions`.
+ */
+ async start(options: TracingOptions = {}): Promise<void> {
+ assert(
+ !this.#recording,
+ 'Cannot start recording trace while already recording trace.'
+ );
+
+ const defaultCategories = [
+ '-*',
+ 'devtools.timeline',
+ 'v8.execute',
+ 'disabled-by-default-devtools.timeline',
+ 'disabled-by-default-devtools.timeline.frame',
+ 'toplevel',
+ 'blink.console',
+ 'blink.user_timing',
+ 'latencyInfo',
+ 'disabled-by-default-devtools.timeline.stack',
+ 'disabled-by-default-v8.cpu_profiler',
+ ];
+ const {path, screenshots = false, categories = defaultCategories} = options;
+
+ if (screenshots) {
+ categories.push('disabled-by-default-devtools.screenshot');
+ }
+
+ const excludedCategories = categories
+ .filter(cat => {
+ return cat.startsWith('-');
+ })
+ .map(cat => {
+ return cat.slice(1);
+ });
+ const includedCategories = categories.filter(cat => {
+ return !cat.startsWith('-');
+ });
+
+ this.#path = path;
+ this.#recording = true;
+ await this.#client.send('Tracing.start', {
+ transferMode: 'ReturnAsStream',
+ traceConfig: {
+ excludedCategories,
+ includedCategories,
+ },
+ });
+ }
+
+ /**
+ * Stops a trace started with the `start` method.
+ * @returns Promise which resolves to buffer with trace data.
+ */
+ async stop(): Promise<Buffer | undefined> {
+ const contentDeferred = Deferred.create<Buffer | undefined>();
+ this.#client.once('Tracing.tracingComplete', async event => {
+ try {
+ assert(event.stream, 'Missing "stream"');
+ const readable = await getReadableFromProtocolStream(
+ this.#client,
+ event.stream
+ );
+ const buffer = await getReadableAsBuffer(readable, this.#path);
+ contentDeferred.resolve(buffer ?? undefined);
+ } catch (error) {
+ if (isErrorLike(error)) {
+ contentDeferred.reject(error);
+ } else {
+ contentDeferred.reject(new Error(`Unknown error: ${error}`));
+ }
+ }
+ });
+ await this.#client.send('Tracing.end');
+ this.#recording = false;
+ return await contentDeferred.valueOrThrow();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts
new file mode 100644
index 0000000000..552e8a6cf5
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/WebWorker.ts
@@ -0,0 +1,83 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {Protocol} from 'devtools-protocol';
+
+import type {CDPSession} from '../api/CDPSession.js';
+import type {Realm} from '../api/Realm.js';
+import {WebWorker} from '../api/WebWorker.js';
+import type {ConsoleMessageType} from '../common/ConsoleMessage.js';
+import {TimeoutSettings} from '../common/TimeoutSettings.js';
+import {debugError} from '../common/util.js';
+
+import {ExecutionContext} from './ExecutionContext.js';
+import {IsolatedWorld} from './IsolatedWorld.js';
+import {CdpJSHandle} from './JSHandle.js';
+
+/**
+ * @internal
+ */
+export type ConsoleAPICalledCallback = (
+ eventType: ConsoleMessageType,
+ handles: CdpJSHandle[],
+ trace?: Protocol.Runtime.StackTrace
+) => void;
+
+/**
+ * @internal
+ */
+export type ExceptionThrownCallback = (
+ event: Protocol.Runtime.ExceptionThrownEvent
+) => void;
+
+/**
+ * @internal
+ */
+export class CdpWebWorker extends WebWorker {
+ #world: IsolatedWorld;
+ #client: CDPSession;
+
+ constructor(
+ client: CDPSession,
+ url: string,
+ consoleAPICalled: ConsoleAPICalledCallback,
+ exceptionThrown: ExceptionThrownCallback
+ ) {
+ super(url);
+ this.#client = client;
+ this.#world = new IsolatedWorld(this, new TimeoutSettings());
+
+ this.#client.once('Runtime.executionContextCreated', async event => {
+ this.#world.setContext(
+ new ExecutionContext(client, event.context, this.#world)
+ );
+ });
+ this.#client.on('Runtime.consoleAPICalled', async event => {
+ try {
+ return consoleAPICalled(
+ event.type,
+ event.args.map((object: Protocol.Runtime.RemoteObject) => {
+ return new CdpJSHandle(this.#world, object);
+ }),
+ event.stackTrace
+ );
+ } catch (err) {
+ debugError(err);
+ }
+ });
+ this.#client.on('Runtime.exceptionThrown', exceptionThrown);
+
+ // This might fail if the target is closed before we receive all execution contexts.
+ this.#client.send('Runtime.enable').catch(debugError);
+ }
+
+ mainRealm(): Realm {
+ return this.#world;
+ }
+
+ get client(): CDPSession {
+ return this.#client;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/cdp.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/cdp.ts
new file mode 100644
index 0000000000..1533d63f35
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/cdp.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './Accessibility.js';
+export * from './AriaQueryHandler.js';
+export * from './Binding.js';
+export * from './Browser.js';
+export * from './BrowserConnector.js';
+export * from './cdp.js';
+export * from './CDPSession.js';
+export * from './ChromeTargetManager.js';
+export * from './Connection.js';
+export * from './Coverage.js';
+export * from './DeviceRequestPrompt.js';
+export * from './Dialog.js';
+export * from './ElementHandle.js';
+export * from './EmulationManager.js';
+export * from './ExecutionContext.js';
+export * from './FirefoxTargetManager.js';
+export * from './Frame.js';
+export * from './FrameManager.js';
+export * from './FrameManagerEvents.js';
+export * from './FrameTree.js';
+export * from './HTTPRequest.js';
+export * from './HTTPResponse.js';
+export * from './Input.js';
+export * from './IsolatedWorld.js';
+export * from './IsolatedWorlds.js';
+export * from './JSHandle.js';
+export * from './LifecycleWatcher.js';
+export * from './NetworkEventManager.js';
+export * from './NetworkManager.js';
+export * from './Page.js';
+export * from './PredefinedNetworkConditions.js';
+export * from './Target.js';
+export * from './TargetManager.js';
+export * from './Tracing.js';
+export * from './utils.js';
+export * from './WebWorker.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/utils.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/utils.ts
new file mode 100644
index 0000000000..989a3cd6a3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/utils.ts
@@ -0,0 +1,232 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import {PuppeteerURL, evaluationString} from '../common/util.js';
+import {assert} from '../util/assert.js';
+
+/**
+ * @internal
+ */
+export function createEvaluationError(
+ details: Protocol.Runtime.ExceptionDetails
+): unknown {
+ let name: string;
+ let message: string;
+ if (!details.exception) {
+ name = 'Error';
+ message = details.text;
+ } else if (
+ (details.exception.type !== 'object' ||
+ details.exception.subtype !== 'error') &&
+ !details.exception.objectId
+ ) {
+ return valueFromRemoteObject(details.exception);
+ } else {
+ const detail = getErrorDetails(details);
+ name = detail.name;
+ message = detail.message;
+ }
+ const messageHeight = message.split('\n').length;
+ const error = new Error(message);
+ error.name = name;
+ const stackLines = error.stack!.split('\n');
+ const messageLines = stackLines.splice(0, messageHeight);
+
+ // The first line is this function which we ignore.
+ stackLines.shift();
+ if (details.stackTrace && stackLines.length < Error.stackTraceLimit) {
+ for (const frame of details.stackTrace.callFrames.reverse()) {
+ if (
+ PuppeteerURL.isPuppeteerURL(frame.url) &&
+ frame.url !== PuppeteerURL.INTERNAL_URL
+ ) {
+ const url = PuppeteerURL.parse(frame.url);
+ stackLines.unshift(
+ ` at ${frame.functionName || url.functionName} (${
+ url.functionName
+ } at ${url.siteString}, <anonymous>:${frame.lineNumber}:${
+ frame.columnNumber
+ })`
+ );
+ } else {
+ stackLines.push(
+ ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
+ frame.lineNumber
+ }:${frame.columnNumber})`
+ );
+ }
+ if (stackLines.length >= Error.stackTraceLimit) {
+ break;
+ }
+ }
+ }
+
+ error.stack = [...messageLines, ...stackLines].join('\n');
+ return error;
+}
+
+const getErrorDetails = (details: Protocol.Runtime.ExceptionDetails) => {
+ let name = '';
+ let message: string;
+ const lines = details.exception?.description?.split('\n at ') ?? [];
+ const size = Math.min(
+ details.stackTrace?.callFrames.length ?? 0,
+ lines.length - 1
+ );
+ lines.splice(-size, size);
+ if (details.exception?.className) {
+ name = details.exception.className;
+ }
+ message = lines.join('\n');
+ if (name && message.startsWith(`${name}: `)) {
+ message = message.slice(name.length + 2);
+ }
+ return {message, name};
+};
+
+/**
+ * @internal
+ */
+export function createClientError(
+ details: Protocol.Runtime.ExceptionDetails
+): Error {
+ let name: string;
+ let message: string;
+ if (!details.exception) {
+ name = 'Error';
+ message = details.text;
+ } else if (
+ (details.exception.type !== 'object' ||
+ details.exception.subtype !== 'error') &&
+ !details.exception.objectId
+ ) {
+ return valueFromRemoteObject(details.exception);
+ } else {
+ const detail = getErrorDetails(details);
+ name = detail.name;
+ message = detail.message;
+ }
+ const error = new Error(message);
+ error.name = name;
+
+ const messageHeight = error.message.split('\n').length;
+ const messageLines = error.stack!.split('\n').splice(0, messageHeight);
+
+ const stackLines = [];
+ if (details.stackTrace) {
+ for (const frame of details.stackTrace.callFrames) {
+ // Note we need to add `1` because the values are 0-indexed.
+ stackLines.push(
+ ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
+ frame.lineNumber + 1
+ }:${frame.columnNumber + 1})`
+ );
+ if (stackLines.length >= Error.stackTraceLimit) {
+ break;
+ }
+ }
+ }
+
+ error.stack = [...messageLines, ...stackLines].join('\n');
+ return error;
+}
+
+/**
+ * @internal
+ */
+export function valueFromRemoteObject(
+ remoteObject: Protocol.Runtime.RemoteObject
+): any {
+ assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
+ if (remoteObject.unserializableValue) {
+ if (remoteObject.type === 'bigint') {
+ return BigInt(remoteObject.unserializableValue.replace('n', ''));
+ }
+ switch (remoteObject.unserializableValue) {
+ case '-0':
+ return -0;
+ case 'NaN':
+ return NaN;
+ case 'Infinity':
+ return Infinity;
+ case '-Infinity':
+ return -Infinity;
+ default:
+ throw new Error(
+ 'Unsupported unserializable value: ' +
+ remoteObject.unserializableValue
+ );
+ }
+ }
+ return remoteObject.value;
+}
+
+/**
+ * @internal
+ */
+export function addPageBinding(type: string, name: string): void {
+ // This is the CDP binding.
+ // @ts-expect-error: In a different context.
+ const callCdp = globalThis[name];
+
+ // Depending on the frame loading state either Runtime.evaluate or
+ // Page.addScriptToEvaluateOnNewDocument might succeed. Let's check that we
+ // don't re-wrap Puppeteer's binding.
+ if (callCdp[Symbol.toStringTag] === 'PuppeteerBinding') {
+ return;
+ }
+
+ // We replace the CDP binding with a Puppeteer binding.
+ Object.assign(globalThis, {
+ [name](...args: unknown[]): Promise<unknown> {
+ // This is the Puppeteer binding.
+ // @ts-expect-error: In a different context.
+ const callPuppeteer = globalThis[name];
+ callPuppeteer.args ??= new Map();
+ callPuppeteer.callbacks ??= new Map();
+
+ const seq = (callPuppeteer.lastSeq ?? 0) + 1;
+ callPuppeteer.lastSeq = seq;
+ callPuppeteer.args.set(seq, args);
+
+ callCdp(
+ JSON.stringify({
+ type,
+ name,
+ seq,
+ args,
+ isTrivial: !args.some(value => {
+ return value instanceof Node;
+ }),
+ })
+ );
+
+ return new Promise((resolve, reject) => {
+ callPuppeteer.callbacks.set(seq, {
+ resolve(value: unknown) {
+ callPuppeteer.args.delete(seq);
+ resolve(value);
+ },
+ reject(value?: unknown) {
+ callPuppeteer.args.delete(seq);
+ reject(value);
+ },
+ });
+ });
+ },
+ });
+ // @ts-expect-error: In a different context.
+ globalThis[name][Symbol.toStringTag] = 'PuppeteerBinding';
+}
+
+/**
+ * @internal
+ */
+export function pageBindingInitString(type: string, name: string): string {
+ return evaluationString(addPageBinding, type, name);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts
new file mode 100644
index 0000000000..217e53bedd
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts
@@ -0,0 +1,114 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Browser} from '../api/Browser.js';
+import {_connectToBiDiBrowser} from '../bidi/BrowserConnector.js';
+import {_connectToCdpBrowser} from '../cdp/BrowserConnector.js';
+import {isNode} from '../environment.js';
+import {assert} from '../util/assert.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import type {ConnectionTransport} from './ConnectionTransport.js';
+import type {ConnectOptions} from './ConnectOptions.js';
+import type {BrowserConnectOptions} from './ConnectOptions.js';
+import {getFetch} from './fetch.js';
+
+const getWebSocketTransportClass = async () => {
+ return isNode
+ ? (await import('../node/NodeWebSocketTransport.js')).NodeWebSocketTransport
+ : (await import('../common/BrowserWebSocketTransport.js'))
+ .BrowserWebSocketTransport;
+};
+
+/**
+ * Users should never call this directly; it's called when calling
+ * `puppeteer.connect`. This method attaches Puppeteer to an existing browser instance.
+ *
+ * @internal
+ */
+export async function _connectToBrowser(
+ options: ConnectOptions
+): Promise<Browser> {
+ const {connectionTransport, endpointUrl} =
+ await getConnectionTransport(options);
+
+ if (options.protocol === 'webDriverBiDi') {
+ const bidiBrowser = await _connectToBiDiBrowser(
+ connectionTransport,
+ endpointUrl,
+ options
+ );
+ return bidiBrowser;
+ } else {
+ const cdpBrowser = await _connectToCdpBrowser(
+ connectionTransport,
+ endpointUrl,
+ options
+ );
+ return cdpBrowser;
+ }
+}
+
+/**
+ * Establishes a websocket connection by given options and returns both transport and
+ * endpoint url the transport is connected to.
+ */
+async function getConnectionTransport(
+ options: BrowserConnectOptions & ConnectOptions
+): Promise<{connectionTransport: ConnectionTransport; endpointUrl: string}> {
+ const {browserWSEndpoint, browserURL, transport, headers = {}} = options;
+
+ assert(
+ Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) ===
+ 1,
+ 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect'
+ );
+
+ if (transport) {
+ return {connectionTransport: transport, endpointUrl: ''};
+ } else if (browserWSEndpoint) {
+ const WebSocketClass = await getWebSocketTransportClass();
+ const connectionTransport: ConnectionTransport =
+ await WebSocketClass.create(browserWSEndpoint, headers);
+ return {
+ connectionTransport: connectionTransport,
+ endpointUrl: browserWSEndpoint,
+ };
+ } else if (browserURL) {
+ const connectionURL = await getWSEndpoint(browserURL);
+ const WebSocketClass = await getWebSocketTransportClass();
+ const connectionTransport: ConnectionTransport =
+ await WebSocketClass.create(connectionURL);
+ return {
+ connectionTransport: connectionTransport,
+ endpointUrl: connectionURL,
+ };
+ }
+ throw new Error('Invalid connection options');
+}
+
+async function getWSEndpoint(browserURL: string): Promise<string> {
+ const endpointURL = new URL('/json/version', browserURL);
+
+ const fetch = await getFetch();
+ try {
+ const result = await fetch(endpointURL.toString(), {
+ method: 'GET',
+ });
+ if (!result.ok) {
+ throw new Error(`HTTP ${result.statusText}`);
+ }
+ const data = await result.json();
+ return data.webSocketDebuggerUrl;
+ } catch (error) {
+ if (isErrorLike(error)) {
+ error.message =
+ `Failed to fetch browser webSocket URL from ${endpointURL}: ` +
+ error.message;
+ }
+ throw error;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts
new file mode 100644
index 0000000000..cc0f81cb06
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {ConnectionTransport} from './ConnectionTransport.js';
+
+/**
+ * @internal
+ */
+export class BrowserWebSocketTransport implements ConnectionTransport {
+ static create(url: string): Promise<BrowserWebSocketTransport> {
+ return new Promise((resolve, reject) => {
+ const ws = new WebSocket(url);
+
+ ws.addEventListener('open', () => {
+ return resolve(new BrowserWebSocketTransport(ws));
+ });
+ ws.addEventListener('error', reject);
+ });
+ }
+
+ #ws: WebSocket;
+ onmessage?: (message: string) => void;
+ onclose?: () => void;
+
+ constructor(ws: WebSocket) {
+ this.#ws = ws;
+ this.#ws.addEventListener('message', event => {
+ if (this.onmessage) {
+ this.onmessage.call(null, event.data);
+ }
+ });
+ this.#ws.addEventListener('close', () => {
+ if (this.onclose) {
+ this.onclose.call(null);
+ }
+ });
+ // Silently ignore all errors - we don't know what to do with them.
+ this.#ws.addEventListener('error', () => {});
+ }
+
+ send(message: string): void {
+ this.#ws.send(message);
+ }
+
+ close(): void {
+ this.#ws.close();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/CallbackRegistry.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/CallbackRegistry.ts
new file mode 100644
index 0000000000..ea9f3d5abb
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/CallbackRegistry.ts
@@ -0,0 +1,177 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {Deferred} from '../util/Deferred.js';
+import {rewriteError} from '../util/ErrorLike.js';
+
+import {ProtocolError, TargetCloseError} from './Errors.js';
+import {debugError} from './util.js';
+
+/**
+ * Manages callbacks and their IDs for the protocol request/response communication.
+ *
+ * @internal
+ */
+export class CallbackRegistry {
+ #callbacks = new Map<number, Callback>();
+ #idGenerator = createIncrementalIdGenerator();
+
+ create(
+ label: string,
+ timeout: number | undefined,
+ request: (id: number) => void
+ ): Promise<unknown> {
+ const callback = new Callback(this.#idGenerator(), label, timeout);
+ this.#callbacks.set(callback.id, callback);
+ try {
+ request(callback.id);
+ } catch (error) {
+ // We still throw sync errors synchronously and clean up the scheduled
+ // callback.
+ callback.promise
+ .valueOrThrow()
+ .catch(debugError)
+ .finally(() => {
+ this.#callbacks.delete(callback.id);
+ });
+ callback.reject(error as Error);
+ throw error;
+ }
+ // Must only have sync code up until here.
+ return callback.promise.valueOrThrow().finally(() => {
+ this.#callbacks.delete(callback.id);
+ });
+ }
+
+ reject(id: number, message: string, originalMessage?: string): void {
+ const callback = this.#callbacks.get(id);
+ if (!callback) {
+ return;
+ }
+ this._reject(callback, message, originalMessage);
+ }
+
+ _reject(
+ callback: Callback,
+ errorMessage: string | ProtocolError,
+ originalMessage?: string
+ ): void {
+ let error: ProtocolError;
+ let message: string;
+ if (errorMessage instanceof ProtocolError) {
+ error = errorMessage;
+ error.cause = callback.error;
+ message = errorMessage.message;
+ } else {
+ error = callback.error;
+ message = errorMessage;
+ }
+
+ callback.reject(
+ rewriteError(
+ error,
+ `Protocol error (${callback.label}): ${message}`,
+ originalMessage
+ )
+ );
+ }
+
+ resolve(id: number, value: unknown): void {
+ const callback = this.#callbacks.get(id);
+ if (!callback) {
+ return;
+ }
+ callback.resolve(value);
+ }
+
+ clear(): void {
+ for (const callback of this.#callbacks.values()) {
+ // TODO: probably we can accept error messages as params.
+ this._reject(callback, new TargetCloseError('Target closed'));
+ }
+ this.#callbacks.clear();
+ }
+
+ /**
+ * @internal
+ */
+ getPendingProtocolErrors(): Error[] {
+ const result: Error[] = [];
+ for (const callback of this.#callbacks.values()) {
+ result.push(
+ new Error(`${callback.label} timed out. Trace: ${callback.error.stack}`)
+ );
+ }
+ return result;
+ }
+}
+/**
+ * @internal
+ */
+
+export class Callback {
+ #id: number;
+ #error = new ProtocolError();
+ #deferred = Deferred.create<unknown>();
+ #timer?: ReturnType<typeof setTimeout>;
+ #label: string;
+
+ constructor(id: number, label: string, timeout?: number) {
+ this.#id = id;
+ this.#label = label;
+ if (timeout) {
+ this.#timer = setTimeout(() => {
+ this.#deferred.reject(
+ rewriteError(
+ this.#error,
+ `${label} timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.`
+ )
+ );
+ }, timeout);
+ }
+ }
+
+ resolve(value: unknown): void {
+ clearTimeout(this.#timer);
+ this.#deferred.resolve(value);
+ }
+
+ reject(error: Error): void {
+ clearTimeout(this.#timer);
+ this.#deferred.reject(error);
+ }
+
+ get id(): number {
+ return this.#id;
+ }
+
+ get promise(): Deferred<unknown> {
+ return this.#deferred;
+ }
+
+ get error(): ProtocolError {
+ return this.#error;
+ }
+
+ get label(): string {
+ return this.#label;
+ }
+}
+
+/**
+ * @internal
+ */
+export function createIncrementalIdGenerator(): GetIdFn {
+ let id = 0;
+ return (): number => {
+ return ++id;
+ };
+}
+
+/**
+ * @internal
+ */
+export type GetIdFn = () => number;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts
new file mode 100644
index 0000000000..c64d109a7c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts
@@ -0,0 +1,120 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Product} from './Product.js';
+
+/**
+ * Defines experiment options for Puppeteer.
+ *
+ * See individual properties for more information.
+ *
+ * @public
+ */
+export type ExperimentsConfiguration = Record<string, never>;
+
+/**
+ * Defines options to configure Puppeteer's behavior during installation and
+ * runtime.
+ *
+ * See individual properties for more information.
+ *
+ * @public
+ */
+export interface Configuration {
+ /**
+ * Specifies a certain version of the browser you'd like Puppeteer to use.
+ *
+ * Can be overridden by `PUPPETEER_BROWSER_REVISION`.
+ *
+ * See {@link PuppeteerNode.launch | puppeteer.launch} on how executable path
+ * is inferred.
+ *
+ * @defaultValue A compatible-revision of the browser.
+ */
+ browserRevision?: string;
+ /**
+ * Defines the directory to be used by Puppeteer for caching.
+ *
+ * Can be overridden by `PUPPETEER_CACHE_DIR`.
+ *
+ * @defaultValue `path.join(os.homedir(), '.cache', 'puppeteer')`
+ */
+ cacheDirectory?: string;
+ /**
+ * Specifies the URL prefix that is used to download the browser.
+ *
+ * Can be overridden by `PUPPETEER_DOWNLOAD_BASE_URL`.
+ *
+ * @remarks
+ * This must include the protocol and may even need a path prefix.
+ *
+ * @defaultValue Either https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing or
+ * https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central,
+ * depending on the product.
+ */
+ downloadBaseUrl?: string;
+ /**
+ * Specifies the path for the downloads folder.
+ *
+ * Can be overridden by `PUPPETEER_DOWNLOAD_PATH`.
+ *
+ * @defaultValue `<cacheDirectory>`
+ */
+ downloadPath?: string;
+ /**
+ * Specifies an executable path to be used in
+ * {@link PuppeteerNode.launch | puppeteer.launch}.
+ *
+ * Can be overridden by `PUPPETEER_EXECUTABLE_PATH`.
+ *
+ * @defaultValue **Auto-computed.**
+ */
+ executablePath?: string;
+ /**
+ * Specifies which browser you'd like Puppeteer to use.
+ *
+ * Can be overridden by `PUPPETEER_PRODUCT`.
+ *
+ * @defaultValue `chrome`
+ */
+ defaultProduct?: Product;
+ /**
+ * Defines the directory to be used by Puppeteer for creating temporary files.
+ *
+ * Can be overridden by `PUPPETEER_TMP_DIR`.
+ *
+ * @defaultValue `os.tmpdir()`
+ */
+ temporaryDirectory?: string;
+ /**
+ * Tells Puppeteer to not download during installation.
+ *
+ * Can be overridden by `PUPPETEER_SKIP_DOWNLOAD`.
+ */
+ skipDownload?: boolean;
+ /**
+ * Tells Puppeteer to not Chrome download during installation.
+ *
+ * Can be overridden by `PUPPETEER_SKIP_CHROME_DOWNLOAD`.
+ */
+ skipChromeDownload?: boolean;
+ /**
+ * Tells Puppeteer to not chrome-headless-shell download during installation.
+ *
+ * Can be overridden by `PUPPETEER_SKIP_CHROME_HEADLESSS_HELL_DOWNLOAD`.
+ */
+ skipChromeHeadlessShellDownload?: boolean;
+ /**
+ * Tells Puppeteer to log at the given level.
+ *
+ * @defaultValue `warn`
+ */
+ logLevel?: 'silent' | 'error' | 'warn';
+ /**
+ * Defines experimental options for Puppeteer.
+ */
+ experiments?: ExperimentsConfiguration;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectOptions.ts
new file mode 100644
index 0000000000..ce46585162
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectOptions.ts
@@ -0,0 +1,77 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {
+ IsPageTargetCallback,
+ TargetFilterCallback,
+} from '../api/Browser.js';
+
+import type {ConnectionTransport} from './ConnectionTransport.js';
+import type {Viewport} from './Viewport.js';
+
+/**
+ * @public
+ */
+export type ProtocolType = 'cdp' | 'webDriverBiDi';
+
+/**
+ * Generic browser options that can be passed when launching any browser or when
+ * connecting to an existing browser instance.
+ * @public
+ */
+export interface BrowserConnectOptions {
+ /**
+ * Whether to ignore HTTPS errors during navigation.
+ * @defaultValue `false`
+ */
+ ignoreHTTPSErrors?: boolean;
+ /**
+ * Sets the viewport for each page.
+ *
+ * @defaultValue '\{width: 800, height: 600\}'
+ */
+ defaultViewport?: Viewport | null;
+ /**
+ * Slows down Puppeteer operations by the specified amount of milliseconds to
+ * aid debugging.
+ */
+ slowMo?: number;
+ /**
+ * Callback to decide if Puppeteer should connect to a given target or not.
+ */
+ targetFilter?: TargetFilterCallback;
+ /**
+ * @internal
+ */
+ _isPageTarget?: IsPageTargetCallback;
+
+ /**
+ * @defaultValue 'cdp'
+ * @public
+ */
+ protocol?: ProtocolType;
+ /**
+ * Timeout setting for individual protocol (CDP) calls.
+ *
+ * @defaultValue `180_000`
+ */
+ protocolTimeout?: number;
+}
+
+/**
+ * @public
+ */
+export interface ConnectOptions extends BrowserConnectOptions {
+ browserWSEndpoint?: string;
+ browserURL?: string;
+ transport?: ConnectionTransport;
+ /**
+ * Headers to use for the web socket connection.
+ * @remarks
+ * Only works in the Node.js environment.
+ */
+ headers?: Record<string, string>;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts
new file mode 100644
index 0000000000..ff36a2557a
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @public
+ */
+export interface ConnectionTransport {
+ send(message: string): void;
+ close(): void;
+ onmessage?: (message: string) => void;
+ onclose?: () => void;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts
new file mode 100644
index 0000000000..85d2db9f75
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts
@@ -0,0 +1,113 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JSHandle} from '../api/JSHandle.js';
+
+/**
+ * @public
+ */
+export interface ConsoleMessageLocation {
+ /**
+ * URL of the resource if known or `undefined` otherwise.
+ */
+ url?: string;
+
+ /**
+ * 0-based line number in the resource if known or `undefined` otherwise.
+ */
+ lineNumber?: number;
+
+ /**
+ * 0-based column number in the resource if known or `undefined` otherwise.
+ */
+ columnNumber?: number;
+}
+
+/**
+ * The supported types for console messages.
+ * @public
+ */
+export type ConsoleMessageType =
+ | 'log'
+ | 'debug'
+ | 'info'
+ | 'error'
+ | 'warning'
+ | 'dir'
+ | 'dirxml'
+ | 'table'
+ | 'trace'
+ | 'clear'
+ | 'startGroup'
+ | 'startGroupCollapsed'
+ | 'endGroup'
+ | 'assert'
+ | 'profile'
+ | 'profileEnd'
+ | 'count'
+ | 'timeEnd'
+ | 'verbose';
+
+/**
+ * ConsoleMessage objects are dispatched by page via the 'console' event.
+ * @public
+ */
+export class ConsoleMessage {
+ #type: ConsoleMessageType;
+ #text: string;
+ #args: JSHandle[];
+ #stackTraceLocations: ConsoleMessageLocation[];
+
+ /**
+ * @public
+ */
+ constructor(
+ type: ConsoleMessageType,
+ text: string,
+ args: JSHandle[],
+ stackTraceLocations: ConsoleMessageLocation[]
+ ) {
+ this.#type = type;
+ this.#text = text;
+ this.#args = args;
+ this.#stackTraceLocations = stackTraceLocations;
+ }
+
+ /**
+ * The type of the console message.
+ */
+ type(): ConsoleMessageType {
+ return this.#type;
+ }
+
+ /**
+ * The text of the console message.
+ */
+ text(): string {
+ return this.#text;
+ }
+
+ /**
+ * An array of arguments passed to the console.
+ */
+ args(): JSHandle[] {
+ return this.#args;
+ }
+
+ /**
+ * The location of the console message.
+ */
+ location(): ConsoleMessageLocation {
+ return this.#stackTraceLocations[0] ?? {};
+ }
+
+ /**
+ * The array of locations on the stack of the console message.
+ */
+ stackTrace(): ConsoleMessageLocation[] {
+ return this.#stackTraceLocations;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts
new file mode 100644
index 0000000000..33e5f889c1
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts
@@ -0,0 +1,207 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type PuppeteerUtil from '../injected/injected.js';
+import {assert} from '../util/assert.js';
+import {interpolateFunction, stringifyFunction} from '../util/Function.js';
+
+import {
+ QueryHandler,
+ type QuerySelector,
+ type QuerySelectorAll,
+} from './QueryHandler.js';
+import {scriptInjector} from './ScriptInjector.js';
+
+/**
+ * @public
+ */
+export interface CustomQueryHandler {
+ /**
+ * Searches for a {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Node} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}.
+ */
+ queryOne?: (node: Node, selector: string) => Node | null;
+ /**
+ * Searches for some {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Nodes} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}.
+ */
+ queryAll?: (node: Node, selector: string) => Iterable<Node>;
+}
+
+/**
+ * The registry of {@link CustomQueryHandler | custom query handlers}.
+ *
+ * @example
+ *
+ * ```ts
+ * Puppeteer.customQueryHandlers.register('lit', { … });
+ * const aHandle = await page.$('lit/…');
+ * ```
+ *
+ * @internal
+ */
+export class CustomQueryHandlerRegistry {
+ #handlers = new Map<
+ string,
+ [registerScript: string, Handler: typeof QueryHandler]
+ >();
+
+ get(name: string): typeof QueryHandler | undefined {
+ const handler = this.#handlers.get(name);
+ return handler ? handler[1] : undefined;
+ }
+
+ /**
+ * Registers a {@link CustomQueryHandler | custom query handler}.
+ *
+ * @remarks
+ * After registration, the handler can be used everywhere where a selector is
+ * expected by prepending the selection string with `<name>/`. The name is
+ * only allowed to consist of lower- and upper case latin letters.
+ *
+ * @example
+ *
+ * ```ts
+ * Puppeteer.customQueryHandlers.register('lit', { … });
+ * const aHandle = await page.$('lit/…');
+ * ```
+ *
+ * @param name - Name to register under.
+ * @param queryHandler - {@link CustomQueryHandler | Custom query handler} to
+ * register.
+ */
+ register(name: string, handler: CustomQueryHandler): void {
+ assert(
+ !this.#handlers.has(name),
+ `Cannot register over existing handler: ${name}`
+ );
+ assert(
+ /^[a-zA-Z]+$/.test(name),
+ `Custom query handler names may only contain [a-zA-Z]`
+ );
+ assert(
+ handler.queryAll || handler.queryOne,
+ `At least one query method must be implemented.`
+ );
+
+ const Handler = class extends QueryHandler {
+ static override querySelectorAll: QuerySelectorAll = interpolateFunction(
+ (node, selector, PuppeteerUtil) => {
+ return PuppeteerUtil.customQuerySelectors
+ .get(PLACEHOLDER('name'))!
+ .querySelectorAll(node, selector);
+ },
+ {name: JSON.stringify(name)}
+ );
+ static override querySelector: QuerySelector = interpolateFunction(
+ (node, selector, PuppeteerUtil) => {
+ return PuppeteerUtil.customQuerySelectors
+ .get(PLACEHOLDER('name'))!
+ .querySelector(node, selector);
+ },
+ {name: JSON.stringify(name)}
+ );
+ };
+ const registerScript = interpolateFunction(
+ (PuppeteerUtil: PuppeteerUtil) => {
+ PuppeteerUtil.customQuerySelectors.register(PLACEHOLDER('name'), {
+ queryAll: PLACEHOLDER('queryAll'),
+ queryOne: PLACEHOLDER('queryOne'),
+ });
+ },
+ {
+ name: JSON.stringify(name),
+ queryAll: handler.queryAll
+ ? stringifyFunction(handler.queryAll)
+ : String(undefined),
+ queryOne: handler.queryOne
+ ? stringifyFunction(handler.queryOne)
+ : String(undefined),
+ }
+ ).toString();
+
+ this.#handlers.set(name, [registerScript, Handler]);
+ scriptInjector.append(registerScript);
+ }
+
+ /**
+ * Unregisters the {@link CustomQueryHandler | custom query handler} for the
+ * given name.
+ *
+ * @throws `Error` if there is no handler under the given name.
+ */
+ unregister(name: string): void {
+ const handler = this.#handlers.get(name);
+ if (!handler) {
+ throw new Error(`Cannot unregister unknown handler: ${name}`);
+ }
+ scriptInjector.pop(handler[0]);
+ this.#handlers.delete(name);
+ }
+
+ /**
+ * Gets the names of all {@link CustomQueryHandler | custom query handlers}.
+ */
+ names(): string[] {
+ return [...this.#handlers.keys()];
+ }
+
+ /**
+ * Unregisters all custom query handlers.
+ */
+ clear(): void {
+ for (const [registerScript] of this.#handlers) {
+ scriptInjector.pop(registerScript);
+ }
+ this.#handlers.clear();
+ }
+}
+
+/**
+ * @internal
+ */
+export const customQueryHandlers = new CustomQueryHandlerRegistry();
+
+/**
+ * @deprecated Import {@link Puppeteer} and use the static method
+ * {@link Puppeteer.registerCustomQueryHandler}
+ *
+ * @public
+ */
+export function registerCustomQueryHandler(
+ name: string,
+ handler: CustomQueryHandler
+): void {
+ customQueryHandlers.register(name, handler);
+}
+
+/**
+ * @deprecated Import {@link Puppeteer} and use the static method
+ * {@link Puppeteer.unregisterCustomQueryHandler}
+ *
+ * @public
+ */
+export function unregisterCustomQueryHandler(name: string): void {
+ customQueryHandlers.unregister(name);
+}
+
+/**
+ * @deprecated Import {@link Puppeteer} and use the static method
+ * {@link Puppeteer.customQueryHandlerNames}
+ *
+ * @public
+ */
+export function customQueryHandlerNames(): string[] {
+ return customQueryHandlers.names();
+}
+
+/**
+ * @deprecated Import {@link Puppeteer} and use the static method
+ * {@link Puppeteer.clearCustomQueryHandlers}
+ *
+ * @public
+ */
+export function clearCustomQueryHandlers(): void {
+ customQueryHandlers.clear();
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts
new file mode 100644
index 0000000000..06ac9f58f9
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts
@@ -0,0 +1,128 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type Debug from 'debug';
+
+import {isNode} from '../environment.js';
+
+declare global {
+ // eslint-disable-next-line no-var
+ var __PUPPETEER_DEBUG: string;
+}
+
+/**
+ * @internal
+ */
+let debugModule: typeof Debug | null = null;
+/**
+ * @internal
+ */
+export async function importDebug(): Promise<typeof Debug> {
+ if (!debugModule) {
+ debugModule = (await import('debug')).default;
+ }
+ return debugModule;
+}
+
+/**
+ * A debug function that can be used in any environment.
+ *
+ * @remarks
+ * If used in Node, it falls back to the
+ * {@link https://www.npmjs.com/package/debug | debug module}. In the browser it
+ * uses `console.log`.
+ *
+ * In Node, use the `DEBUG` environment variable to control logging:
+ *
+ * ```
+ * DEBUG=* // logs all channels
+ * DEBUG=foo // logs the `foo` channel
+ * DEBUG=foo* // logs any channels starting with `foo`
+ * ```
+ *
+ * In the browser, set `window.__PUPPETEER_DEBUG` to a string:
+ *
+ * ```
+ * window.__PUPPETEER_DEBUG='*'; // logs all channels
+ * window.__PUPPETEER_DEBUG='foo'; // logs the `foo` channel
+ * window.__PUPPETEER_DEBUG='foo*'; // logs any channels starting with `foo`
+ * ```
+ *
+ * @example
+ *
+ * ```
+ * const log = debug('Page');
+ *
+ * log('new page created')
+ * // logs "Page: new page created"
+ * ```
+ *
+ * @param prefix - this will be prefixed to each log.
+ * @returns a function that can be called to log to that debug channel.
+ *
+ * @internal
+ */
+export const debug = (prefix: string): ((...args: unknown[]) => void) => {
+ if (isNode) {
+ return async (...logArgs: unknown[]) => {
+ if (captureLogs) {
+ capturedLogs.push(prefix + logArgs);
+ }
+ (await importDebug())(prefix)(logArgs);
+ };
+ }
+
+ return (...logArgs: unknown[]): void => {
+ const debugLevel = (globalThis as any).__PUPPETEER_DEBUG;
+ if (!debugLevel) {
+ return;
+ }
+
+ const everythingShouldBeLogged = debugLevel === '*';
+
+ const prefixMatchesDebugLevel =
+ everythingShouldBeLogged ||
+ /**
+ * If the debug level is `foo*`, that means we match any prefix that
+ * starts with `foo`. If the level is `foo`, we match only the prefix
+ * `foo`.
+ */
+ (debugLevel.endsWith('*')
+ ? prefix.startsWith(debugLevel)
+ : prefix === debugLevel);
+
+ if (!prefixMatchesDebugLevel) {
+ return;
+ }
+
+ // eslint-disable-next-line no-console
+ console.log(`${prefix}:`, ...logArgs);
+ };
+};
+
+/**
+ * @internal
+ */
+let capturedLogs: string[] = [];
+/**
+ * @internal
+ */
+let captureLogs = false;
+
+/**
+ * @internal
+ */
+export function setLogCapture(value: boolean): void {
+ capturedLogs = [];
+ captureLogs = value;
+}
+
+/**
+ * @internal
+ */
+export function getCapturedLogs(): string[] {
+ return capturedLogs;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts
new file mode 100644
index 0000000000..dbf5c13c95
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts
@@ -0,0 +1,1552 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Viewport} from './Viewport.js';
+
+/**
+ * @public
+ */
+export interface Device {
+ userAgent: string;
+ viewport: Viewport;
+}
+
+const knownDevices = [
+ {
+ name: 'Blackberry PlayBook',
+ userAgent:
+ 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
+ viewport: {
+ width: 600,
+ height: 1024,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Blackberry PlayBook landscape',
+ userAgent:
+ 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
+ viewport: {
+ width: 1024,
+ height: 600,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'BlackBerry Z30',
+ userAgent:
+ 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'BlackBerry Z30 landscape',
+ userAgent:
+ 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy Note 3',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy Note 3 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy Note II',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy Note II landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy S III',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy S III landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy S5',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy S5 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy S8',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36',
+ viewport: {
+ width: 360,
+ height: 740,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy S8 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36',
+ viewport: {
+ width: 740,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy S9+',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36',
+ viewport: {
+ width: 320,
+ height: 658,
+ deviceScaleFactor: 4.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy S9+ landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36',
+ viewport: {
+ width: 658,
+ height: 320,
+ deviceScaleFactor: 4.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy Tab S4',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36',
+ viewport: {
+ width: 712,
+ height: 1138,
+ deviceScaleFactor: 2.25,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy Tab S4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36',
+ viewport: {
+ width: 1138,
+ height: 712,
+ deviceScaleFactor: 2.25,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 768,
+ height: 1024,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 1024,
+ height: 768,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad (gen 6)',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 768,
+ height: 1024,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad (gen 6) landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 1024,
+ height: 768,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad (gen 7)',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 810,
+ height: 1080,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad (gen 7) landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 1080,
+ height: 810,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad Mini',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 768,
+ height: 1024,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad Mini landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 1024,
+ height: 768,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad Pro',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 1024,
+ height: 1366,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad Pro landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 1366,
+ height: 1024,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad Pro 11',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 834,
+ height: 1194,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad Pro 11 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 1194,
+ height: 834,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 4',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
+ viewport: {
+ width: 320,
+ height: 480,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
+ viewport: {
+ width: 480,
+ height: 320,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 5',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+ viewport: {
+ width: 320,
+ height: 568,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 5 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+ viewport: {
+ width: 568,
+ height: 320,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 6',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 667,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 6 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 667,
+ height: 375,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 6 Plus',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 736,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 6 Plus landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 736,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 7',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 667,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 7 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 667,
+ height: 375,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 7 Plus',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 736,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 7 Plus landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 736,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 8',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 667,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 8 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 667,
+ height: 375,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 8 Plus',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 736,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 8 Plus landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 736,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone SE',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+ viewport: {
+ width: 320,
+ height: 568,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone SE landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+ viewport: {
+ width: 568,
+ height: 320,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone X',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 812,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone X landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 812,
+ height: 375,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone XR',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 896,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone XR landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 896,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 11',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 828,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 11 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 828,
+ height: 414,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 11 Pro',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 812,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 11 Pro landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 812,
+ height: 375,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 11 Pro Max',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 896,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 11 Pro Max landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 896,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 12',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 390,
+ height: 844,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 12 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 844,
+ height: 390,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 12 Pro',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 390,
+ height: 844,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 12 Pro landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 844,
+ height: 390,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 12 Pro Max',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 428,
+ height: 926,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 12 Pro Max landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 926,
+ height: 428,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 12 Mini',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 812,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 12 Mini landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 812,
+ height: 375,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 13',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 390,
+ height: 844,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 13 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 844,
+ height: 390,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 13 Pro',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 390,
+ height: 844,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 13 Pro landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 844,
+ height: 390,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 13 Pro Max',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 428,
+ height: 926,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 13 Pro Max landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 926,
+ height: 428,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 13 Mini',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 812,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 13 Mini landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 812,
+ height: 375,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'JioPhone 2',
+ userAgent:
+ 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
+ viewport: {
+ width: 240,
+ height: 320,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'JioPhone 2 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
+ viewport: {
+ width: 320,
+ height: 240,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Kindle Fire HDX',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
+ viewport: {
+ width: 800,
+ height: 1280,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Kindle Fire HDX landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
+ viewport: {
+ width: 1280,
+ height: 800,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'LG Optimus L70',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 384,
+ height: 640,
+ deviceScaleFactor: 1.25,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'LG Optimus L70 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 384,
+ deviceScaleFactor: 1.25,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Microsoft Lumia 550',
+ userAgent:
+ 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Microsoft Lumia 950',
+ userAgent:
+ 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 4,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Microsoft Lumia 950 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 4,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 10',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
+ viewport: {
+ width: 800,
+ height: 1280,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 10 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
+ viewport: {
+ width: 1280,
+ height: 800,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 4',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 384,
+ height: 640,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 384,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 5',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 5 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 5X',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 412,
+ height: 732,
+ deviceScaleFactor: 2.625,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 5X landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 732,
+ height: 412,
+ deviceScaleFactor: 2.625,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 6',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 412,
+ height: 732,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 6 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 732,
+ height: 412,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 6P',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 412,
+ height: 732,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 6P landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 732,
+ height: 412,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 7',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
+ viewport: {
+ width: 600,
+ height: 960,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 7 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
+ viewport: {
+ width: 960,
+ height: 600,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nokia Lumia 520',
+ userAgent:
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
+ viewport: {
+ width: 320,
+ height: 533,
+ deviceScaleFactor: 1.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nokia Lumia 520 landscape',
+ userAgent:
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
+ viewport: {
+ width: 533,
+ height: 320,
+ deviceScaleFactor: 1.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nokia N9',
+ userAgent:
+ 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
+ viewport: {
+ width: 480,
+ height: 854,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nokia N9 landscape',
+ userAgent:
+ 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
+ viewport: {
+ width: 854,
+ height: 480,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 2',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 411,
+ height: 731,
+ deviceScaleFactor: 2.625,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 2 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 731,
+ height: 411,
+ deviceScaleFactor: 2.625,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 2 XL',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 411,
+ height: 823,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 2 XL landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 823,
+ height: 411,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 3',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36',
+ viewport: {
+ width: 393,
+ height: 786,
+ deviceScaleFactor: 2.75,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 3 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36',
+ viewport: {
+ width: 786,
+ height: 393,
+ deviceScaleFactor: 2.75,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 4',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36',
+ viewport: {
+ width: 353,
+ height: 745,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36',
+ viewport: {
+ width: 745,
+ height: 353,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 4a (5G)',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 353,
+ height: 745,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 4a (5G) landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 745,
+ height: 353,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 5',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 393,
+ height: 851,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 5 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 851,
+ height: 393,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Moto G4',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Moto G4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+] as const;
+
+const knownDevicesByName = {} as Record<
+ (typeof knownDevices)[number]['name'],
+ Device
+>;
+
+for (const device of knownDevices) {
+ knownDevicesByName[device.name] = device;
+}
+
+/**
+ * A list of devices to be used with {@link Page.emulate}.
+ *
+ * @example
+ *
+ * ```ts
+ * import {KnownDevices} from 'puppeteer';
+ * const iPhone = KnownDevices['iPhone 6'];
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.emulate(iPhone);
+ * await page.goto('https://www.google.com');
+ * // other actions...
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @public
+ */
+export const KnownDevices = Object.freeze(knownDevicesByName);
+
+/**
+ * @deprecated Import {@link KnownDevices}
+ *
+ * @public
+ */
+export const devices = KnownDevices;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts
new file mode 100644
index 0000000000..8225d64f07
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts
@@ -0,0 +1,124 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @deprecated Do not use.
+ *
+ * @public
+ */
+export class CustomError extends Error {
+ /**
+ * @internal
+ */
+ constructor(message?: string) {
+ super(message);
+ this.name = this.constructor.name;
+ }
+
+ /**
+ * @internal
+ */
+ get [Symbol.toStringTag](): string {
+ return this.constructor.name;
+ }
+}
+
+/**
+ * TimeoutError is emitted whenever certain operations are terminated due to
+ * timeout.
+ *
+ * @remarks
+ * Example operations are {@link Page.waitForSelector | page.waitForSelector} or
+ * {@link PuppeteerNode.launch | puppeteer.launch}.
+ *
+ * @public
+ */
+export class TimeoutError extends CustomError {}
+
+/**
+ * ProtocolError is emitted whenever there is an error from the protocol.
+ *
+ * @public
+ */
+export class ProtocolError extends CustomError {
+ #code?: number;
+ #originalMessage = '';
+
+ set code(code: number | undefined) {
+ this.#code = code;
+ }
+ /**
+ * @readonly
+ * @public
+ */
+ get code(): number | undefined {
+ return this.#code;
+ }
+
+ set originalMessage(originalMessage: string) {
+ this.#originalMessage = originalMessage;
+ }
+ /**
+ * @readonly
+ * @public
+ */
+ get originalMessage(): string {
+ return this.#originalMessage;
+ }
+}
+
+/**
+ * Puppeteer will throw this error if a method is not
+ * supported by the currently used protocol
+ *
+ * @public
+ */
+export class UnsupportedOperation extends CustomError {}
+
+/**
+ * @internal
+ */
+export class TargetCloseError extends ProtocolError {}
+
+/**
+ * @deprecated Do not use.
+ *
+ * @public
+ */
+export interface PuppeteerErrors {
+ TimeoutError: typeof TimeoutError;
+ ProtocolError: typeof ProtocolError;
+}
+
+/**
+ * @deprecated Import error classes directly.
+ *
+ * Puppeteer methods might throw errors if they are unable to fulfill a request.
+ * For example, `page.waitForSelector(selector[, options])` might fail if the
+ * selector doesn't match any nodes during the given timeframe.
+ *
+ * For certain types of errors Puppeteer uses specific error classes. These
+ * classes are available via `puppeteer.errors`.
+ *
+ * @example
+ * An example of handling a timeout error:
+ *
+ * ```ts
+ * try {
+ * await page.waitForSelector('.foo');
+ * } catch (e) {
+ * if (e instanceof TimeoutError) {
+ * // Do something if this is a timeout.
+ * }
+ * }
+ * ```
+ *
+ * @public
+ */
+export const errors: PuppeteerErrors = Object.freeze({
+ TimeoutError,
+ ProtocolError,
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts
new file mode 100644
index 0000000000..cf05ef6700
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts
@@ -0,0 +1,185 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it, beforeEach} from 'node:test';
+
+import expect from 'expect';
+import sinon from 'sinon';
+
+import {EventEmitter} from './EventEmitter.js';
+
+describe('EventEmitter', () => {
+ let emitter: EventEmitter<Record<string, unknown>>;
+
+ beforeEach(() => {
+ emitter = new EventEmitter();
+ });
+
+ describe('on', () => {
+ const onTests = (methodName: 'on' | 'addListener'): void => {
+ it(`${methodName}: adds an event listener that is fired when the event is emitted`, () => {
+ const listener = sinon.spy();
+ emitter[methodName]('foo', listener);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it(`${methodName} sends the event data to the handler`, () => {
+ const listener = sinon.spy();
+ const data = {};
+ emitter[methodName]('foo', listener);
+ emitter.emit('foo', data);
+ expect(listener.callCount).toEqual(1);
+ expect(listener.firstCall.args[0]).toBe(data);
+ });
+
+ it(`${methodName}: supports chaining`, () => {
+ const listener = sinon.spy();
+ const returnValue = emitter[methodName]('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ };
+ onTests('on');
+ // we support addListener for legacy reasons
+ onTests('addListener');
+ });
+
+ describe('off', () => {
+ const offTests = (methodName: 'off' | 'removeListener'): void => {
+ it(`${methodName}: removes the listener so it is no longer called`, () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ emitter.off('foo', listener);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it(`${methodName}: supports chaining`, () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ const returnValue = emitter.off('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ };
+ offTests('off');
+ // we support removeListener for legacy reasons
+ offTests('removeListener');
+ });
+
+ describe('once', () => {
+ it('only calls the listener once and then removes it', () => {
+ const listener = sinon.spy();
+ emitter.once('foo', listener);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it('supports chaining', () => {
+ const listener = sinon.spy();
+ const returnValue = emitter.once('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ });
+
+ describe('emit', () => {
+ it('calls all the listeners for an event', () => {
+ const listener1 = sinon.spy();
+ const listener2 = sinon.spy();
+ const listener3 = sinon.spy();
+ emitter.on('foo', listener1).on('foo', listener2).on('bar', listener3);
+
+ emitter.emit('foo', undefined);
+
+ expect(listener1.callCount).toEqual(1);
+ expect(listener2.callCount).toEqual(1);
+ expect(listener3.callCount).toEqual(0);
+ });
+
+ it('passes data through to the listener', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ const data = {};
+
+ emitter.emit('foo', data);
+ expect(listener.callCount).toEqual(1);
+ expect(listener.firstCall.args[0]).toBe(data);
+ });
+
+ it('returns true if the event has listeners', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ expect(emitter.emit('foo', undefined)).toBe(true);
+ });
+
+ it('returns false if the event has listeners', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ expect(emitter.emit('notFoo', undefined)).toBe(false);
+ });
+ });
+
+ describe('listenerCount', () => {
+ it('returns the number of listeners for the given event', () => {
+ emitter.on('foo', () => {});
+ emitter.on('foo', () => {});
+ emitter.on('bar', () => {});
+ expect(emitter.listenerCount('foo')).toEqual(2);
+ expect(emitter.listenerCount('bar')).toEqual(1);
+ expect(emitter.listenerCount('noListeners')).toEqual(0);
+ });
+ });
+
+ describe('removeAllListeners', () => {
+ it('removes every listener from all events by default', () => {
+ emitter.on('foo', () => {}).on('bar', () => {});
+
+ emitter.removeAllListeners();
+ expect(emitter.emit('foo', undefined)).toBe(false);
+ expect(emitter.emit('bar', undefined)).toBe(false);
+ });
+
+ it('returns the emitter for chaining', () => {
+ expect(emitter.removeAllListeners()).toBe(emitter);
+ });
+
+ it('can filter to remove only listeners for a given event name', () => {
+ emitter
+ .on('foo', () => {})
+ .on('bar', () => {})
+ .on('bar', () => {});
+
+ emitter.removeAllListeners('bar');
+ expect(emitter.emit('foo', undefined)).toBe(true);
+ expect(emitter.emit('bar', undefined)).toBe(false);
+ });
+ });
+
+ describe('dispose', () => {
+ it('should dispose higher order emitters properly', () => {
+ let values = '';
+ emitter.on('foo', () => {
+ values += '1';
+ });
+ const higherOrderEmitter = new EventEmitter(emitter);
+
+ higherOrderEmitter.on('foo', () => {
+ values += '2';
+ });
+ higherOrderEmitter.emit('foo', undefined);
+
+ expect(values).toMatch('12');
+
+ higherOrderEmitter.off('foo');
+ higherOrderEmitter.emit('foo', undefined);
+
+ expect(values).toMatch('121');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts
new file mode 100644
index 0000000000..4a8bcb801f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts
@@ -0,0 +1,253 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import mitt, {type Emitter} from '../../third_party/mitt/mitt.js';
+import {disposeSymbol} from '../util/disposable.js';
+
+/**
+ * @public
+ */
+export type EventType = string | symbol;
+
+/**
+ * @public
+ */
+export type Handler<T = unknown> = (event: T) => void;
+
+/**
+ * @public
+ */
+export interface CommonEventEmitter<Events extends Record<EventType, unknown>> {
+ on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): this;
+ off<Key extends keyof Events>(
+ type: Key,
+ handler?: Handler<Events[Key]>
+ ): this;
+ emit<Key extends keyof Events>(type: Key, event: Events[Key]): boolean;
+ /* To maintain parity with the built in NodeJS event emitter which uses removeListener
+ * rather than `off`.
+ * If you're implementing new code you should use `off`.
+ */
+ addListener<Key extends keyof Events>(
+ type: Key,
+ handler: Handler<Events[Key]>
+ ): this;
+ removeListener<Key extends keyof Events>(
+ type: Key,
+ handler: Handler<Events[Key]>
+ ): this;
+ once<Key extends keyof Events>(
+ type: Key,
+ handler: Handler<Events[Key]>
+ ): this;
+ listenerCount(event: keyof Events): number;
+
+ removeAllListeners(event?: keyof Events): this;
+}
+
+/**
+ * @public
+ */
+export type EventsWithWildcard<Events extends Record<EventType, unknown>> =
+ Events & {
+ '*': Events[keyof Events];
+ };
+
+/**
+ * The EventEmitter class that many Puppeteer classes extend.
+ *
+ * @remarks
+ *
+ * This allows you to listen to events that Puppeteer classes fire and act
+ * accordingly. Therefore you'll mostly use {@link EventEmitter.on | on} and
+ * {@link EventEmitter.off | off} to bind
+ * and unbind to event listeners.
+ *
+ * @public
+ */
+export class EventEmitter<Events extends Record<EventType, unknown>>
+ implements CommonEventEmitter<EventsWithWildcard<Events>>
+{
+ #emitter: Emitter<EventsWithWildcard<Events>> | EventEmitter<Events>;
+ #handlers = new Map<keyof Events | '*', Array<Handler<any>>>();
+
+ /**
+ * If you pass an emitter, the returned emitter will wrap the passed emitter.
+ *
+ * @internal
+ */
+ constructor(
+ emitter: Emitter<EventsWithWildcard<Events>> | EventEmitter<Events> = mitt(
+ new Map()
+ )
+ ) {
+ this.#emitter = emitter;
+ }
+
+ /**
+ * Bind an event listener to fire when an event occurs.
+ * @param type - the event type you'd like to listen to. Can be a string or symbol.
+ * @param handler - the function to be called when the event occurs.
+ * @returns `this` to enable you to chain method calls.
+ */
+ on<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ const handlers = this.#handlers.get(type);
+ if (handlers === undefined) {
+ this.#handlers.set(type, [handler]);
+ } else {
+ handlers.push(handler);
+ }
+
+ this.#emitter.on(type, handler);
+ return this;
+ }
+
+ /**
+ * Remove an event listener from firing.
+ * @param type - the event type you'd like to stop listening to.
+ * @param handler - the function that should be removed.
+ * @returns `this` to enable you to chain method calls.
+ */
+ off<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler?: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ const handlers = this.#handlers.get(type) ?? [];
+ if (handler === undefined) {
+ for (const handler of handlers) {
+ this.#emitter.off(type, handler);
+ }
+ this.#handlers.delete(type);
+ return this;
+ }
+ const index = handlers.lastIndexOf(handler);
+ if (index > -1) {
+ this.#emitter.off(type, ...handlers.splice(index, 1));
+ }
+ return this;
+ }
+
+ /**
+ * Emit an event and call any associated listeners.
+ *
+ * @param type - the event you'd like to emit
+ * @param eventData - any data you'd like to emit with the event
+ * @returns `true` if there are any listeners, `false` if there are not.
+ */
+ emit<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ event: EventsWithWildcard<Events>[Key]
+ ): boolean {
+ this.#emitter.emit(type, event);
+ return this.listenerCount(type) > 0;
+ }
+
+ /**
+ * Remove an event listener.
+ *
+ * @deprecated please use {@link EventEmitter.off} instead.
+ */
+ removeListener<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ return this.off(type, handler);
+ }
+
+ /**
+ * Add an event listener.
+ *
+ * @deprecated please use {@link EventEmitter.on} instead.
+ */
+ addListener<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ return this.on(type, handler);
+ }
+
+ /**
+ * Like `on` but the listener will only be fired once and then it will be removed.
+ * @param type - the event you'd like to listen to
+ * @param handler - the handler function to run when the event occurs
+ * @returns `this` to enable you to chain method calls.
+ */
+ once<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ const onceHandler: Handler<EventsWithWildcard<Events>[Key]> = eventData => {
+ handler(eventData);
+ this.off(type, onceHandler);
+ };
+
+ return this.on(type, onceHandler);
+ }
+
+ /**
+ * Gets the number of listeners for a given event.
+ *
+ * @param type - the event to get the listener count for
+ * @returns the number of listeners bound to the given event
+ */
+ listenerCount(type: keyof EventsWithWildcard<Events>): number {
+ return this.#handlers.get(type)?.length || 0;
+ }
+
+ /**
+ * Removes all listeners. If given an event argument, it will remove only
+ * listeners for that event.
+ *
+ * @param type - the event to remove listeners for.
+ * @returns `this` to enable you to chain method calls.
+ */
+ removeAllListeners(type?: keyof EventsWithWildcard<Events>): this {
+ if (type !== undefined) {
+ return this.off(type);
+ }
+ this[disposeSymbol]();
+ return this;
+ }
+
+ /**
+ * @internal
+ */
+ [disposeSymbol](): void {
+ for (const [type, handlers] of this.#handlers) {
+ for (const handler of handlers) {
+ this.#emitter.off(type, handler);
+ }
+ }
+ this.#handlers.clear();
+ }
+}
+
+/**
+ * @internal
+ */
+export class EventSubscription<
+ Target extends CommonEventEmitter<Record<Type, Event>>,
+ Type extends EventType = EventType,
+ Event = unknown,
+> {
+ #target: Target;
+ #type: Type;
+ #handler: Handler<Event>;
+
+ constructor(target: Target, type: Type, handler: Handler<Event>) {
+ this.#target = target;
+ this.#type = type;
+ this.#handler = handler;
+ this.#target.on(this.#type, this.#handler);
+ }
+
+ [disposeSymbol](): void {
+ this.#target.off(this.#type, this.#handler);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts
new file mode 100644
index 0000000000..2e4fd14fa7
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts
@@ -0,0 +1,92 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import {assert} from '../util/assert.js';
+
+/**
+ * File choosers let you react to the page requesting for a file.
+ *
+ * @remarks
+ * `FileChooser` instances are returned via the {@link Page.waitForFileChooser} method.
+ *
+ * In browsers, only one file chooser can be opened at a time.
+ * All file choosers must be accepted or canceled. Not doing so will prevent
+ * subsequent file choosers from appearing.
+ *
+ * @example
+ *
+ * ```ts
+ * const [fileChooser] = await Promise.all([
+ * page.waitForFileChooser(),
+ * page.click('#upload-file-button'), // some button that triggers file selection
+ * ]);
+ * await fileChooser.accept(['/tmp/myfile.pdf']);
+ * ```
+ *
+ * @public
+ */
+export class FileChooser {
+ #element: ElementHandle<HTMLInputElement>;
+ #multiple: boolean;
+ #handled = false;
+
+ /**
+ * @internal
+ */
+ constructor(
+ element: ElementHandle<HTMLInputElement>,
+ event: Protocol.Page.FileChooserOpenedEvent
+ ) {
+ this.#element = element;
+ this.#multiple = event.mode !== 'selectSingle';
+ }
+
+ /**
+ * Whether file chooser allow for
+ * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple | multiple}
+ * file selection.
+ */
+ isMultiple(): boolean {
+ return this.#multiple;
+ }
+
+ /**
+ * Accept the file chooser request with the given file paths.
+ *
+ * @remarks This will not validate whether the file paths exists. Also, if a
+ * path is relative, then it is resolved against the
+ * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}.
+ * For locals script connecting to remote chrome environments, paths must be
+ * absolute.
+ */
+ async accept(paths: string[]): Promise<void> {
+ assert(
+ !this.#handled,
+ 'Cannot accept FileChooser which is already handled!'
+ );
+ this.#handled = true;
+ await this.#element.uploadFile(...paths);
+ }
+
+ /**
+ * Closes the file chooser without selecting any files.
+ */
+ async cancel(): Promise<void> {
+ assert(
+ !this.#handled,
+ 'Cannot cancel FileChooser which is already handled!'
+ );
+ this.#handled = true;
+ // XXX: These events should converted to trusted events. Perhaps do this
+ // in `DOM.setFileInputFiles`?
+ await this.#element.evaluate(element => {
+ element.dispatchEvent(new Event('cancel', {bubbles: true}));
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts
new file mode 100644
index 0000000000..1d8bb01414
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ARIAQueryHandler} from '../cdp/AriaQueryHandler.js';
+
+import {customQueryHandlers} from './CustomQueryHandler.js';
+import {PierceQueryHandler} from './PierceQueryHandler.js';
+import {PQueryHandler} from './PQueryHandler.js';
+import type {QueryHandler} from './QueryHandler.js';
+import {TextQueryHandler} from './TextQueryHandler.js';
+import {XPathQueryHandler} from './XPathQueryHandler.js';
+
+const BUILTIN_QUERY_HANDLERS = {
+ aria: ARIAQueryHandler,
+ pierce: PierceQueryHandler,
+ xpath: XPathQueryHandler,
+ text: TextQueryHandler,
+} as const;
+
+const QUERY_SEPARATORS = ['=', '/'];
+
+/**
+ * @internal
+ */
+export function getQueryHandlerAndSelector(selector: string): {
+ updatedSelector: string;
+ QueryHandler: typeof QueryHandler;
+} {
+ for (const handlerMap of [
+ customQueryHandlers.names().map(name => {
+ return [name, customQueryHandlers.get(name)!] as const;
+ }),
+ Object.entries(BUILTIN_QUERY_HANDLERS),
+ ]) {
+ for (const [name, QueryHandler] of handlerMap) {
+ for (const separator of QUERY_SEPARATORS) {
+ const prefix = `${name}${separator}`;
+ if (selector.startsWith(prefix)) {
+ selector = selector.slice(prefix.length);
+ return {updatedSelector: selector, QueryHandler};
+ }
+ }
+ }
+ }
+ return {updatedSelector: selector, QueryHandler: PQueryHandler};
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts
new file mode 100644
index 0000000000..c88003ed71
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JSHandle} from '../api/JSHandle.js';
+import {DisposableStack, disposeSymbol} from '../util/disposable.js';
+
+import type {AwaitableIterable, HandleFor} from './types.js';
+
+const DEFAULT_BATCH_SIZE = 20;
+
+/**
+ * This will transpose an iterator JSHandle into a fast, Puppeteer-side iterator
+ * of JSHandles.
+ *
+ * @param size - The number of elements to transpose. This should be something
+ * reasonable.
+ */
+async function* fastTransposeIteratorHandle<T>(
+ iterator: JSHandle<AwaitableIterator<T>>,
+ size: number
+) {
+ using array = await iterator.evaluateHandle(async (iterator, size) => {
+ const results = [];
+ while (results.length < size) {
+ const result = await iterator.next();
+ if (result.done) {
+ break;
+ }
+ results.push(result.value);
+ }
+ return results;
+ }, size);
+ const properties = (await array.getProperties()) as Map<string, HandleFor<T>>;
+ const handles = properties.values();
+ using stack = new DisposableStack();
+ stack.defer(() => {
+ for (using handle of handles) {
+ handle[disposeSymbol]();
+ }
+ });
+ yield* handles;
+ return properties.size === 0;
+}
+
+/**
+ * This will transpose an iterator JSHandle in batches based on the default size
+ * of {@link fastTransposeIteratorHandle}.
+ */
+
+async function* transposeIteratorHandle<T>(
+ iterator: JSHandle<AwaitableIterator<T>>
+) {
+ let size = DEFAULT_BATCH_SIZE;
+ while (!(yield* fastTransposeIteratorHandle(iterator, size))) {
+ size <<= 1;
+ }
+}
+
+type AwaitableIterator<T> = Iterator<T> | AsyncIterator<T>;
+
+/**
+ * @internal
+ */
+export async function* transposeIterableHandle<T>(
+ handle: JSHandle<AwaitableIterable<T>>
+): AsyncIterableIterator<HandleFor<T>> {
+ using generatorHandle = await handle.evaluateHandle(iterable => {
+ return (async function* () {
+ yield* iterable;
+ })();
+ });
+ yield* transposeIteratorHandle(generatorHandle);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts
new file mode 100644
index 0000000000..ed30281dd8
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JSHandle} from '../api/JSHandle.js';
+import type PuppeteerUtil from '../injected/injected.js';
+
+/**
+ * @internal
+ */
+export interface PuppeteerUtilWrapper {
+ puppeteerUtil: Promise<JSHandle<PuppeteerUtil>>;
+}
+
+/**
+ * @internal
+ */
+export class LazyArg<T, Context = PuppeteerUtilWrapper> {
+ static create = <T>(
+ get: (context: PuppeteerUtilWrapper) => Promise<T> | T
+ ): T => {
+ // We don't want to introduce LazyArg to the type system, otherwise we would
+ // have to make it public.
+ return new LazyArg(get) as unknown as T;
+ };
+
+ #get: (context: Context) => Promise<T> | T;
+ private constructor(get: (context: Context) => Promise<T> | T) {
+ this.#get = get;
+ }
+
+ async get(context: Context): Promise<T> {
+ return await this.#get(context);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManagerEvents.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManagerEvents.ts
new file mode 100644
index 0000000000..eae26252d1
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManagerEvents.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {HTTPRequest} from '../api/HTTPRequest.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+
+import type {EventType} from './EventEmitter.js';
+
+/**
+ * We use symbols to prevent any external parties listening to these events.
+ * They are internal to Puppeteer.
+ *
+ * @internal
+ */
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace NetworkManagerEvent {
+ export const Request = Symbol('NetworkManager.Request');
+ export const RequestServedFromCache = Symbol(
+ 'NetworkManager.RequestServedFromCache'
+ );
+ export const Response = Symbol('NetworkManager.Response');
+ export const RequestFailed = Symbol('NetworkManager.RequestFailed');
+ export const RequestFinished = Symbol('NetworkManager.RequestFinished');
+}
+
+/**
+ * @internal
+ */
+export interface NetworkManagerEvents extends Record<EventType, unknown> {
+ [NetworkManagerEvent.Request]: HTTPRequest;
+ [NetworkManagerEvent.RequestServedFromCache]: HTTPRequest | undefined;
+ [NetworkManagerEvent.Response]: HTTPResponse;
+ [NetworkManagerEvent.RequestFailed]: HTTPRequest;
+ [NetworkManagerEvent.RequestFinished]: HTTPRequest;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts
new file mode 100644
index 0000000000..7cae9191a9
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts
@@ -0,0 +1,217 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @public
+ */
+export interface PDFMargin {
+ top?: string | number;
+ bottom?: string | number;
+ left?: string | number;
+ right?: string | number;
+}
+
+/**
+ * @public
+ */
+export type LowerCasePaperFormat =
+ | 'letter'
+ | 'legal'
+ | 'tabloid'
+ | 'ledger'
+ | 'a0'
+ | 'a1'
+ | 'a2'
+ | 'a3'
+ | 'a4'
+ | 'a5'
+ | 'a6';
+
+/**
+ * All the valid paper format types when printing a PDF.
+ *
+ * @remarks
+ *
+ * The sizes of each format are as follows:
+ *
+ * - `Letter`: 8.5in x 11in
+ *
+ * - `Legal`: 8.5in x 14in
+ *
+ * - `Tabloid`: 11in x 17in
+ *
+ * - `Ledger`: 17in x 11in
+ *
+ * - `A0`: 33.1in x 46.8in
+ *
+ * - `A1`: 23.4in x 33.1in
+ *
+ * - `A2`: 16.54in x 23.4in
+ *
+ * - `A3`: 11.7in x 16.54in
+ *
+ * - `A4`: 8.27in x 11.7in
+ *
+ * - `A5`: 5.83in x 8.27in
+ *
+ * - `A6`: 4.13in x 5.83in
+ *
+ * @public
+ */
+export type PaperFormat =
+ | Uppercase<LowerCasePaperFormat>
+ | Capitalize<LowerCasePaperFormat>
+ | LowerCasePaperFormat;
+
+/**
+ * Valid options to configure PDF generation via {@link Page.pdf}.
+ * @public
+ */
+export interface PDFOptions {
+ /**
+ * Scales the rendering of the web page. Amount must be between `0.1` and `2`.
+ * @defaultValue `1`
+ */
+ scale?: number;
+ /**
+ * Whether to show the header and footer.
+ * @defaultValue `false`
+ */
+ displayHeaderFooter?: boolean;
+ /**
+ * HTML template for the print header. Should be valid HTML with the following
+ * classes used to inject values into them:
+ *
+ * - `date` formatted print date
+ *
+ * - `title` document title
+ *
+ * - `url` document location
+ *
+ * - `pageNumber` current page number
+ *
+ * - `totalPages` total pages in the document
+ */
+ headerTemplate?: string;
+ /**
+ * HTML template for the print footer. Has the same constraints and support
+ * for special classes as {@link PDFOptions | PDFOptions.headerTemplate}.
+ */
+ footerTemplate?: string;
+ /**
+ * Set to `true` to print background graphics.
+ * @defaultValue `false`
+ */
+ printBackground?: boolean;
+ /**
+ * Whether to print in landscape orientation.
+ * @defaultValue `false`
+ */
+ landscape?: boolean;
+ /**
+ * Paper ranges to print, e.g. `1-5, 8, 11-13`.
+ * @defaultValue The empty string, which means all pages are printed.
+ */
+ pageRanges?: string;
+ /**
+ * @remarks
+ * If set, this takes priority over the `width` and `height` options.
+ * @defaultValue `letter`.
+ */
+ format?: PaperFormat;
+ /**
+ * Sets the width of paper. You can pass in a number or a string with a unit.
+ */
+ width?: string | number;
+ /**
+ * Sets the height of paper. You can pass in a number or a string with a unit.
+ */
+ height?: string | number;
+ /**
+ * Give any CSS `@page` size declared in the page priority over what is
+ * declared in the `width` or `height` or `format` option.
+ * @defaultValue `false`, which will scale the content to fit the paper size.
+ */
+ preferCSSPageSize?: boolean;
+ /**
+ * Set the PDF margins.
+ * @defaultValue `undefined` no margins are set.
+ */
+ margin?: PDFMargin;
+ /**
+ * The path to save the file to.
+ *
+ * @remarks
+ *
+ * If the path is relative, it's resolved relative to the current working directory.
+ *
+ * @defaultValue `undefined`, which means the PDF will not be written to disk.
+ */
+ path?: string;
+ /**
+ * Hides default white background and allows generating pdfs with transparency.
+ * @defaultValue `false`
+ */
+ omitBackground?: boolean;
+ /**
+ * Generate tagged (accessible) PDF.
+ * @defaultValue `false`
+ * @experimental
+ */
+ tagged?: boolean;
+ /**
+ * Timeout in milliseconds. Pass `0` to disable timeout.
+ * @defaultValue `30_000`
+ */
+ timeout?: number;
+}
+
+/**
+ * @internal
+ */
+export interface PaperFormatDimensions {
+ width: number;
+ height: number;
+}
+
+/**
+ * @internal
+ */
+export interface ParsedPDFOptionsInterface {
+ width: number;
+ height: number;
+ margin: {
+ top: number;
+ bottom: number;
+ left: number;
+ right: number;
+ };
+}
+
+/**
+ * @internal
+ */
+export type ParsedPDFOptions = Required<
+ Omit<PDFOptions, 'path' | 'format' | 'timeout'> & ParsedPDFOptionsInterface
+>;
+
+/**
+ * @internal
+ */
+export const paperFormats: Record<LowerCasePaperFormat, PaperFormatDimensions> =
+ {
+ letter: {width: 8.5, height: 11},
+ legal: {width: 8.5, height: 14},
+ tabloid: {width: 11, height: 17},
+ ledger: {width: 17, height: 11},
+ a0: {width: 33.1, height: 46.8},
+ a1: {width: 23.4, height: 33.1},
+ a2: {width: 16.54, height: 23.4},
+ a3: {width: 11.7, height: 16.54},
+ a4: {width: 8.27, height: 11.7},
+ a5: {width: 5.83, height: 8.27},
+ a6: {width: 4.13, height: 5.83},
+ } as const;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts
new file mode 100644
index 0000000000..db9b832d77
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ QueryHandler,
+ type QuerySelector,
+ type QuerySelectorAll,
+} from './QueryHandler.js';
+
+/**
+ * @internal
+ */
+export class PQueryHandler extends QueryHandler {
+ static override querySelectorAll: QuerySelectorAll = (
+ element,
+ selector,
+ {pQuerySelectorAll}
+ ) => {
+ return pQuerySelectorAll(element, selector);
+ };
+ static override querySelector: QuerySelector = (
+ element,
+ selector,
+ {pQuerySelector}
+ ) => {
+ return pQuerySelector(element, selector);
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts
new file mode 100644
index 0000000000..36ddbe7f3e
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type PuppeteerUtil from '../injected/injected.js';
+
+import {QueryHandler} from './QueryHandler.js';
+
+/**
+ * @internal
+ */
+export class PierceQueryHandler extends QueryHandler {
+ static override querySelector = (
+ element: Node,
+ selector: string,
+ {pierceQuerySelector}: PuppeteerUtil
+ ): Node | null => {
+ return pierceQuerySelector(element, selector);
+ };
+ static override querySelectorAll = (
+ element: Node,
+ selector: string,
+ {pierceQuerySelectorAll}: PuppeteerUtil
+ ): Iterable<Node> => {
+ return pierceQuerySelectorAll(element, selector);
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts
new file mode 100644
index 0000000000..dcd75aceb6
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Supported products.
+ * @public
+ */
+export type Product = 'chrome' | 'firefox';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts
new file mode 100644
index 0000000000..844a3622bd
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts
@@ -0,0 +1,123 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Browser} from '../api/Browser.js';
+
+import {_connectToBrowser} from './BrowserConnector.js';
+import type {ConnectOptions} from './ConnectOptions.js';
+import {
+ type CustomQueryHandler,
+ customQueryHandlers,
+} from './CustomQueryHandler.js';
+
+/**
+ * Settings that are common to the Puppeteer class, regardless of environment.
+ *
+ * @internal
+ */
+export interface CommonPuppeteerSettings {
+ isPuppeteerCore: boolean;
+}
+
+/**
+ * The main Puppeteer class.
+ *
+ * IMPORTANT: if you are using Puppeteer in a Node environment, you will get an
+ * instance of {@link PuppeteerNode} when you import or require `puppeteer`.
+ * That class extends `Puppeteer`, so has all the methods documented below as
+ * well as all that are defined on {@link PuppeteerNode}.
+ *
+ * @public
+ */
+export class Puppeteer {
+ /**
+ * Operations for {@link CustomQueryHandler | custom query handlers}. See
+ * {@link CustomQueryHandlerRegistry}.
+ *
+ * @internal
+ */
+ static customQueryHandlers = customQueryHandlers;
+
+ /**
+ * Registers a {@link CustomQueryHandler | custom query handler}.
+ *
+ * @remarks
+ * After registration, the handler can be used everywhere where a selector is
+ * expected by prepending the selection string with `<name>/`. The name is only
+ * allowed to consist of lower- and upper case latin letters.
+ *
+ * @example
+ *
+ * ```
+ * puppeteer.registerCustomQueryHandler('text', { … });
+ * const aHandle = await page.$('text/…');
+ * ```
+ *
+ * @param name - The name that the custom query handler will be registered
+ * under.
+ * @param queryHandler - The {@link CustomQueryHandler | custom query handler}
+ * to register.
+ *
+ * @public
+ */
+ static registerCustomQueryHandler(
+ name: string,
+ queryHandler: CustomQueryHandler
+ ): void {
+ return this.customQueryHandlers.register(name, queryHandler);
+ }
+
+ /**
+ * Unregisters a custom query handler for a given name.
+ */
+ static unregisterCustomQueryHandler(name: string): void {
+ return this.customQueryHandlers.unregister(name);
+ }
+
+ /**
+ * Gets the names of all custom query handlers.
+ */
+ static customQueryHandlerNames(): string[] {
+ return this.customQueryHandlers.names();
+ }
+
+ /**
+ * Unregisters all custom query handlers.
+ */
+ static clearCustomQueryHandlers(): void {
+ return this.customQueryHandlers.clear();
+ }
+
+ /**
+ * @internal
+ */
+ _isPuppeteerCore: boolean;
+ /**
+ * @internal
+ */
+ protected _changedProduct = false;
+
+ /**
+ * @internal
+ */
+ constructor(settings: CommonPuppeteerSettings) {
+ this._isPuppeteerCore = settings.isPuppeteerCore;
+
+ this.connect = this.connect.bind(this);
+ }
+
+ /**
+ * This method attaches Puppeteer to an existing browser instance.
+ *
+ * @remarks
+ *
+ * @param options - Set of configurable options to set on the browser.
+ * @returns Promise which resolves to browser instance.
+ */
+ connect(options: ConnectOptions): Promise<Browser> {
+ return _connectToBrowser(options);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts
new file mode 100644
index 0000000000..1655c7dba2
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts
@@ -0,0 +1,205 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import {_isElementHandle} from '../api/ElementHandleSymbol.js';
+import type {Frame} from '../api/Frame.js';
+import type {WaitForSelectorOptions} from '../api/Page.js';
+import type PuppeteerUtil from '../injected/injected.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+import {interpolateFunction, stringifyFunction} from '../util/Function.js';
+
+import {transposeIterableHandle} from './HandleIterator.js';
+import {LazyArg} from './LazyArg.js';
+import type {Awaitable, AwaitableIterable} from './types.js';
+
+/**
+ * @internal
+ */
+export type QuerySelectorAll = (
+ node: Node,
+ selector: string,
+ PuppeteerUtil: PuppeteerUtil
+) => AwaitableIterable<Node>;
+
+/**
+ * @internal
+ */
+export type QuerySelector = (
+ node: Node,
+ selector: string,
+ PuppeteerUtil: PuppeteerUtil
+) => Awaitable<Node | null>;
+
+/**
+ * @internal
+ */
+export class QueryHandler {
+ // Either one of these may be implemented, but at least one must be.
+ static querySelectorAll?: QuerySelectorAll;
+ static querySelector?: QuerySelector;
+
+ static get _querySelector(): QuerySelector {
+ if (this.querySelector) {
+ return this.querySelector;
+ }
+ if (!this.querySelectorAll) {
+ throw new Error('Cannot create default `querySelector`.');
+ }
+
+ return (this.querySelector = interpolateFunction(
+ async (node, selector, PuppeteerUtil) => {
+ const querySelectorAll: QuerySelectorAll =
+ PLACEHOLDER('querySelectorAll');
+ const results = querySelectorAll(node, selector, PuppeteerUtil);
+ for await (const result of results) {
+ return result;
+ }
+ return null;
+ },
+ {
+ querySelectorAll: stringifyFunction(this.querySelectorAll),
+ }
+ ));
+ }
+
+ static get _querySelectorAll(): QuerySelectorAll {
+ if (this.querySelectorAll) {
+ return this.querySelectorAll;
+ }
+ if (!this.querySelector) {
+ throw new Error('Cannot create default `querySelectorAll`.');
+ }
+
+ return (this.querySelectorAll = interpolateFunction(
+ async function* (node, selector, PuppeteerUtil) {
+ const querySelector: QuerySelector = PLACEHOLDER('querySelector');
+ const result = await querySelector(node, selector, PuppeteerUtil);
+ if (result) {
+ yield result;
+ }
+ },
+ {
+ querySelector: stringifyFunction(this.querySelector),
+ }
+ ));
+ }
+
+ /**
+ * Queries for multiple nodes given a selector and {@link ElementHandle}.
+ *
+ * Akin to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll | Document.querySelectorAll()}.
+ */
+ static async *queryAll(
+ element: ElementHandle<Node>,
+ selector: string
+ ): AwaitableIterable<ElementHandle<Node>> {
+ using handle = await element.evaluateHandle(
+ this._querySelectorAll,
+ selector,
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ })
+ );
+ yield* transposeIterableHandle(handle);
+ }
+
+ /**
+ * Queries for a single node given a selector and {@link ElementHandle}.
+ *
+ * Akin to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector}.
+ */
+ static async queryOne(
+ element: ElementHandle<Node>,
+ selector: string
+ ): Promise<ElementHandle<Node> | null> {
+ using result = await element.evaluateHandle(
+ this._querySelector,
+ selector,
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ })
+ );
+ if (!(_isElementHandle in result)) {
+ return null;
+ }
+ return result.move();
+ }
+
+ /**
+ * Waits until a single node appears for a given selector and
+ * {@link ElementHandle}.
+ *
+ * This will always query the handle in the Puppeteer world and migrate the
+ * result to the main world.
+ */
+ static async waitFor(
+ elementOrFrame: ElementHandle<Node> | Frame,
+ selector: string,
+ options: WaitForSelectorOptions
+ ): Promise<ElementHandle<Node> | null> {
+ let frame!: Frame;
+ using element = await (async () => {
+ if (!(_isElementHandle in elementOrFrame)) {
+ frame = elementOrFrame;
+ return;
+ }
+ frame = elementOrFrame.frame;
+ return await frame.isolatedRealm().adoptHandle(elementOrFrame);
+ })();
+
+ const {visible = false, hidden = false, timeout, signal} = options;
+
+ try {
+ signal?.throwIfAborted();
+
+ using handle = await frame.isolatedRealm().waitForFunction(
+ async (PuppeteerUtil, query, selector, root, visible) => {
+ const querySelector = PuppeteerUtil.createFunction(
+ query
+ ) as QuerySelector;
+ const node = await querySelector(
+ root ?? document,
+ selector,
+ PuppeteerUtil
+ );
+ return PuppeteerUtil.checkVisibility(node, visible);
+ },
+ {
+ polling: visible || hidden ? 'raf' : 'mutation',
+ root: element,
+ timeout,
+ signal,
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ stringifyFunction(this._querySelector),
+ selector,
+ element,
+ visible ? true : hidden ? false : undefined
+ );
+
+ if (signal?.aborted) {
+ throw signal.reason;
+ }
+
+ if (!(_isElementHandle in handle)) {
+ return null;
+ }
+ return await frame.mainRealm().transferHandle(handle);
+ } catch (error) {
+ if (!isErrorLike(error)) {
+ throw error;
+ }
+ if (error.name === 'AbortError') {
+ throw error;
+ }
+ error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`;
+ throw error;
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts
new file mode 100644
index 0000000000..0264c9175f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts
@@ -0,0 +1,52 @@
+import {source as injectedSource} from '../generated/injected.js';
+
+/**
+ * @internal
+ */
+export class ScriptInjector {
+ #updated = false;
+ #amendments = new Set<string>();
+
+ // Appends a statement of the form `(PuppeteerUtil) => {...}`.
+ append(statement: string): void {
+ this.#update(() => {
+ this.#amendments.add(statement);
+ });
+ }
+
+ pop(statement: string): void {
+ this.#update(() => {
+ this.#amendments.delete(statement);
+ });
+ }
+
+ inject(inject: (script: string) => void, force = false): void {
+ if (this.#updated || force) {
+ inject(this.#get());
+ }
+ this.#updated = false;
+ }
+
+ #update(callback: () => void): void {
+ callback();
+ this.#updated = true;
+ }
+
+ #get(): string {
+ return `(() => {
+ const module = {};
+ ${injectedSource}
+ ${[...this.#amendments]
+ .map(statement => {
+ return `(${statement})(module.exports.default);`;
+ })
+ .join('')}
+ return module.exports.default;
+ })()`;
+ }
+}
+
+/**
+ * @internal
+ */
+export const scriptInjector = new ScriptInjector();
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts
new file mode 100644
index 0000000000..188eeea9ad
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts
@@ -0,0 +1,78 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+/**
+ * The SecurityDetails class represents the security details of a
+ * response that was received over a secure connection.
+ *
+ * @public
+ */
+export class SecurityDetails {
+ #subjectName: string;
+ #issuer: string;
+ #validFrom: number;
+ #validTo: number;
+ #protocol: string;
+ #sanList: string[];
+
+ /**
+ * @internal
+ */
+ constructor(securityPayload: Protocol.Network.SecurityDetails) {
+ this.#subjectName = securityPayload.subjectName;
+ this.#issuer = securityPayload.issuer;
+ this.#validFrom = securityPayload.validFrom;
+ this.#validTo = securityPayload.validTo;
+ this.#protocol = securityPayload.protocol;
+ this.#sanList = securityPayload.sanList;
+ }
+
+ /**
+ * The name of the issuer of the certificate.
+ */
+ issuer(): string {
+ return this.#issuer;
+ }
+
+ /**
+ * {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp}
+ * marking the start of the certificate's validity.
+ */
+ validFrom(): number {
+ return this.#validFrom;
+ }
+
+ /**
+ * {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp}
+ * marking the end of the certificate's validity.
+ */
+ validTo(): number {
+ return this.#validTo;
+ }
+
+ /**
+ * The security protocol being used, e.g. "TLS 1.2".
+ */
+ protocol(): string {
+ return this.#protocol;
+ }
+
+ /**
+ * The name of the subject to which the certificate was issued.
+ */
+ subjectName(): string {
+ return this.#subjectName;
+ }
+
+ /**
+ * The list of {@link https://en.wikipedia.org/wiki/Subject_Alternative_Name | subject alternative names (SANs)} of the certificate.
+ */
+ subjectAlternativeNames(): string[] {
+ return this.#sanList;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts
new file mode 100644
index 0000000000..3ad1409c1b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export class TaskQueue {
+ #chain: Promise<void>;
+
+ constructor() {
+ this.#chain = Promise.resolve();
+ }
+
+ postTask<T>(task: () => Promise<T>): Promise<T> {
+ const result = this.#chain.then(task);
+ this.#chain = result.then(
+ () => {
+ return undefined;
+ },
+ () => {
+ return undefined;
+ }
+ );
+ return result;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts
new file mode 100644
index 0000000000..450ed06957
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {QueryHandler, type QuerySelectorAll} from './QueryHandler.js';
+
+/**
+ * @internal
+ */
+export class TextQueryHandler extends QueryHandler {
+ static override querySelectorAll: QuerySelectorAll = (
+ element,
+ selector,
+ {textQuerySelectorAll}
+ ) => {
+ return textQuerySelectorAll(element, selector);
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts
new file mode 100644
index 0000000000..7789d89b75
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const DEFAULT_TIMEOUT = 30000;
+
+/**
+ * @internal
+ */
+export class TimeoutSettings {
+ #defaultTimeout: number | null;
+ #defaultNavigationTimeout: number | null;
+
+ constructor() {
+ this.#defaultTimeout = null;
+ this.#defaultNavigationTimeout = null;
+ }
+
+ setDefaultTimeout(timeout: number): void {
+ this.#defaultTimeout = timeout;
+ }
+
+ setDefaultNavigationTimeout(timeout: number): void {
+ this.#defaultNavigationTimeout = timeout;
+ }
+
+ navigationTimeout(): number {
+ if (this.#defaultNavigationTimeout !== null) {
+ return this.#defaultNavigationTimeout;
+ }
+ if (this.#defaultTimeout !== null) {
+ return this.#defaultTimeout;
+ }
+ return DEFAULT_TIMEOUT;
+ }
+
+ timeout(): number {
+ if (this.#defaultTimeout !== null) {
+ return this.#defaultTimeout;
+ }
+ return DEFAULT_TIMEOUT;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts
new file mode 100644
index 0000000000..0a6d2f2e18
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts
@@ -0,0 +1,671 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export interface KeyDefinition {
+ keyCode?: number;
+ shiftKeyCode?: number;
+ key?: string;
+ shiftKey?: string;
+ code?: string;
+ text?: string;
+ shiftText?: string;
+ location?: number;
+}
+
+/**
+ * All the valid keys that can be passed to functions that take user input, such
+ * as {@link Keyboard.press | keyboard.press }
+ *
+ * @public
+ */
+export type KeyInput =
+ | '0'
+ | '1'
+ | '2'
+ | '3'
+ | '4'
+ | '5'
+ | '6'
+ | '7'
+ | '8'
+ | '9'
+ | 'Power'
+ | 'Eject'
+ | 'Abort'
+ | 'Help'
+ | 'Backspace'
+ | 'Tab'
+ | 'Numpad5'
+ | 'NumpadEnter'
+ | 'Enter'
+ | '\r'
+ | '\n'
+ | 'ShiftLeft'
+ | 'ShiftRight'
+ | 'ControlLeft'
+ | 'ControlRight'
+ | 'AltLeft'
+ | 'AltRight'
+ | 'Pause'
+ | 'CapsLock'
+ | 'Escape'
+ | 'Convert'
+ | 'NonConvert'
+ | 'Space'
+ | 'Numpad9'
+ | 'PageUp'
+ | 'Numpad3'
+ | 'PageDown'
+ | 'End'
+ | 'Numpad1'
+ | 'Home'
+ | 'Numpad7'
+ | 'ArrowLeft'
+ | 'Numpad4'
+ | 'Numpad8'
+ | 'ArrowUp'
+ | 'ArrowRight'
+ | 'Numpad6'
+ | 'Numpad2'
+ | 'ArrowDown'
+ | 'Select'
+ | 'Open'
+ | 'PrintScreen'
+ | 'Insert'
+ | 'Numpad0'
+ | 'Delete'
+ | 'NumpadDecimal'
+ | 'Digit0'
+ | 'Digit1'
+ | 'Digit2'
+ | 'Digit3'
+ | 'Digit4'
+ | 'Digit5'
+ | 'Digit6'
+ | 'Digit7'
+ | 'Digit8'
+ | 'Digit9'
+ | 'KeyA'
+ | 'KeyB'
+ | 'KeyC'
+ | 'KeyD'
+ | 'KeyE'
+ | 'KeyF'
+ | 'KeyG'
+ | 'KeyH'
+ | 'KeyI'
+ | 'KeyJ'
+ | 'KeyK'
+ | 'KeyL'
+ | 'KeyM'
+ | 'KeyN'
+ | 'KeyO'
+ | 'KeyP'
+ | 'KeyQ'
+ | 'KeyR'
+ | 'KeyS'
+ | 'KeyT'
+ | 'KeyU'
+ | 'KeyV'
+ | 'KeyW'
+ | 'KeyX'
+ | 'KeyY'
+ | 'KeyZ'
+ | 'MetaLeft'
+ | 'MetaRight'
+ | 'ContextMenu'
+ | 'NumpadMultiply'
+ | 'NumpadAdd'
+ | 'NumpadSubtract'
+ | 'NumpadDivide'
+ | 'F1'
+ | 'F2'
+ | 'F3'
+ | 'F4'
+ | 'F5'
+ | 'F6'
+ | 'F7'
+ | 'F8'
+ | 'F9'
+ | 'F10'
+ | 'F11'
+ | 'F12'
+ | 'F13'
+ | 'F14'
+ | 'F15'
+ | 'F16'
+ | 'F17'
+ | 'F18'
+ | 'F19'
+ | 'F20'
+ | 'F21'
+ | 'F22'
+ | 'F23'
+ | 'F24'
+ | 'NumLock'
+ | 'ScrollLock'
+ | 'AudioVolumeMute'
+ | 'AudioVolumeDown'
+ | 'AudioVolumeUp'
+ | 'MediaTrackNext'
+ | 'MediaTrackPrevious'
+ | 'MediaStop'
+ | 'MediaPlayPause'
+ | 'Semicolon'
+ | 'Equal'
+ | 'NumpadEqual'
+ | 'Comma'
+ | 'Minus'
+ | 'Period'
+ | 'Slash'
+ | 'Backquote'
+ | 'BracketLeft'
+ | 'Backslash'
+ | 'BracketRight'
+ | 'Quote'
+ | 'AltGraph'
+ | 'Props'
+ | 'Cancel'
+ | 'Clear'
+ | 'Shift'
+ | 'Control'
+ | 'Alt'
+ | 'Accept'
+ | 'ModeChange'
+ | ' '
+ | 'Print'
+ | 'Execute'
+ | '\u0000'
+ | 'a'
+ | 'b'
+ | 'c'
+ | 'd'
+ | 'e'
+ | 'f'
+ | 'g'
+ | 'h'
+ | 'i'
+ | 'j'
+ | 'k'
+ | 'l'
+ | 'm'
+ | 'n'
+ | 'o'
+ | 'p'
+ | 'q'
+ | 'r'
+ | 's'
+ | 't'
+ | 'u'
+ | 'v'
+ | 'w'
+ | 'x'
+ | 'y'
+ | 'z'
+ | 'Meta'
+ | '*'
+ | '+'
+ | '-'
+ | '/'
+ | ';'
+ | '='
+ | ','
+ | '.'
+ | '`'
+ | '['
+ | '\\'
+ | ']'
+ | "'"
+ | 'Attn'
+ | 'CrSel'
+ | 'ExSel'
+ | 'EraseEof'
+ | 'Play'
+ | 'ZoomOut'
+ | ')'
+ | '!'
+ | '@'
+ | '#'
+ | '$'
+ | '%'
+ | '^'
+ | '&'
+ | '('
+ | 'A'
+ | 'B'
+ | 'C'
+ | 'D'
+ | 'E'
+ | 'F'
+ | 'G'
+ | 'H'
+ | 'I'
+ | 'J'
+ | 'K'
+ | 'L'
+ | 'M'
+ | 'N'
+ | 'O'
+ | 'P'
+ | 'Q'
+ | 'R'
+ | 'S'
+ | 'T'
+ | 'U'
+ | 'V'
+ | 'W'
+ | 'X'
+ | 'Y'
+ | 'Z'
+ | ':'
+ | '<'
+ | '_'
+ | '>'
+ | '?'
+ | '~'
+ | '{'
+ | '|'
+ | '}'
+ | '"'
+ | 'SoftLeft'
+ | 'SoftRight'
+ | 'Camera'
+ | 'Call'
+ | 'EndCall'
+ | 'VolumeDown'
+ | 'VolumeUp';
+
+/**
+ * @internal
+ */
+export const _keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
+ '0': {keyCode: 48, key: '0', code: 'Digit0'},
+ '1': {keyCode: 49, key: '1', code: 'Digit1'},
+ '2': {keyCode: 50, key: '2', code: 'Digit2'},
+ '3': {keyCode: 51, key: '3', code: 'Digit3'},
+ '4': {keyCode: 52, key: '4', code: 'Digit4'},
+ '5': {keyCode: 53, key: '5', code: 'Digit5'},
+ '6': {keyCode: 54, key: '6', code: 'Digit6'},
+ '7': {keyCode: 55, key: '7', code: 'Digit7'},
+ '8': {keyCode: 56, key: '8', code: 'Digit8'},
+ '9': {keyCode: 57, key: '9', code: 'Digit9'},
+ Power: {key: 'Power', code: 'Power'},
+ Eject: {key: 'Eject', code: 'Eject'},
+ Abort: {keyCode: 3, code: 'Abort', key: 'Cancel'},
+ Help: {keyCode: 6, code: 'Help', key: 'Help'},
+ Backspace: {keyCode: 8, code: 'Backspace', key: 'Backspace'},
+ Tab: {keyCode: 9, code: 'Tab', key: 'Tab'},
+ Numpad5: {
+ keyCode: 12,
+ shiftKeyCode: 101,
+ key: 'Clear',
+ code: 'Numpad5',
+ shiftKey: '5',
+ location: 3,
+ },
+ NumpadEnter: {
+ keyCode: 13,
+ code: 'NumpadEnter',
+ key: 'Enter',
+ text: '\r',
+ location: 3,
+ },
+ Enter: {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'},
+ '\r': {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'},
+ '\n': {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'},
+ ShiftLeft: {keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1},
+ ShiftRight: {keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2},
+ ControlLeft: {
+ keyCode: 17,
+ code: 'ControlLeft',
+ key: 'Control',
+ location: 1,
+ },
+ ControlRight: {
+ keyCode: 17,
+ code: 'ControlRight',
+ key: 'Control',
+ location: 2,
+ },
+ AltLeft: {keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1},
+ AltRight: {keyCode: 18, code: 'AltRight', key: 'Alt', location: 2},
+ Pause: {keyCode: 19, code: 'Pause', key: 'Pause'},
+ CapsLock: {keyCode: 20, code: 'CapsLock', key: 'CapsLock'},
+ Escape: {keyCode: 27, code: 'Escape', key: 'Escape'},
+ Convert: {keyCode: 28, code: 'Convert', key: 'Convert'},
+ NonConvert: {keyCode: 29, code: 'NonConvert', key: 'NonConvert'},
+ Space: {keyCode: 32, code: 'Space', key: ' '},
+ Numpad9: {
+ keyCode: 33,
+ shiftKeyCode: 105,
+ key: 'PageUp',
+ code: 'Numpad9',
+ shiftKey: '9',
+ location: 3,
+ },
+ PageUp: {keyCode: 33, code: 'PageUp', key: 'PageUp'},
+ Numpad3: {
+ keyCode: 34,
+ shiftKeyCode: 99,
+ key: 'PageDown',
+ code: 'Numpad3',
+ shiftKey: '3',
+ location: 3,
+ },
+ PageDown: {keyCode: 34, code: 'PageDown', key: 'PageDown'},
+ End: {keyCode: 35, code: 'End', key: 'End'},
+ Numpad1: {
+ keyCode: 35,
+ shiftKeyCode: 97,
+ key: 'End',
+ code: 'Numpad1',
+ shiftKey: '1',
+ location: 3,
+ },
+ Home: {keyCode: 36, code: 'Home', key: 'Home'},
+ Numpad7: {
+ keyCode: 36,
+ shiftKeyCode: 103,
+ key: 'Home',
+ code: 'Numpad7',
+ shiftKey: '7',
+ location: 3,
+ },
+ ArrowLeft: {keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft'},
+ Numpad4: {
+ keyCode: 37,
+ shiftKeyCode: 100,
+ key: 'ArrowLeft',
+ code: 'Numpad4',
+ shiftKey: '4',
+ location: 3,
+ },
+ Numpad8: {
+ keyCode: 38,
+ shiftKeyCode: 104,
+ key: 'ArrowUp',
+ code: 'Numpad8',
+ shiftKey: '8',
+ location: 3,
+ },
+ ArrowUp: {keyCode: 38, code: 'ArrowUp', key: 'ArrowUp'},
+ ArrowRight: {keyCode: 39, code: 'ArrowRight', key: 'ArrowRight'},
+ Numpad6: {
+ keyCode: 39,
+ shiftKeyCode: 102,
+ key: 'ArrowRight',
+ code: 'Numpad6',
+ shiftKey: '6',
+ location: 3,
+ },
+ Numpad2: {
+ keyCode: 40,
+ shiftKeyCode: 98,
+ key: 'ArrowDown',
+ code: 'Numpad2',
+ shiftKey: '2',
+ location: 3,
+ },
+ ArrowDown: {keyCode: 40, code: 'ArrowDown', key: 'ArrowDown'},
+ Select: {keyCode: 41, code: 'Select', key: 'Select'},
+ Open: {keyCode: 43, code: 'Open', key: 'Execute'},
+ PrintScreen: {keyCode: 44, code: 'PrintScreen', key: 'PrintScreen'},
+ Insert: {keyCode: 45, code: 'Insert', key: 'Insert'},
+ Numpad0: {
+ keyCode: 45,
+ shiftKeyCode: 96,
+ key: 'Insert',
+ code: 'Numpad0',
+ shiftKey: '0',
+ location: 3,
+ },
+ Delete: {keyCode: 46, code: 'Delete', key: 'Delete'},
+ NumpadDecimal: {
+ keyCode: 46,
+ shiftKeyCode: 110,
+ code: 'NumpadDecimal',
+ key: '\u0000',
+ shiftKey: '.',
+ location: 3,
+ },
+ Digit0: {keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0'},
+ Digit1: {keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1'},
+ Digit2: {keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2'},
+ Digit3: {keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3'},
+ Digit4: {keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4'},
+ Digit5: {keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5'},
+ Digit6: {keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6'},
+ Digit7: {keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7'},
+ Digit8: {keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8'},
+ Digit9: {keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9'},
+ KeyA: {keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a'},
+ KeyB: {keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b'},
+ KeyC: {keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c'},
+ KeyD: {keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd'},
+ KeyE: {keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e'},
+ KeyF: {keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f'},
+ KeyG: {keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g'},
+ KeyH: {keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h'},
+ KeyI: {keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i'},
+ KeyJ: {keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j'},
+ KeyK: {keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k'},
+ KeyL: {keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l'},
+ KeyM: {keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm'},
+ KeyN: {keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n'},
+ KeyO: {keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o'},
+ KeyP: {keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p'},
+ KeyQ: {keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q'},
+ KeyR: {keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r'},
+ KeyS: {keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's'},
+ KeyT: {keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't'},
+ KeyU: {keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u'},
+ KeyV: {keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v'},
+ KeyW: {keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w'},
+ KeyX: {keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x'},
+ KeyY: {keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y'},
+ KeyZ: {keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z'},
+ MetaLeft: {keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1},
+ MetaRight: {keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2},
+ ContextMenu: {keyCode: 93, code: 'ContextMenu', key: 'ContextMenu'},
+ NumpadMultiply: {
+ keyCode: 106,
+ code: 'NumpadMultiply',
+ key: '*',
+ location: 3,
+ },
+ NumpadAdd: {keyCode: 107, code: 'NumpadAdd', key: '+', location: 3},
+ NumpadSubtract: {
+ keyCode: 109,
+ code: 'NumpadSubtract',
+ key: '-',
+ location: 3,
+ },
+ NumpadDivide: {keyCode: 111, code: 'NumpadDivide', key: '/', location: 3},
+ F1: {keyCode: 112, code: 'F1', key: 'F1'},
+ F2: {keyCode: 113, code: 'F2', key: 'F2'},
+ F3: {keyCode: 114, code: 'F3', key: 'F3'},
+ F4: {keyCode: 115, code: 'F4', key: 'F4'},
+ F5: {keyCode: 116, code: 'F5', key: 'F5'},
+ F6: {keyCode: 117, code: 'F6', key: 'F6'},
+ F7: {keyCode: 118, code: 'F7', key: 'F7'},
+ F8: {keyCode: 119, code: 'F8', key: 'F8'},
+ F9: {keyCode: 120, code: 'F9', key: 'F9'},
+ F10: {keyCode: 121, code: 'F10', key: 'F10'},
+ F11: {keyCode: 122, code: 'F11', key: 'F11'},
+ F12: {keyCode: 123, code: 'F12', key: 'F12'},
+ F13: {keyCode: 124, code: 'F13', key: 'F13'},
+ F14: {keyCode: 125, code: 'F14', key: 'F14'},
+ F15: {keyCode: 126, code: 'F15', key: 'F15'},
+ F16: {keyCode: 127, code: 'F16', key: 'F16'},
+ F17: {keyCode: 128, code: 'F17', key: 'F17'},
+ F18: {keyCode: 129, code: 'F18', key: 'F18'},
+ F19: {keyCode: 130, code: 'F19', key: 'F19'},
+ F20: {keyCode: 131, code: 'F20', key: 'F20'},
+ F21: {keyCode: 132, code: 'F21', key: 'F21'},
+ F22: {keyCode: 133, code: 'F22', key: 'F22'},
+ F23: {keyCode: 134, code: 'F23', key: 'F23'},
+ F24: {keyCode: 135, code: 'F24', key: 'F24'},
+ NumLock: {keyCode: 144, code: 'NumLock', key: 'NumLock'},
+ ScrollLock: {keyCode: 145, code: 'ScrollLock', key: 'ScrollLock'},
+ AudioVolumeMute: {
+ keyCode: 173,
+ code: 'AudioVolumeMute',
+ key: 'AudioVolumeMute',
+ },
+ AudioVolumeDown: {
+ keyCode: 174,
+ code: 'AudioVolumeDown',
+ key: 'AudioVolumeDown',
+ },
+ AudioVolumeUp: {keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp'},
+ MediaTrackNext: {
+ keyCode: 176,
+ code: 'MediaTrackNext',
+ key: 'MediaTrackNext',
+ },
+ MediaTrackPrevious: {
+ keyCode: 177,
+ code: 'MediaTrackPrevious',
+ key: 'MediaTrackPrevious',
+ },
+ MediaStop: {keyCode: 178, code: 'MediaStop', key: 'MediaStop'},
+ MediaPlayPause: {
+ keyCode: 179,
+ code: 'MediaPlayPause',
+ key: 'MediaPlayPause',
+ },
+ Semicolon: {keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';'},
+ Equal: {keyCode: 187, code: 'Equal', shiftKey: '+', key: '='},
+ NumpadEqual: {keyCode: 187, code: 'NumpadEqual', key: '=', location: 3},
+ Comma: {keyCode: 188, code: 'Comma', shiftKey: '<', key: ','},
+ Minus: {keyCode: 189, code: 'Minus', shiftKey: '_', key: '-'},
+ Period: {keyCode: 190, code: 'Period', shiftKey: '>', key: '.'},
+ Slash: {keyCode: 191, code: 'Slash', shiftKey: '?', key: '/'},
+ Backquote: {keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`'},
+ BracketLeft: {keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '['},
+ Backslash: {keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\'},
+ BracketRight: {keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']'},
+ Quote: {keyCode: 222, code: 'Quote', shiftKey: '"', key: "'"},
+ AltGraph: {keyCode: 225, code: 'AltGraph', key: 'AltGraph'},
+ Props: {keyCode: 247, code: 'Props', key: 'CrSel'},
+ Cancel: {keyCode: 3, key: 'Cancel', code: 'Abort'},
+ Clear: {keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3},
+ Shift: {keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1},
+ Control: {keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1},
+ Alt: {keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1},
+ Accept: {keyCode: 30, key: 'Accept'},
+ ModeChange: {keyCode: 31, key: 'ModeChange'},
+ ' ': {keyCode: 32, key: ' ', code: 'Space'},
+ Print: {keyCode: 42, key: 'Print'},
+ Execute: {keyCode: 43, key: 'Execute', code: 'Open'},
+ '\u0000': {keyCode: 46, key: '\u0000', code: 'NumpadDecimal', location: 3},
+ a: {keyCode: 65, key: 'a', code: 'KeyA'},
+ b: {keyCode: 66, key: 'b', code: 'KeyB'},
+ c: {keyCode: 67, key: 'c', code: 'KeyC'},
+ d: {keyCode: 68, key: 'd', code: 'KeyD'},
+ e: {keyCode: 69, key: 'e', code: 'KeyE'},
+ f: {keyCode: 70, key: 'f', code: 'KeyF'},
+ g: {keyCode: 71, key: 'g', code: 'KeyG'},
+ h: {keyCode: 72, key: 'h', code: 'KeyH'},
+ i: {keyCode: 73, key: 'i', code: 'KeyI'},
+ j: {keyCode: 74, key: 'j', code: 'KeyJ'},
+ k: {keyCode: 75, key: 'k', code: 'KeyK'},
+ l: {keyCode: 76, key: 'l', code: 'KeyL'},
+ m: {keyCode: 77, key: 'm', code: 'KeyM'},
+ n: {keyCode: 78, key: 'n', code: 'KeyN'},
+ o: {keyCode: 79, key: 'o', code: 'KeyO'},
+ p: {keyCode: 80, key: 'p', code: 'KeyP'},
+ q: {keyCode: 81, key: 'q', code: 'KeyQ'},
+ r: {keyCode: 82, key: 'r', code: 'KeyR'},
+ s: {keyCode: 83, key: 's', code: 'KeyS'},
+ t: {keyCode: 84, key: 't', code: 'KeyT'},
+ u: {keyCode: 85, key: 'u', code: 'KeyU'},
+ v: {keyCode: 86, key: 'v', code: 'KeyV'},
+ w: {keyCode: 87, key: 'w', code: 'KeyW'},
+ x: {keyCode: 88, key: 'x', code: 'KeyX'},
+ y: {keyCode: 89, key: 'y', code: 'KeyY'},
+ z: {keyCode: 90, key: 'z', code: 'KeyZ'},
+ Meta: {keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1},
+ '*': {keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3},
+ '+': {keyCode: 107, key: '+', code: 'NumpadAdd', location: 3},
+ '-': {keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3},
+ '/': {keyCode: 111, key: '/', code: 'NumpadDivide', location: 3},
+ ';': {keyCode: 186, key: ';', code: 'Semicolon'},
+ '=': {keyCode: 187, key: '=', code: 'Equal'},
+ ',': {keyCode: 188, key: ',', code: 'Comma'},
+ '.': {keyCode: 190, key: '.', code: 'Period'},
+ '`': {keyCode: 192, key: '`', code: 'Backquote'},
+ '[': {keyCode: 219, key: '[', code: 'BracketLeft'},
+ '\\': {keyCode: 220, key: '\\', code: 'Backslash'},
+ ']': {keyCode: 221, key: ']', code: 'BracketRight'},
+ "'": {keyCode: 222, key: "'", code: 'Quote'},
+ Attn: {keyCode: 246, key: 'Attn'},
+ CrSel: {keyCode: 247, key: 'CrSel', code: 'Props'},
+ ExSel: {keyCode: 248, key: 'ExSel'},
+ EraseEof: {keyCode: 249, key: 'EraseEof'},
+ Play: {keyCode: 250, key: 'Play'},
+ ZoomOut: {keyCode: 251, key: 'ZoomOut'},
+ ')': {keyCode: 48, key: ')', code: 'Digit0'},
+ '!': {keyCode: 49, key: '!', code: 'Digit1'},
+ '@': {keyCode: 50, key: '@', code: 'Digit2'},
+ '#': {keyCode: 51, key: '#', code: 'Digit3'},
+ $: {keyCode: 52, key: '$', code: 'Digit4'},
+ '%': {keyCode: 53, key: '%', code: 'Digit5'},
+ '^': {keyCode: 54, key: '^', code: 'Digit6'},
+ '&': {keyCode: 55, key: '&', code: 'Digit7'},
+ '(': {keyCode: 57, key: '(', code: 'Digit9'},
+ A: {keyCode: 65, key: 'A', code: 'KeyA'},
+ B: {keyCode: 66, key: 'B', code: 'KeyB'},
+ C: {keyCode: 67, key: 'C', code: 'KeyC'},
+ D: {keyCode: 68, key: 'D', code: 'KeyD'},
+ E: {keyCode: 69, key: 'E', code: 'KeyE'},
+ F: {keyCode: 70, key: 'F', code: 'KeyF'},
+ G: {keyCode: 71, key: 'G', code: 'KeyG'},
+ H: {keyCode: 72, key: 'H', code: 'KeyH'},
+ I: {keyCode: 73, key: 'I', code: 'KeyI'},
+ J: {keyCode: 74, key: 'J', code: 'KeyJ'},
+ K: {keyCode: 75, key: 'K', code: 'KeyK'},
+ L: {keyCode: 76, key: 'L', code: 'KeyL'},
+ M: {keyCode: 77, key: 'M', code: 'KeyM'},
+ N: {keyCode: 78, key: 'N', code: 'KeyN'},
+ O: {keyCode: 79, key: 'O', code: 'KeyO'},
+ P: {keyCode: 80, key: 'P', code: 'KeyP'},
+ Q: {keyCode: 81, key: 'Q', code: 'KeyQ'},
+ R: {keyCode: 82, key: 'R', code: 'KeyR'},
+ S: {keyCode: 83, key: 'S', code: 'KeyS'},
+ T: {keyCode: 84, key: 'T', code: 'KeyT'},
+ U: {keyCode: 85, key: 'U', code: 'KeyU'},
+ V: {keyCode: 86, key: 'V', code: 'KeyV'},
+ W: {keyCode: 87, key: 'W', code: 'KeyW'},
+ X: {keyCode: 88, key: 'X', code: 'KeyX'},
+ Y: {keyCode: 89, key: 'Y', code: 'KeyY'},
+ Z: {keyCode: 90, key: 'Z', code: 'KeyZ'},
+ ':': {keyCode: 186, key: ':', code: 'Semicolon'},
+ '<': {keyCode: 188, key: '<', code: 'Comma'},
+ _: {keyCode: 189, key: '_', code: 'Minus'},
+ '>': {keyCode: 190, key: '>', code: 'Period'},
+ '?': {keyCode: 191, key: '?', code: 'Slash'},
+ '~': {keyCode: 192, key: '~', code: 'Backquote'},
+ '{': {keyCode: 219, key: '{', code: 'BracketLeft'},
+ '|': {keyCode: 220, key: '|', code: 'Backslash'},
+ '}': {keyCode: 221, key: '}', code: 'BracketRight'},
+ '"': {keyCode: 222, key: '"', code: 'Quote'},
+ SoftLeft: {key: 'SoftLeft', code: 'SoftLeft', location: 4},
+ SoftRight: {key: 'SoftRight', code: 'SoftRight', location: 4},
+ Camera: {keyCode: 44, key: 'Camera', code: 'Camera', location: 4},
+ Call: {key: 'Call', code: 'Call', location: 4},
+ EndCall: {keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4},
+ VolumeDown: {
+ keyCode: 182,
+ key: 'VolumeDown',
+ code: 'VolumeDown',
+ location: 4,
+ },
+ VolumeUp: {keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4},
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Viewport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Viewport.ts
new file mode 100644
index 0000000000..46a937a88f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Viewport.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @public
+ */
+export interface Viewport {
+ /**
+ * The page width in CSS pixels.
+ *
+ * @remarks
+ * Setting this value to `0` will reset this value to the system default.
+ */
+ width: number;
+ /**
+ * The page height in CSS pixels.
+ *
+ * @remarks
+ * Setting this value to `0` will reset this value to the system default.
+ */
+ height: number;
+ /**
+ * Specify device scale factor.
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio | devicePixelRatio} for more info.
+ *
+ * @remarks
+ * Setting this value to `0` will reset this value to the system default.
+ *
+ * @defaultValue `1`
+ */
+ deviceScaleFactor?: number;
+ /**
+ * Whether the `meta viewport` tag is taken into account.
+ * @defaultValue `false`
+ */
+ isMobile?: boolean;
+ /**
+ * Specifies if the viewport is in landscape mode.
+ * @defaultValue `false`
+ */
+ isLandscape?: boolean;
+ /**
+ * Specify if the viewport supports touch events.
+ * @defaultValue `false`
+ */
+ hasTouch?: boolean;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts
new file mode 100644
index 0000000000..d0c1e2a038
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts
@@ -0,0 +1,275 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import type {JSHandle} from '../api/JSHandle.js';
+import type {Realm} from '../api/Realm.js';
+import type {Poller} from '../injected/Poller.js';
+import {Deferred} from '../util/Deferred.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+import {stringifyFunction} from '../util/Function.js';
+
+import {TimeoutError} from './Errors.js';
+import {LazyArg} from './LazyArg.js';
+import type {HandleFor} from './types.js';
+
+/**
+ * @internal
+ */
+export interface WaitTaskOptions {
+ polling: 'raf' | 'mutation' | number;
+ root?: ElementHandle<Node>;
+ timeout: number;
+ signal?: AbortSignal;
+}
+
+/**
+ * @internal
+ */
+export class WaitTask<T = unknown> {
+ #world: Realm;
+ #polling: 'raf' | 'mutation' | number;
+ #root?: ElementHandle<Node>;
+
+ #fn: string;
+ #args: unknown[];
+
+ #timeout?: NodeJS.Timeout;
+ #timeoutError?: TimeoutError;
+
+ #result = Deferred.create<HandleFor<T>>();
+
+ #poller?: JSHandle<Poller<T>>;
+ #signal?: AbortSignal;
+ #reruns: AbortController[] = [];
+
+ constructor(
+ world: Realm,
+ options: WaitTaskOptions,
+ fn: ((...args: unknown[]) => Promise<T>) | string,
+ ...args: unknown[]
+ ) {
+ this.#world = world;
+ this.#polling = options.polling;
+ this.#root = options.root;
+ this.#signal = options.signal;
+ this.#signal?.addEventListener(
+ 'abort',
+ () => {
+ void this.terminate(this.#signal?.reason);
+ },
+ {
+ once: true,
+ }
+ );
+
+ switch (typeof fn) {
+ case 'string':
+ this.#fn = `() => {return (${fn});}`;
+ break;
+ default:
+ this.#fn = stringifyFunction(fn);
+ break;
+ }
+ this.#args = args;
+
+ this.#world.taskManager.add(this);
+
+ if (options.timeout) {
+ this.#timeoutError = new TimeoutError(
+ `Waiting failed: ${options.timeout}ms exceeded`
+ );
+ this.#timeout = setTimeout(() => {
+ void this.terminate(this.#timeoutError);
+ }, options.timeout);
+ }
+
+ void this.rerun();
+ }
+
+ get result(): Promise<HandleFor<T>> {
+ return this.#result.valueOrThrow();
+ }
+
+ async rerun(): Promise<void> {
+ for (const prev of this.#reruns) {
+ prev.abort();
+ }
+ this.#reruns.length = 0;
+ const controller = new AbortController();
+ this.#reruns.push(controller);
+ try {
+ switch (this.#polling) {
+ case 'raf':
+ this.#poller = await this.#world.evaluateHandle(
+ ({RAFPoller, createFunction}, fn, ...args) => {
+ const fun = createFunction(fn);
+ return new RAFPoller(() => {
+ return fun(...args) as Promise<T>;
+ });
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ this.#fn,
+ ...this.#args
+ );
+ break;
+ case 'mutation':
+ this.#poller = await this.#world.evaluateHandle(
+ ({MutationPoller, createFunction}, root, fn, ...args) => {
+ const fun = createFunction(fn);
+ return new MutationPoller(() => {
+ return fun(...args) as Promise<T>;
+ }, root || document);
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ this.#root,
+ this.#fn,
+ ...this.#args
+ );
+ break;
+ default:
+ this.#poller = await this.#world.evaluateHandle(
+ ({IntervalPoller, createFunction}, ms, fn, ...args) => {
+ const fun = createFunction(fn);
+ return new IntervalPoller(() => {
+ return fun(...args) as Promise<T>;
+ }, ms);
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ this.#polling,
+ this.#fn,
+ ...this.#args
+ );
+ break;
+ }
+
+ await this.#poller.evaluate(poller => {
+ void poller.start();
+ });
+
+ const result = await this.#poller.evaluateHandle(poller => {
+ return poller.result();
+ });
+ this.#result.resolve(result);
+
+ await this.terminate();
+ } catch (error) {
+ if (controller.signal.aborted) {
+ return;
+ }
+ const badError = this.getBadError(error);
+ if (badError) {
+ await this.terminate(badError);
+ }
+ }
+ }
+
+ async terminate(error?: Error): Promise<void> {
+ this.#world.taskManager.delete(this);
+
+ clearTimeout(this.#timeout);
+
+ if (error && !this.#result.finished()) {
+ this.#result.reject(error);
+ }
+
+ if (this.#poller) {
+ try {
+ await this.#poller.evaluateHandle(async poller => {
+ await poller.stop();
+ });
+ if (this.#poller) {
+ await this.#poller.dispose();
+ this.#poller = undefined;
+ }
+ } catch {
+ // Ignore errors since they most likely come from low-level cleanup.
+ }
+ }
+ }
+
+ /**
+ * Not all errors lead to termination. They usually imply we need to rerun the task.
+ */
+ getBadError(error: unknown): Error | undefined {
+ if (isErrorLike(error)) {
+ // When frame is detached the task should have been terminated by the IsolatedWorld.
+ // This can fail if we were adding this task while the frame was detached,
+ // so we terminate here instead.
+ if (
+ error.message.includes(
+ 'Execution context is not available in detached frame'
+ )
+ ) {
+ return new Error('Waiting failed: Frame detached');
+ }
+
+ // When the page is navigated, the promise is rejected.
+ // We will try again in the new execution context.
+ if (error.message.includes('Execution context was destroyed')) {
+ return;
+ }
+
+ // We could have tried to evaluate in a context which was already
+ // destroyed.
+ if (error.message.includes('Cannot find context with specified id')) {
+ return;
+ }
+
+ // Errors coming from WebDriver BiDi. TODO: Adjust messages after
+ // https://github.com/w3c/webdriver-bidi/issues/540 is resolved.
+ if (
+ error.message.includes(
+ "AbortError: Actor 'MessageHandlerFrame' destroyed"
+ )
+ ) {
+ return;
+ }
+
+ return error;
+ }
+
+ return new Error('WaitTask failed with an error', {
+ cause: error,
+ });
+ }
+}
+
+/**
+ * @internal
+ */
+export class TaskManager {
+ #tasks: Set<WaitTask> = new Set<WaitTask>();
+
+ add(task: WaitTask<any>): void {
+ this.#tasks.add(task);
+ }
+
+ delete(task: WaitTask<any>): void {
+ this.#tasks.delete(task);
+ }
+
+ terminateAll(error?: Error): void {
+ for (const task of this.#tasks) {
+ void task.terminate(error);
+ }
+ this.#tasks.clear();
+ }
+
+ async rerunAll(): Promise<void> {
+ await Promise.all(
+ [...this.#tasks].map(task => {
+ return task.rerun();
+ })
+ );
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts
new file mode 100644
index 0000000000..b6e3a67bad
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ QueryHandler,
+ type QuerySelectorAll,
+ type QuerySelector,
+} from './QueryHandler.js';
+
+/**
+ * @internal
+ */
+export class XPathQueryHandler extends QueryHandler {
+ static override querySelectorAll: QuerySelectorAll = (
+ element,
+ selector,
+ {xpathQuerySelectorAll}
+ ) => {
+ return xpathQuerySelectorAll(element, selector);
+ };
+
+ static override querySelector: QuerySelector = (
+ element: Node,
+ selector: string,
+ {xpathQuerySelectorAll}
+ ) => {
+ for (const result of xpathQuerySelectorAll(element, selector, 1)) {
+ return result;
+ }
+ return null;
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts
new file mode 100644
index 0000000000..6ef8925605
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './BrowserWebSocketTransport.js';
+export * from './CallbackRegistry.js';
+export * from './Configuration.js';
+export * from './ConnectionTransport.js';
+export * from './ConnectOptions.js';
+export * from './ConsoleMessage.js';
+export * from './CustomQueryHandler.js';
+export * from './Debug.js';
+export * from './Device.js';
+export * from './Errors.js';
+export * from './EventEmitter.js';
+export * from './fetch.js';
+export * from './FileChooser.js';
+export * from './GetQueryHandler.js';
+export * from './HandleIterator.js';
+export * from './LazyArg.js';
+export * from './NetworkManagerEvents.js';
+export * from './PDFOptions.js';
+export * from './PierceQueryHandler.js';
+export * from './PQueryHandler.js';
+export * from './Product.js';
+export * from './Puppeteer.js';
+export * from './QueryHandler.js';
+export * from './ScriptInjector.js';
+export * from './SecurityDetails.js';
+export * from './TaskQueue.js';
+export * from './TextQueryHandler.js';
+export * from './TimeoutSettings.js';
+export * from './types.js';
+export * from './USKeyboardLayout.js';
+export * from './util.js';
+export * from './Viewport.js';
+export * from './WaitTask.js';
+export * from './XPathQueryHandler.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts
new file mode 100644
index 0000000000..6c7a2b451c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts
@@ -0,0 +1,14 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Gets the global version if we're in the browser, else loads the node-fetch module.
+ *
+ * @internal
+ */
+export const getFetch = async (): Promise<typeof fetch> => {
+ return (globalThis as any).fetch || (await import('cross-fetch')).fetch;
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts
new file mode 100644
index 0000000000..3f2cf5d4f3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts
@@ -0,0 +1,225 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import type {JSHandle} from '../api/JSHandle.js';
+
+import type {LazyArg} from './LazyArg.js';
+
+/**
+ * @public
+ */
+export type AwaitablePredicate<T> = (value: T) => Awaitable<boolean>;
+
+/**
+ * @public
+ */
+export interface Moveable {
+ /**
+ * Moves the resource when 'using'.
+ */
+ move(): this;
+}
+
+/**
+ * @internal
+ */
+export interface Disposed {
+ get disposed(): boolean;
+}
+
+/**
+ * @internal
+ */
+export interface BindingPayload {
+ type: string;
+ name: string;
+ seq: number;
+ args: unknown[];
+ /**
+ * Determines whether the arguments of the payload are trivial.
+ */
+ isTrivial: boolean;
+}
+
+/**
+ * @internal
+ */
+export type AwaitableIterator<T> = Iterator<T> | AsyncIterator<T>;
+
+/**
+ * @public
+ */
+export type AwaitableIterable<T> = Iterable<T> | AsyncIterable<T>;
+
+/**
+ * @public
+ */
+export type Awaitable<T> = T | PromiseLike<T>;
+
+/**
+ * @public
+ */
+export type HandleFor<T> = T extends Node ? ElementHandle<T> : JSHandle<T>;
+
+/**
+ * @public
+ */
+export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;
+
+/**
+ * @public
+ */
+export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
+
+/**
+ * @internal
+ */
+export type FlattenLazyArg<T> = T extends LazyArg<infer U> ? U : T;
+
+/**
+ * @internal
+ */
+export type InnerLazyParams<T extends unknown[]> = {
+ [K in keyof T]: FlattenLazyArg<T[K]>;
+};
+
+/**
+ * @public
+ */
+export type InnerParams<T extends unknown[]> = {
+ [K in keyof T]: FlattenHandle<T[K]>;
+};
+
+/**
+ * @public
+ */
+export type ElementFor<
+ TagName extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap,
+> = TagName extends keyof HTMLElementTagNameMap
+ ? HTMLElementTagNameMap[TagName]
+ : TagName extends keyof SVGElementTagNameMap
+ ? SVGElementTagNameMap[TagName]
+ : never;
+
+/**
+ * @public
+ */
+export type EvaluateFunc<T extends unknown[]> = (
+ ...params: InnerParams<T>
+) => Awaitable<unknown>;
+
+/**
+ * @public
+ */
+export type EvaluateFuncWith<V, T extends unknown[]> = (
+ ...params: [V, ...InnerParams<T>]
+) => Awaitable<unknown>;
+
+/**
+ * @public
+ */
+export type NodeFor<ComplexSelector extends string> =
+ TypeSelectorOfComplexSelector<ComplexSelector> extends infer TypeSelector
+ ? TypeSelector extends
+ | keyof HTMLElementTagNameMap
+ | keyof SVGElementTagNameMap
+ ? ElementFor<TypeSelector>
+ : Element
+ : never;
+
+type TypeSelectorOfComplexSelector<ComplexSelector extends string> =
+ CompoundSelectorsOfComplexSelector<ComplexSelector> extends infer CompoundSelectors
+ ? CompoundSelectors extends NonEmptyReadonlyArray<string>
+ ? Last<CompoundSelectors> extends infer LastCompoundSelector
+ ? LastCompoundSelector extends string
+ ? TypeSelectorOfCompoundSelector<LastCompoundSelector>
+ : never
+ : never
+ : unknown
+ : never;
+
+type TypeSelectorOfCompoundSelector<CompoundSelector extends string> =
+ SplitWithDelemiters<
+ CompoundSelector,
+ BeginSubclassSelectorTokens
+ > extends infer CompoundSelectorTokens
+ ? CompoundSelectorTokens extends [infer TypeSelector, ...any[]]
+ ? TypeSelector extends ''
+ ? unknown
+ : TypeSelector
+ : never
+ : never;
+
+type Last<Arr extends NonEmptyReadonlyArray<unknown>> = Arr extends [
+ infer Head,
+ ...infer Tail,
+]
+ ? Tail extends NonEmptyReadonlyArray<unknown>
+ ? Last<Tail>
+ : Head
+ : never;
+
+type NonEmptyReadonlyArray<T> = [T, ...(readonly T[])];
+
+type CompoundSelectorsOfComplexSelector<ComplexSelector extends string> =
+ SplitWithDelemiters<
+ ComplexSelector,
+ CombinatorTokens
+ > extends infer IntermediateTokens
+ ? IntermediateTokens extends readonly string[]
+ ? Drop<IntermediateTokens, ''>
+ : never
+ : never;
+
+type SplitWithDelemiters<
+ Input extends string,
+ Delemiters extends readonly string[],
+> = Delemiters extends [infer FirstDelemiter, ...infer RestDelemiters]
+ ? FirstDelemiter extends string
+ ? RestDelemiters extends readonly string[]
+ ? FlatmapSplitWithDelemiters<Split<Input, FirstDelemiter>, RestDelemiters>
+ : never
+ : never
+ : [Input];
+
+type BeginSubclassSelectorTokens = ['.', '#', '[', ':'];
+
+type CombinatorTokens = [' ', '>', '+', '~', '|', '|'];
+
+type Drop<
+ Arr extends readonly unknown[],
+ Remove,
+ Acc extends unknown[] = [],
+> = Arr extends [infer Head, ...infer Tail]
+ ? Head extends Remove
+ ? Drop<Tail, Remove>
+ : Drop<Tail, Remove, [...Acc, Head]>
+ : Acc;
+
+type FlatmapSplitWithDelemiters<
+ Inputs extends readonly string[],
+ Delemiters extends readonly string[],
+ Acc extends string[] = [],
+> = Inputs extends [infer FirstInput, ...infer RestInputs]
+ ? FirstInput extends string
+ ? RestInputs extends readonly string[]
+ ? FlatmapSplitWithDelemiters<
+ RestInputs,
+ Delemiters,
+ [...Acc, ...SplitWithDelemiters<FirstInput, Delemiters>]
+ >
+ : Acc
+ : Acc
+ : Acc;
+
+type Split<
+ Input extends string,
+ Delimiter extends string,
+ Acc extends string[] = [],
+> = Input extends `${infer Prefix}${Delimiter}${infer Suffix}`
+ ? Split<Suffix, Delimiter, [...Acc, Prefix]>
+ : [...Acc, Input];
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts
new file mode 100644
index 0000000000..2c8f76f664
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts
@@ -0,0 +1,447 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type FS from 'fs/promises';
+import type {Readable} from 'stream';
+
+import {map, NEVER, Observable, timer} from '../../third_party/rxjs/rxjs.js';
+import type {CDPSession} from '../api/CDPSession.js';
+import {isNode} from '../environment.js';
+import {assert} from '../util/assert.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import {debug} from './Debug.js';
+import {TimeoutError} from './Errors.js';
+import type {EventEmitter, EventType} from './EventEmitter.js';
+import type {
+ LowerCasePaperFormat,
+ ParsedPDFOptions,
+ PDFOptions,
+} from './PDFOptions.js';
+import {paperFormats} from './PDFOptions.js';
+
+/**
+ * @internal
+ */
+export const debugError = debug('puppeteer:error');
+
+/**
+ * @internal
+ */
+export const DEFAULT_VIEWPORT = Object.freeze({width: 800, height: 600});
+
+/**
+ * @internal
+ */
+const SOURCE_URL = Symbol('Source URL for Puppeteer evaluation scripts');
+
+/**
+ * @internal
+ */
+export class PuppeteerURL {
+ static INTERNAL_URL = 'pptr:internal';
+
+ static fromCallSite(
+ functionName: string,
+ site: NodeJS.CallSite
+ ): PuppeteerURL {
+ const url = new PuppeteerURL();
+ url.#functionName = functionName;
+ url.#siteString = site.toString();
+ return url;
+ }
+
+ static parse = (url: string): PuppeteerURL => {
+ url = url.slice('pptr:'.length);
+ const [functionName = '', siteString = ''] = url.split(';');
+ const puppeteerUrl = new PuppeteerURL();
+ puppeteerUrl.#functionName = functionName;
+ puppeteerUrl.#siteString = decodeURIComponent(siteString);
+ return puppeteerUrl;
+ };
+
+ static isPuppeteerURL = (url: string): boolean => {
+ return url.startsWith('pptr:');
+ };
+
+ #functionName!: string;
+ #siteString!: string;
+
+ get functionName(): string {
+ return this.#functionName;
+ }
+
+ get siteString(): string {
+ return this.#siteString;
+ }
+
+ toString(): string {
+ return `pptr:${[
+ this.#functionName,
+ encodeURIComponent(this.#siteString),
+ ].join(';')}`;
+ }
+}
+
+/**
+ * @internal
+ */
+export const withSourcePuppeteerURLIfNone = <T extends NonNullable<unknown>>(
+ functionName: string,
+ object: T
+): T => {
+ if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
+ return object;
+ }
+ const original = Error.prepareStackTrace;
+ Error.prepareStackTrace = (_, stack) => {
+ // First element is the function.
+ // Second element is the caller of this function.
+ // Third element is the caller of the caller of this function
+ // which is precisely what we want.
+ return stack[2];
+ };
+ const site = new Error().stack as unknown as NodeJS.CallSite;
+ Error.prepareStackTrace = original;
+ return Object.assign(object, {
+ [SOURCE_URL]: PuppeteerURL.fromCallSite(functionName, site),
+ });
+};
+
+/**
+ * @internal
+ */
+export const getSourcePuppeteerURLIfAvailable = <
+ T extends NonNullable<unknown>,
+>(
+ object: T
+): PuppeteerURL | undefined => {
+ if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
+ return object[SOURCE_URL as keyof T] as PuppeteerURL;
+ }
+ return undefined;
+};
+
+/**
+ * @internal
+ */
+export const isString = (obj: unknown): obj is string => {
+ return typeof obj === 'string' || obj instanceof String;
+};
+
+/**
+ * @internal
+ */
+export const isNumber = (obj: unknown): obj is number => {
+ return typeof obj === 'number' || obj instanceof Number;
+};
+
+/**
+ * @internal
+ */
+export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => {
+ return typeof obj === 'object' && obj?.constructor === Object;
+};
+
+/**
+ * @internal
+ */
+export const isRegExp = (obj: unknown): obj is RegExp => {
+ return typeof obj === 'object' && obj?.constructor === RegExp;
+};
+
+/**
+ * @internal
+ */
+export const isDate = (obj: unknown): obj is Date => {
+ return typeof obj === 'object' && obj?.constructor === Date;
+};
+
+/**
+ * @internal
+ */
+export function evaluationString(
+ fun: Function | string,
+ ...args: unknown[]
+): string {
+ if (isString(fun)) {
+ assert(args.length === 0, 'Cannot evaluate a string with arguments');
+ return fun;
+ }
+
+ function serializeArgument(arg: unknown): string {
+ if (Object.is(arg, undefined)) {
+ return 'undefined';
+ }
+ return JSON.stringify(arg);
+ }
+
+ return `(${fun})(${args.map(serializeArgument).join(',')})`;
+}
+
+/**
+ * @internal
+ */
+let fs: typeof FS | null = null;
+/**
+ * @internal
+ */
+export async function importFSPromises(): Promise<typeof FS> {
+ if (!fs) {
+ try {
+ fs = await import('fs/promises');
+ } catch (error) {
+ if (error instanceof TypeError) {
+ throw new Error(
+ 'Cannot write to a path outside of a Node-like environment.'
+ );
+ }
+ throw error;
+ }
+ }
+ return fs;
+}
+
+/**
+ * @internal
+ */
+export async function getReadableAsBuffer(
+ readable: Readable,
+ path?: string
+): Promise<Buffer | null> {
+ const buffers = [];
+ if (path) {
+ const fs = await importFSPromises();
+ const fileHandle = await fs.open(path, 'w+');
+ try {
+ for await (const chunk of readable) {
+ buffers.push(chunk);
+ await fileHandle.writeFile(chunk);
+ }
+ } finally {
+ await fileHandle.close();
+ }
+ } else {
+ for await (const chunk of readable) {
+ buffers.push(chunk);
+ }
+ }
+ try {
+ return Buffer.concat(buffers);
+ } catch (error) {
+ return null;
+ }
+}
+
+/**
+ * @internal
+ */
+export async function getReadableFromProtocolStream(
+ client: CDPSession,
+ handle: string
+): Promise<Readable> {
+ // TODO: Once Node 18 becomes the lowest supported version, we can migrate to
+ // ReadableStream.
+ if (!isNode) {
+ throw new Error('Cannot create a stream outside of Node.js environment.');
+ }
+
+ const {Readable} = await import('stream');
+
+ let eof = false;
+ return new Readable({
+ async read(size: number) {
+ if (eof) {
+ return;
+ }
+
+ try {
+ const response = await client.send('IO.read', {handle, size});
+ this.push(response.data, response.base64Encoded ? 'base64' : undefined);
+ if (response.eof) {
+ eof = true;
+ await client.send('IO.close', {handle});
+ this.push(null);
+ }
+ } catch (error) {
+ if (isErrorLike(error)) {
+ this.destroy(error);
+ return;
+ }
+ throw error;
+ }
+ },
+ });
+}
+
+/**
+ * @internal
+ */
+export function validateDialogType(
+ type: string
+): 'alert' | 'confirm' | 'prompt' | 'beforeunload' {
+ let dialogType = null;
+ const validDialogTypes = new Set([
+ 'alert',
+ 'confirm',
+ 'prompt',
+ 'beforeunload',
+ ]);
+
+ if (validDialogTypes.has(type)) {
+ dialogType = type;
+ }
+ assert(dialogType, `Unknown javascript dialog type: ${type}`);
+ return dialogType as 'alert' | 'confirm' | 'prompt' | 'beforeunload';
+}
+
+/**
+ * @internal
+ */
+export function timeout(ms: number): Observable<never> {
+ return ms === 0
+ ? NEVER
+ : timer(ms).pipe(
+ map(() => {
+ throw new TimeoutError(`Timed out after waiting ${ms}ms`);
+ })
+ );
+}
+
+/**
+ * @internal
+ */
+export const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
+
+/**
+ * @internal
+ */
+export const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
+/**
+ * @internal
+ */
+export function getSourceUrlComment(url: string): string {
+ return `//# sourceURL=${url}`;
+}
+
+/**
+ * @internal
+ */
+export const NETWORK_IDLE_TIME = 500;
+
+/**
+ * @internal
+ */
+export function parsePDFOptions(
+ options: PDFOptions = {},
+ lengthUnit: 'in' | 'cm' = 'in'
+): ParsedPDFOptions {
+ const defaults: Omit<ParsedPDFOptions, 'width' | 'height' | 'margin'> = {
+ scale: 1,
+ displayHeaderFooter: false,
+ headerTemplate: '',
+ footerTemplate: '',
+ printBackground: false,
+ landscape: false,
+ pageRanges: '',
+ preferCSSPageSize: false,
+ omitBackground: false,
+ tagged: false,
+ };
+
+ let width = 8.5;
+ let height = 11;
+ if (options.format) {
+ const format =
+ paperFormats[options.format.toLowerCase() as LowerCasePaperFormat];
+ assert(format, 'Unknown paper format: ' + options.format);
+ width = format.width;
+ height = format.height;
+ } else {
+ width = convertPrintParameterToInches(options.width, lengthUnit) ?? width;
+ height =
+ convertPrintParameterToInches(options.height, lengthUnit) ?? height;
+ }
+
+ const margin = {
+ top: convertPrintParameterToInches(options.margin?.top, lengthUnit) || 0,
+ left: convertPrintParameterToInches(options.margin?.left, lengthUnit) || 0,
+ bottom:
+ convertPrintParameterToInches(options.margin?.bottom, lengthUnit) || 0,
+ right:
+ convertPrintParameterToInches(options.margin?.right, lengthUnit) || 0,
+ };
+
+ return {
+ ...defaults,
+ ...options,
+ width,
+ height,
+ margin,
+ };
+}
+
+/**
+ * @internal
+ */
+export const unitToPixels = {
+ px: 1,
+ in: 96,
+ cm: 37.8,
+ mm: 3.78,
+};
+
+function convertPrintParameterToInches(
+ parameter?: string | number,
+ lengthUnit: 'in' | 'cm' = 'in'
+): number | undefined {
+ if (typeof parameter === 'undefined') {
+ return undefined;
+ }
+ let pixels;
+ if (isNumber(parameter)) {
+ // Treat numbers as pixel values to be aligned with phantom's paperSize.
+ pixels = parameter;
+ } else if (isString(parameter)) {
+ const text = parameter;
+ let unit = text.substring(text.length - 2).toLowerCase();
+ let valueText = '';
+ if (unit in unitToPixels) {
+ valueText = text.substring(0, text.length - 2);
+ } else {
+ // In case of unknown unit try to parse the whole parameter as number of pixels.
+ // This is consistent with phantom's paperSize behavior.
+ unit = 'px';
+ valueText = text;
+ }
+ const value = Number(valueText);
+ assert(!isNaN(value), 'Failed to parse parameter value: ' + text);
+ pixels = value * unitToPixels[unit as keyof typeof unitToPixels];
+ } else {
+ throw new Error(
+ 'page.pdf() Cannot handle parameter type: ' + typeof parameter
+ );
+ }
+ return pixels / unitToPixels[lengthUnit];
+}
+
+/**
+ * @internal
+ */
+export function fromEmitterEvent<
+ Events extends Record<EventType, unknown>,
+ Event extends keyof Events,
+>(emitter: EventEmitter<Events>, eventName: Event): Observable<Events[Event]> {
+ return new Observable(subscriber => {
+ const listener = (event: Events[Event]) => {
+ subscriber.next(event);
+ };
+ emitter.on(eventName, listener);
+ return () => {
+ emitter.off(eventName, listener);
+ };
+ });
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/environment.ts b/remote/test/puppeteer/packages/puppeteer-core/src/environment.ts
new file mode 100644
index 0000000000..bf7227243d
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/environment.ts
@@ -0,0 +1,10 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export const isNode = !!(typeof process !== 'undefined' && process.version);
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/ARIAQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/ARIAQuerySelector.ts
new file mode 100644
index 0000000000..972b6a6c64
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/ARIAQuerySelector.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+declare global {
+ interface Window {
+ /**
+ * @internal
+ */
+ __ariaQuerySelector(root: Node, selector: string): Promise<Node | null>;
+ /**
+ * @internal
+ */
+ __ariaQuerySelectorAll(root: Node, selector: string): Promise<Node[]>;
+ }
+}
+
+export const ariaQuerySelector = (
+ root: Node,
+ selector: string
+): Promise<Node | null> => {
+ return window.__ariaQuerySelector(root, selector);
+};
+export const ariaQuerySelectorAll = async function* (
+ root: Node,
+ selector: string
+): AsyncIterable<Node> {
+ yield* await window.__ariaQuerySelectorAll(root, selector);
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/CustomQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/CustomQuerySelector.ts
new file mode 100644
index 0000000000..ccd041deea
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/CustomQuerySelector.ts
@@ -0,0 +1,59 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {CustomQueryHandler} from '../common/CustomQueryHandler.js';
+import type {Awaitable, AwaitableIterable} from '../common/types.js';
+
+export interface CustomQuerySelector {
+ querySelector(root: Node, selector: string): Awaitable<Node | null>;
+ querySelectorAll(root: Node, selector: string): AwaitableIterable<Node>;
+}
+
+/**
+ * This class mimics the injected {@link CustomQuerySelectorRegistry}.
+ */
+class CustomQuerySelectorRegistry {
+ #selectors = new Map<string, CustomQuerySelector>();
+
+ register(name: string, handler: CustomQueryHandler): void {
+ if (!handler.queryOne && handler.queryAll) {
+ const querySelectorAll = handler.queryAll;
+ handler.queryOne = (node, selector) => {
+ for (const result of querySelectorAll(node, selector)) {
+ return result;
+ }
+ return null;
+ };
+ } else if (handler.queryOne && !handler.queryAll) {
+ const querySelector = handler.queryOne;
+ handler.queryAll = (node, selector) => {
+ const result = querySelector(node, selector);
+ return result ? [result] : [];
+ };
+ } else if (!handler.queryOne || !handler.queryAll) {
+ throw new Error('At least one query method must be defined.');
+ }
+
+ this.#selectors.set(name, {
+ querySelector: handler.queryOne,
+ querySelectorAll: handler.queryAll!,
+ });
+ }
+
+ unregister(name: string): void {
+ this.#selectors.delete(name);
+ }
+
+ get(name: string): CustomQuerySelector | undefined {
+ return this.#selectors.get(name);
+ }
+
+ clear() {
+ this.#selectors.clear();
+ }
+}
+
+export const customQuerySelectors = new CustomQuerySelectorRegistry();
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/PQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PQuerySelector.ts
new file mode 100644
index 0000000000..11499c072f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PQuerySelector.ts
@@ -0,0 +1,298 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {AwaitableIterable} from '../common/types.js';
+import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
+
+import {ariaQuerySelectorAll} from './ARIAQuerySelector.js';
+import {customQuerySelectors} from './CustomQuerySelector.js';
+import {
+ type ComplexPSelector,
+ type ComplexPSelectorList,
+ type CompoundPSelector,
+ type CSSSelector,
+ parsePSelectors,
+ PCombinator,
+ type PPseudoSelector,
+} from './PSelectorParser.js';
+import {textQuerySelectorAll} from './TextQuerySelector.js';
+import {pierce, pierceAll} from './util.js';
+import {xpathQuerySelectorAll} from './XPathQuerySelector.js';
+
+const IDENT_TOKEN_START = /[-\w\P{ASCII}*]/;
+
+interface QueryableNode extends Node {
+ querySelectorAll: typeof Document.prototype.querySelectorAll;
+}
+
+const isQueryableNode = (node: Node): node is QueryableNode => {
+ return 'querySelectorAll' in node;
+};
+
+class SelectorError extends Error {
+ constructor(selector: string, message: string) {
+ super(`${selector} is not a valid selector: ${message}`);
+ }
+}
+
+class PQueryEngine {
+ #input: string;
+
+ #complexSelector: ComplexPSelector;
+ #compoundSelector: CompoundPSelector = [];
+ #selector: CSSSelector | PPseudoSelector | undefined = undefined;
+
+ elements: AwaitableIterable<Node>;
+
+ constructor(element: Node, input: string, complexSelector: ComplexPSelector) {
+ this.elements = [element];
+ this.#input = input;
+ this.#complexSelector = complexSelector;
+ this.#next();
+ }
+
+ async run(): Promise<void> {
+ if (typeof this.#selector === 'string') {
+ switch (this.#selector.trimStart()) {
+ case ':scope':
+ // `:scope` has some special behavior depending on the node. It always
+ // represents the current node within a compound selector, but by
+ // itself, it depends on the node. For example, Document is
+ // represented by `<html>`, but any HTMLElement is not represented by
+ // itself (i.e. `null`). This can be troublesome if our combinators
+ // are used right after so we treat this selector specially.
+ this.#next();
+ break;
+ }
+ }
+
+ for (; this.#selector !== undefined; this.#next()) {
+ const selector = this.#selector;
+ const input = this.#input;
+ if (typeof selector === 'string') {
+ // The regular expression tests if the selector is a type/universal
+ // selector. Any other case means we want to apply the selector onto
+ // the element itself (e.g. `element.class`, `element>div`,
+ // `element:hover`, etc.).
+ if (selector[0] && IDENT_TOKEN_START.test(selector[0])) {
+ this.elements = AsyncIterableUtil.flatMap(
+ this.elements,
+ async function* (element) {
+ if (isQueryableNode(element)) {
+ yield* element.querySelectorAll(selector);
+ }
+ }
+ );
+ } else {
+ this.elements = AsyncIterableUtil.flatMap(
+ this.elements,
+ async function* (element) {
+ if (!element.parentElement) {
+ if (!isQueryableNode(element)) {
+ return;
+ }
+ yield* element.querySelectorAll(selector);
+ return;
+ }
+
+ let index = 0;
+ for (const child of element.parentElement.children) {
+ ++index;
+ if (child === element) {
+ break;
+ }
+ }
+ yield* element.parentElement.querySelectorAll(
+ `:scope>:nth-child(${index})${selector}`
+ );
+ }
+ );
+ }
+ } else {
+ this.elements = AsyncIterableUtil.flatMap(
+ this.elements,
+ async function* (element) {
+ switch (selector.name) {
+ case 'text':
+ yield* textQuerySelectorAll(element, selector.value);
+ break;
+ case 'xpath':
+ yield* xpathQuerySelectorAll(element, selector.value);
+ break;
+ case 'aria':
+ yield* ariaQuerySelectorAll(element, selector.value);
+ break;
+ default:
+ const querySelector = customQuerySelectors.get(selector.name);
+ if (!querySelector) {
+ throw new SelectorError(
+ input,
+ `Unknown selector type: ${selector.name}`
+ );
+ }
+ yield* querySelector.querySelectorAll(element, selector.value);
+ }
+ }
+ );
+ }
+ }
+ }
+
+ #next() {
+ if (this.#compoundSelector.length !== 0) {
+ this.#selector = this.#compoundSelector.shift();
+ return;
+ }
+ if (this.#complexSelector.length === 0) {
+ this.#selector = undefined;
+ return;
+ }
+ const selector = this.#complexSelector.shift();
+ switch (selector) {
+ case PCombinator.Child: {
+ this.elements = AsyncIterableUtil.flatMap(this.elements, pierce);
+ this.#next();
+ break;
+ }
+ case PCombinator.Descendent: {
+ this.elements = AsyncIterableUtil.flatMap(this.elements, pierceAll);
+ this.#next();
+ break;
+ }
+ default:
+ this.#compoundSelector = selector as CompoundPSelector;
+ this.#next();
+ break;
+ }
+ }
+}
+
+class DepthCalculator {
+ #cache = new WeakMap<Node, number[]>();
+
+ calculate(node: Node | null, depth: number[] = []): number[] {
+ if (node === null) {
+ return depth;
+ }
+ if (node instanceof ShadowRoot) {
+ node = node.host;
+ }
+
+ const cachedDepth = this.#cache.get(node);
+ if (cachedDepth) {
+ return [...cachedDepth, ...depth];
+ }
+
+ let index = 0;
+ for (
+ let prevSibling = node.previousSibling;
+ prevSibling;
+ prevSibling = prevSibling.previousSibling
+ ) {
+ ++index;
+ }
+
+ const value = this.calculate(node.parentNode, [index]);
+ this.#cache.set(node, value);
+ return [...value, ...depth];
+ }
+}
+
+const compareDepths = (a: number[], b: number[]): -1 | 0 | 1 => {
+ if (a.length + b.length === 0) {
+ return 0;
+ }
+ const [i = -1, ...otherA] = a;
+ const [j = -1, ...otherB] = b;
+ if (i === j) {
+ return compareDepths(otherA, otherB);
+ }
+ return i < j ? -1 : 1;
+};
+
+const domSort = async function* (elements: AwaitableIterable<Node>) {
+ const results = new Set<Node>();
+ for await (const element of elements) {
+ results.add(element);
+ }
+ const calculator = new DepthCalculator();
+ yield* [...results.values()]
+ .map(result => {
+ return [result, calculator.calculate(result)] as const;
+ })
+ .sort(([, a], [, b]) => {
+ return compareDepths(a, b);
+ })
+ .map(([result]) => {
+ return result;
+ });
+};
+
+/**
+ * Queries the given node for all nodes matching the given text selector.
+ *
+ * @internal
+ */
+export const pQuerySelectorAll = function (
+ root: Node,
+ selector: string
+): AwaitableIterable<Node> {
+ let selectors: ComplexPSelectorList;
+ let isPureCSS: boolean;
+ try {
+ [selectors, isPureCSS] = parsePSelectors(selector);
+ } catch (error) {
+ return (root as unknown as QueryableNode).querySelectorAll(selector);
+ }
+
+ if (isPureCSS) {
+ return (root as unknown as QueryableNode).querySelectorAll(selector);
+ }
+ // If there are any empty elements, then this implies the selector has
+ // contiguous combinators (e.g. `>>> >>>>`) or starts/ends with one which we
+ // treat as illegal, similar to existing behavior.
+ if (
+ selectors.some(parts => {
+ let i = 0;
+ return parts.some(parts => {
+ if (typeof parts === 'string') {
+ ++i;
+ } else {
+ i = 0;
+ }
+ return i > 1;
+ });
+ })
+ ) {
+ throw new SelectorError(
+ selector,
+ 'Multiple deep combinators found in sequence.'
+ );
+ }
+
+ return domSort(
+ AsyncIterableUtil.flatMap(selectors, selectorParts => {
+ const query = new PQueryEngine(root, selector, selectorParts);
+ void query.run();
+ return query.elements;
+ })
+ );
+};
+
+/**
+ * Queries the given node for all nodes matching the given text selector.
+ *
+ * @internal
+ */
+export const pQuerySelector = async function (
+ root: Node,
+ selector: string
+): Promise<Node | null> {
+ for await (const element of pQuerySelectorAll(root, selector)) {
+ return element;
+ }
+ return null;
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts
new file mode 100644
index 0000000000..8044562348
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts
@@ -0,0 +1,105 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {type Token, tokenize, TOKENS, stringify} from 'parsel-js';
+
+export type CSSSelector = string;
+export interface PPseudoSelector {
+ name: string;
+ value: string;
+}
+export const enum PCombinator {
+ Descendent = '>>>',
+ Child = '>>>>',
+}
+export type CompoundPSelector = Array<CSSSelector | PPseudoSelector>;
+export type ComplexPSelector = Array<CompoundPSelector | PCombinator>;
+export type ComplexPSelectorList = ComplexPSelector[];
+
+TOKENS['combinator'] = /\s*(>>>>?|[\s>+~])\s*/g;
+
+const ESCAPE_REGEXP = /\\[\s\S]/g;
+const unquote = (text: string): string => {
+ if (text.length <= 1) {
+ return text;
+ }
+ if ((text[0] === '"' || text[0] === "'") && text.endsWith(text[0])) {
+ text = text.slice(1, -1);
+ }
+ return text.replace(ESCAPE_REGEXP, match => {
+ return match[1] as string;
+ });
+};
+
+export function parsePSelectors(
+ selector: string
+): [selector: ComplexPSelectorList, isPureCSS: boolean] {
+ let isPureCSS = true;
+ const tokens = tokenize(selector);
+ if (tokens.length === 0) {
+ return [[], isPureCSS];
+ }
+ let compoundSelector: CompoundPSelector = [];
+ let complexSelector: ComplexPSelector = [compoundSelector];
+ const selectors: ComplexPSelectorList = [complexSelector];
+ const storage: Token[] = [];
+ for (const token of tokens) {
+ switch (token.type) {
+ case 'combinator':
+ switch (token.content) {
+ case PCombinator.Descendent:
+ isPureCSS = false;
+ if (storage.length) {
+ compoundSelector.push(stringify(storage));
+ storage.splice(0);
+ }
+ compoundSelector = [];
+ complexSelector.push(PCombinator.Descendent);
+ complexSelector.push(compoundSelector);
+ continue;
+ case PCombinator.Child:
+ isPureCSS = false;
+ if (storage.length) {
+ compoundSelector.push(stringify(storage));
+ storage.splice(0);
+ }
+ compoundSelector = [];
+ complexSelector.push(PCombinator.Child);
+ complexSelector.push(compoundSelector);
+ continue;
+ }
+ break;
+ case 'pseudo-element':
+ if (!token.name.startsWith('-p-')) {
+ break;
+ }
+ isPureCSS = false;
+ if (storage.length) {
+ compoundSelector.push(stringify(storage));
+ storage.splice(0);
+ }
+ compoundSelector.push({
+ name: token.name.slice(3),
+ value: unquote(token.argument ?? ''),
+ });
+ continue;
+ case 'comma':
+ if (storage.length) {
+ compoundSelector.push(stringify(storage));
+ storage.splice(0);
+ }
+ compoundSelector = [];
+ complexSelector = [compoundSelector];
+ selectors.push(complexSelector);
+ continue;
+ }
+ storage.push(token);
+ }
+ if (storage.length) {
+ compoundSelector.push(stringify(storage));
+ }
+ return [selectors, isPureCSS];
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/PierceQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PierceQuerySelector.ts
new file mode 100644
index 0000000000..c224ee8324
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PierceQuerySelector.ts
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export const pierceQuerySelector = (
+ root: Node,
+ selector: string
+): Element | null => {
+ let found: Node | null = null;
+ const search = (root: Node) => {
+ const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
+ do {
+ const currentNode = iter.currentNode as Element;
+ if (currentNode.shadowRoot) {
+ search(currentNode.shadowRoot);
+ }
+ if (currentNode instanceof ShadowRoot) {
+ continue;
+ }
+ if (currentNode !== root && !found && currentNode.matches(selector)) {
+ found = currentNode;
+ }
+ } while (!found && iter.nextNode());
+ };
+ if (root instanceof Document) {
+ root = root.documentElement;
+ }
+ search(root);
+ return found;
+};
+
+/**
+ * @internal
+ */
+export const pierceQuerySelectorAll = (
+ element: Node,
+ selector: string
+): Element[] => {
+ const result: Element[] = [];
+ const collect = (root: Node) => {
+ const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
+ do {
+ const currentNode = iter.currentNode as Element;
+ if (currentNode.shadowRoot) {
+ collect(currentNode.shadowRoot);
+ }
+ if (currentNode instanceof ShadowRoot) {
+ continue;
+ }
+ if (currentNode !== root && currentNode.matches(selector)) {
+ result.push(currentNode);
+ }
+ } while (iter.nextNode());
+ };
+ if (element instanceof Document) {
+ element = element.documentElement;
+ }
+ collect(element);
+ return result;
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/Poller.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/Poller.ts
new file mode 100644
index 0000000000..68b9f1812b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/Poller.ts
@@ -0,0 +1,168 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {assert} from '../util/assert.js';
+import {Deferred} from '../util/Deferred.js';
+
+/**
+ * @internal
+ */
+export interface Poller<T> {
+ start(): Promise<void>;
+ stop(): Promise<void>;
+ result(): Promise<T>;
+}
+
+/**
+ * @internal
+ */
+export class MutationPoller<T> implements Poller<T> {
+ #fn: () => Promise<T>;
+
+ #root: Node;
+
+ #observer?: MutationObserver;
+ #deferred?: Deferred<T>;
+ constructor(fn: () => Promise<T>, root: Node) {
+ this.#fn = fn;
+ this.#root = root;
+ }
+
+ async start(): Promise<void> {
+ const deferred = (this.#deferred = Deferred.create<T>());
+ const result = await this.#fn();
+ if (result) {
+ deferred.resolve(result);
+ return;
+ }
+
+ this.#observer = new MutationObserver(async () => {
+ const result = await this.#fn();
+ if (!result) {
+ return;
+ }
+ deferred.resolve(result);
+ await this.stop();
+ });
+ this.#observer.observe(this.#root, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ });
+ }
+
+ async stop(): Promise<void> {
+ assert(this.#deferred, 'Polling never started.');
+ if (!this.#deferred.finished()) {
+ this.#deferred.reject(new Error('Polling stopped'));
+ }
+ if (this.#observer) {
+ this.#observer.disconnect();
+ this.#observer = undefined;
+ }
+ }
+
+ result(): Promise<T> {
+ assert(this.#deferred, 'Polling never started.');
+ return this.#deferred.valueOrThrow();
+ }
+}
+
+/**
+ * @internal
+ */
+export class RAFPoller<T> implements Poller<T> {
+ #fn: () => Promise<T>;
+ #deferred?: Deferred<T>;
+ constructor(fn: () => Promise<T>) {
+ this.#fn = fn;
+ }
+
+ async start(): Promise<void> {
+ const deferred = (this.#deferred = Deferred.create<T>());
+ const result = await this.#fn();
+ if (result) {
+ deferred.resolve(result);
+ return;
+ }
+
+ const poll = async () => {
+ if (deferred.finished()) {
+ return;
+ }
+ const result = await this.#fn();
+ if (!result) {
+ window.requestAnimationFrame(poll);
+ return;
+ }
+ deferred.resolve(result);
+ await this.stop();
+ };
+ window.requestAnimationFrame(poll);
+ }
+
+ async stop(): Promise<void> {
+ assert(this.#deferred, 'Polling never started.');
+ if (!this.#deferred.finished()) {
+ this.#deferred.reject(new Error('Polling stopped'));
+ }
+ }
+
+ result(): Promise<T> {
+ assert(this.#deferred, 'Polling never started.');
+ return this.#deferred.valueOrThrow();
+ }
+}
+
+/**
+ * @internal
+ */
+
+export class IntervalPoller<T> implements Poller<T> {
+ #fn: () => Promise<T>;
+ #ms: number;
+
+ #interval?: NodeJS.Timeout;
+ #deferred?: Deferred<T>;
+ constructor(fn: () => Promise<T>, ms: number) {
+ this.#fn = fn;
+ this.#ms = ms;
+ }
+
+ async start(): Promise<void> {
+ const deferred = (this.#deferred = Deferred.create<T>());
+ const result = await this.#fn();
+ if (result) {
+ deferred.resolve(result);
+ return;
+ }
+
+ this.#interval = setInterval(async () => {
+ const result = await this.#fn();
+ if (!result) {
+ return;
+ }
+ deferred.resolve(result);
+ await this.stop();
+ }, this.#ms);
+ }
+
+ async stop(): Promise<void> {
+ assert(this.#deferred, 'Polling never started.');
+ if (!this.#deferred.finished()) {
+ this.#deferred.reject(new Error('Polling stopped'));
+ }
+ if (this.#interval) {
+ clearInterval(this.#interval);
+ this.#interval = undefined;
+ }
+ }
+
+ result(): Promise<T> {
+ assert(this.#deferred, 'Polling never started.');
+ return this.#deferred.valueOrThrow();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts
new file mode 100644
index 0000000000..ffe8980d5e
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+interface NonTrivialValueNode extends Node {
+ value: string;
+}
+
+const TRIVIAL_VALUE_INPUT_TYPES = new Set(['checkbox', 'image', 'radio']);
+
+/**
+ * Determines if the node has a non-trivial value property.
+ *
+ * @internal
+ */
+const isNonTrivialValueNode = (node: Node): node is NonTrivialValueNode => {
+ if (node instanceof HTMLSelectElement) {
+ return true;
+ }
+ if (node instanceof HTMLTextAreaElement) {
+ return true;
+ }
+ if (
+ node instanceof HTMLInputElement &&
+ !TRIVIAL_VALUE_INPUT_TYPES.has(node.type)
+ ) {
+ return true;
+ }
+ return false;
+};
+
+const UNSUITABLE_NODE_NAMES = new Set(['SCRIPT', 'STYLE']);
+
+/**
+ * Determines whether a given node is suitable for text matching.
+ *
+ * @internal
+ */
+export const isSuitableNodeForTextMatching = (node: Node): boolean => {
+ return (
+ !UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node)
+ );
+};
+
+/**
+ * @internal
+ */
+export interface TextContent {
+ // Contains the full text of the node.
+ full: string;
+ // Contains the text immediately beneath the node.
+ immediate: string[];
+}
+
+/**
+ * Maps {@link Node}s to their computed {@link TextContent}.
+ */
+const textContentCache = new WeakMap<Node, TextContent>();
+const eraseFromCache = (node: Node | null) => {
+ while (node) {
+ textContentCache.delete(node);
+ if (node instanceof ShadowRoot) {
+ node = node.host;
+ } else {
+ node = node.parentNode;
+ }
+ }
+};
+
+/**
+ * Erases the cache when the tree has mutated text.
+ */
+const observedNodes = new WeakSet<Node>();
+const textChangeObserver = new MutationObserver(mutations => {
+ for (const mutation of mutations) {
+ eraseFromCache(mutation.target);
+ }
+});
+
+/**
+ * Builds the text content of a node using some custom logic.
+ *
+ * @remarks
+ * The primary reason this function exists is due to {@link ShadowRoot}s not having
+ * text content.
+ *
+ * @internal
+ */
+export const createTextContent = (root: Node): TextContent => {
+ let value = textContentCache.get(root);
+ if (value) {
+ return value;
+ }
+ value = {full: '', immediate: []};
+ if (!isSuitableNodeForTextMatching(root)) {
+ return value;
+ }
+
+ let currentImmediate = '';
+ if (isNonTrivialValueNode(root)) {
+ value.full = root.value;
+ value.immediate.push(root.value);
+
+ root.addEventListener(
+ 'input',
+ event => {
+ eraseFromCache(event.target as HTMLInputElement);
+ },
+ {once: true, capture: true}
+ );
+ } else {
+ for (let child = root.firstChild; child; child = child.nextSibling) {
+ if (child.nodeType === Node.TEXT_NODE) {
+ value.full += child.nodeValue ?? '';
+ currentImmediate += child.nodeValue ?? '';
+ continue;
+ }
+ if (currentImmediate) {
+ value.immediate.push(currentImmediate);
+ }
+ currentImmediate = '';
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ value.full += createTextContent(child).full;
+ }
+ }
+ if (currentImmediate) {
+ value.immediate.push(currentImmediate);
+ }
+ if (root instanceof Element && root.shadowRoot) {
+ value.full += createTextContent(root.shadowRoot).full;
+ }
+
+ if (!observedNodes.has(root)) {
+ textChangeObserver.observe(root, {
+ childList: true,
+ characterData: true,
+ subtree: true,
+ });
+ observedNodes.add(root);
+ }
+ }
+ textContentCache.set(root, value);
+ return value;
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextQuerySelector.ts
new file mode 100644
index 0000000000..debc423ccf
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextQuerySelector.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ createTextContent,
+ isSuitableNodeForTextMatching,
+} from './TextContent.js';
+
+/**
+ * Queries the given node for all nodes matching the given text selector.
+ *
+ * @internal
+ */
+export const textQuerySelectorAll = function* (
+ root: Node,
+ selector: string
+): Generator<Element> {
+ let yielded = false;
+ for (const node of root.childNodes) {
+ if (node instanceof Element && isSuitableNodeForTextMatching(node)) {
+ let matches: Generator<Element, boolean>;
+ if (!node.shadowRoot) {
+ matches = textQuerySelectorAll(node, selector);
+ } else {
+ matches = textQuerySelectorAll(node.shadowRoot, selector);
+ }
+ for (const match of matches) {
+ yield match;
+ yielded = true;
+ }
+ }
+ }
+ if (yielded) {
+ return;
+ }
+
+ if (root instanceof Element && isSuitableNodeForTextMatching(root)) {
+ const textContent = createTextContent(root);
+ if (textContent.full.includes(selector)) {
+ yield root;
+ }
+ }
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/XPathQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/XPathQuerySelector.ts
new file mode 100644
index 0000000000..039bfa5e54
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/XPathQuerySelector.ts
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export const xpathQuerySelectorAll = function* (
+ root: Node,
+ selector: string,
+ maxResults = -1
+): Iterable<Node> {
+ const doc = root.ownerDocument || document;
+ const iterator = doc.evaluate(
+ selector,
+ root,
+ null,
+ XPathResult.ORDERED_NODE_ITERATOR_TYPE
+ );
+ const items = [];
+ let item;
+
+ // Read all results upfront to avoid
+ // https://stackoverflow.com/questions/48235278/xpath-error-the-document-has-mutated-since-the-result-was-returned.
+ while ((item = iterator.iterateNext())) {
+ items.push(item);
+ if (maxResults && items.length === maxResults) {
+ break;
+ }
+ }
+
+ for (let i = 0; i < items.length; i++) {
+ item = items[i];
+ yield item as Node;
+ delete items[i];
+ }
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/injected.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/injected.ts
new file mode 100644
index 0000000000..e81d274290
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/injected.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {Deferred} from '../util/Deferred.js';
+import {createFunction} from '../util/Function.js';
+
+import * as ARIAQuerySelector from './ARIAQuerySelector.js';
+import * as CustomQuerySelectors from './CustomQuerySelector.js';
+import * as PierceQuerySelector from './PierceQuerySelector.js';
+import {IntervalPoller, MutationPoller, RAFPoller} from './Poller.js';
+import * as PQuerySelector from './PQuerySelector.js';
+import {
+ createTextContent,
+ isSuitableNodeForTextMatching,
+} from './TextContent.js';
+import * as TextQuerySelector from './TextQuerySelector.js';
+import * as util from './util.js';
+import * as XPathQuerySelector from './XPathQuerySelector.js';
+
+/**
+ * @internal
+ */
+const PuppeteerUtil = Object.freeze({
+ ...ARIAQuerySelector,
+ ...CustomQuerySelectors,
+ ...PierceQuerySelector,
+ ...PQuerySelector,
+ ...TextQuerySelector,
+ ...util,
+ ...XPathQuerySelector,
+ Deferred,
+ createFunction,
+ createTextContent,
+ IntervalPoller,
+ isSuitableNodeForTextMatching,
+ MutationPoller,
+ RAFPoller,
+});
+
+/**
+ * @internal
+ */
+type PuppeteerUtil = typeof PuppeteerUtil;
+
+/**
+ * @internal
+ */
+export default PuppeteerUtil;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts
new file mode 100644
index 0000000000..34fe8f7748
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts
@@ -0,0 +1,67 @@
+const HIDDEN_VISIBILITY_VALUES = ['hidden', 'collapse'];
+
+/**
+ * @internal
+ */
+export const checkVisibility = (
+ node: Node | null,
+ visible?: boolean
+): Node | boolean => {
+ if (!node) {
+ return visible === false;
+ }
+ if (visible === undefined) {
+ return node;
+ }
+ const element = (
+ node.nodeType === Node.TEXT_NODE ? node.parentElement : node
+ ) as Element;
+
+ const style = window.getComputedStyle(element);
+ const isVisible =
+ style &&
+ !HIDDEN_VISIBILITY_VALUES.includes(style.visibility) &&
+ !isBoundingBoxEmpty(element);
+ return visible === isVisible ? node : false;
+};
+
+function isBoundingBoxEmpty(element: Element): boolean {
+ const rect = element.getBoundingClientRect();
+ return rect.width === 0 || rect.height === 0;
+}
+
+const hasShadowRoot = (node: Node): node is Node & {shadowRoot: ShadowRoot} => {
+ return 'shadowRoot' in node && node.shadowRoot instanceof ShadowRoot;
+};
+
+/**
+ * @internal
+ */
+export function* pierce(root: Node): IterableIterator<Node | ShadowRoot> {
+ if (hasShadowRoot(root)) {
+ yield root.shadowRoot;
+ } else {
+ yield root;
+ }
+}
+
+/**
+ * @internal
+ */
+export function* pierceAll(root: Node): IterableIterator<Node | ShadowRoot> {
+ root = pierce(root).next().value;
+ yield root;
+ const walkers = [document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)];
+ for (const walker of walkers) {
+ let node: Element | null;
+ while ((node = walker.nextNode() as Element | null)) {
+ if (!node.shadowRoot) {
+ continue;
+ }
+ yield node.shadowRoot;
+ walkers.push(
+ document.createTreeWalker(node.shadowRoot, NodeFilter.SHOW_ELEMENT)
+ );
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.test.ts
new file mode 100644
index 0000000000..9abd3697f7
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.test.ts
@@ -0,0 +1,59 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {getFeatures, removeMatchingFlags} from './ChromeLauncher.js';
+
+describe('getFeatures', () => {
+ it('returns an empty array when no options are provided', () => {
+ const result = getFeatures('--foo');
+ expect(result).toEqual([]);
+ });
+
+ it('returns an empty array when no options match the flag', () => {
+ const result = getFeatures('--foo', ['--bar', '--baz']);
+ expect(result).toEqual([]);
+ });
+
+ it('returns an array of values when options match the flag', () => {
+ const result = getFeatures('--foo', ['--foo=bar', '--foo=baz']);
+ expect(result).toEqual(['bar', 'baz']);
+ });
+
+ it('does not handle whitespace', () => {
+ const result = getFeatures('--foo', ['--foo bar', '--foo baz ']);
+ expect(result).toEqual([]);
+ });
+
+ it('handles equals sign around the flag and value', () => {
+ const result = getFeatures('--foo', ['--foo=bar', '--foo=baz ']);
+ expect(result).toEqual(['bar', 'baz']);
+ });
+});
+
+describe('removeMatchingFlags', () => {
+ it('empty', () => {
+ const a: string[] = [];
+ expect(removeMatchingFlags(a, '--foo')).toEqual([]);
+ });
+
+ it('with one match', () => {
+ const a: string[] = ['--foo=1', '--bar=baz'];
+ expect(removeMatchingFlags(a, '--foo')).toEqual(['--bar=baz']);
+ });
+
+ it('with multiple matches', () => {
+ const a: string[] = ['--foo=1', '--foo=2', '--bar=baz'];
+ expect(removeMatchingFlags(a, '--foo')).toEqual(['--bar=baz']);
+ });
+
+ it('with no matches', () => {
+ const a: string[] = ['--foo=1', '--bar=baz'];
+ expect(removeMatchingFlags(a, '--baz')).toEqual(['--foo=1', '--bar=baz']);
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts
new file mode 100644
index 0000000000..51d5a19983
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts
@@ -0,0 +1,344 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {mkdtemp} from 'fs/promises';
+import os from 'os';
+import path from 'path';
+
+import {
+ computeSystemExecutablePath,
+ Browser as SupportedBrowsers,
+ ChromeReleaseChannel as BrowsersChromeReleaseChannel,
+} from '@puppeteer/browsers';
+
+import type {Browser} from '../api/Browser.js';
+import {debugError} from '../common/util.js';
+import {assert} from '../util/assert.js';
+
+import type {
+ BrowserLaunchArgumentOptions,
+ ChromeReleaseChannel,
+ PuppeteerNodeLaunchOptions,
+} from './LaunchOptions.js';
+import {ProductLauncher, type ResolvedLaunchArgs} from './ProductLauncher.js';
+import type {PuppeteerNode} from './PuppeteerNode.js';
+import {rm} from './util/fs.js';
+
+/**
+ * @internal
+ */
+export class ChromeLauncher extends ProductLauncher {
+ constructor(puppeteer: PuppeteerNode) {
+ super(puppeteer, 'chrome');
+ }
+
+ override launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> {
+ const headless = options.headless ?? true;
+ if (
+ headless === true &&
+ this.puppeteer.configuration.logLevel === 'warn' &&
+ !Boolean(process.env['PUPPETEER_DISABLE_HEADLESS_WARNING'])
+ ) {
+ console.warn(
+ [
+ '\x1B[1m\x1B[43m\x1B[30m',
+ 'Puppeteer old Headless deprecation warning:\x1B[0m\x1B[33m',
+ ' In the near future `headless: true` will default to the new Headless mode',
+ ' for Chrome instead of the old Headless implementation. For more',
+ ' information, please see https://developer.chrome.com/articles/new-headless/.',
+ ' Consider opting in early by passing `headless: "new"` to `puppeteer.launch()`',
+ ' If you encounter any bugs, please report them to https://github.com/puppeteer/puppeteer/issues/new/choose.\x1B[0m\n',
+ ].join('\n ')
+ );
+ }
+
+ if (
+ this.puppeteer.configuration.logLevel === 'warn' &&
+ process.platform === 'darwin' &&
+ process.arch === 'x64'
+ ) {
+ const cpus = os.cpus();
+ if (cpus[0]?.model.includes('Apple')) {
+ console.warn(
+ [
+ '\x1B[1m\x1B[43m\x1B[30m',
+ 'Degraded performance warning:\x1B[0m\x1B[33m',
+ 'Launching Chrome on Mac Silicon (arm64) from an x64 Node installation results in',
+ 'Rosetta translating the Chrome binary, even if Chrome is already arm64. This would',
+ 'result in huge performance issues. To resolve this, you must run Puppeteer with',
+ 'a version of Node built for arm64.',
+ ].join('\n ')
+ );
+ }
+ }
+
+ return super.launch(options);
+ }
+
+ /**
+ * @internal
+ */
+ override async computeLaunchArguments(
+ options: PuppeteerNodeLaunchOptions = {}
+ ): Promise<ResolvedLaunchArgs> {
+ const {
+ ignoreDefaultArgs = false,
+ args = [],
+ pipe = false,
+ debuggingPort,
+ channel,
+ executablePath,
+ } = options;
+
+ const chromeArguments = [];
+ if (!ignoreDefaultArgs) {
+ chromeArguments.push(...this.defaultArgs(options));
+ } else if (Array.isArray(ignoreDefaultArgs)) {
+ chromeArguments.push(
+ ...this.defaultArgs(options).filter(arg => {
+ return !ignoreDefaultArgs.includes(arg);
+ })
+ );
+ } else {
+ chromeArguments.push(...args);
+ }
+
+ if (
+ !chromeArguments.some(argument => {
+ return argument.startsWith('--remote-debugging-');
+ })
+ ) {
+ if (pipe) {
+ assert(
+ !debuggingPort,
+ 'Browser should be launched with either pipe or debugging port - not both.'
+ );
+ chromeArguments.push('--remote-debugging-pipe');
+ } else {
+ chromeArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
+ }
+ }
+
+ let isTempUserDataDir = false;
+
+ // Check for the user data dir argument, which will always be set even
+ // with a custom directory specified via the userDataDir option.
+ let userDataDirIndex = chromeArguments.findIndex(arg => {
+ return arg.startsWith('--user-data-dir');
+ });
+ if (userDataDirIndex < 0) {
+ isTempUserDataDir = true;
+ chromeArguments.push(
+ `--user-data-dir=${await mkdtemp(this.getProfilePath())}`
+ );
+ userDataDirIndex = chromeArguments.length - 1;
+ }
+
+ const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1];
+ assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed');
+
+ let chromeExecutable = executablePath;
+ if (!chromeExecutable) {
+ assert(
+ channel || !this.puppeteer._isPuppeteerCore,
+ `An \`executablePath\` or \`channel\` must be specified for \`puppeteer-core\``
+ );
+ chromeExecutable = this.executablePath(channel, options.headless ?? true);
+ }
+
+ return {
+ executablePath: chromeExecutable,
+ args: chromeArguments,
+ isTempUserDataDir,
+ userDataDir,
+ };
+ }
+
+ /**
+ * @internal
+ */
+ override async cleanUserDataDir(
+ path: string,
+ opts: {isTemp: boolean}
+ ): Promise<void> {
+ if (opts.isTemp) {
+ try {
+ await rm(path);
+ } catch (error) {
+ debugError(error);
+ throw error;
+ }
+ }
+ }
+
+ override defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] {
+ // See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
+
+ const userDisabledFeatures = getFeatures(
+ '--disable-features',
+ options.args
+ );
+ if (options.args && userDisabledFeatures.length > 0) {
+ removeMatchingFlags(options.args, '--disable-features');
+ }
+
+ // Merge default disabled features with user-provided ones, if any.
+ const disabledFeatures = [
+ 'Translate',
+ // AcceptCHFrame disabled because of crbug.com/1348106.
+ 'AcceptCHFrame',
+ 'MediaRouter',
+ 'OptimizationHints',
+ // https://crbug.com/1492053
+ 'ProcessPerSiteUpToMainFrameThreshold',
+ ...userDisabledFeatures,
+ ];
+
+ const userEnabledFeatures = getFeatures('--enable-features', options.args);
+ if (options.args && userEnabledFeatures.length > 0) {
+ removeMatchingFlags(options.args, '--enable-features');
+ }
+
+ // Merge default enabled features with user-provided ones, if any.
+ const enabledFeatures = [
+ 'NetworkServiceInProcess2',
+ ...userEnabledFeatures,
+ ];
+
+ const chromeArguments = [
+ '--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-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
+ '--disable-hang-monitor',
+ '--disable-infobars',
+ '--disable-ipc-flooding-protection',
+ '--disable-popup-blocking',
+ '--disable-prompt-on-repost',
+ '--disable-renderer-backgrounding',
+ '--disable-search-engine-choice-screen',
+ '--disable-sync',
+ '--enable-automation',
+ '--export-tagged-pdf',
+ '--force-color-profile=srgb',
+ '--metrics-recording-only',
+ '--no-first-run',
+ '--password-store=basic',
+ '--use-mock-keychain',
+ `--disable-features=${disabledFeatures.join(',')}`,
+ `--enable-features=${enabledFeatures.join(',')}`,
+ ];
+ const {
+ devtools = false,
+ headless = !devtools,
+ args = [],
+ userDataDir,
+ } = options;
+ if (userDataDir) {
+ chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`);
+ }
+ if (devtools) {
+ chromeArguments.push('--auto-open-devtools-for-tabs');
+ }
+ if (headless) {
+ chromeArguments.push(
+ headless === 'new' ? '--headless=new' : '--headless',
+ '--hide-scrollbars',
+ '--mute-audio'
+ );
+ }
+ if (
+ args.every(arg => {
+ return arg.startsWith('-');
+ })
+ ) {
+ chromeArguments.push('about:blank');
+ }
+ chromeArguments.push(...args);
+ return chromeArguments;
+ }
+
+ override executablePath(
+ channel?: ChromeReleaseChannel,
+ headless?: boolean | 'new'
+ ): string {
+ if (channel) {
+ return computeSystemExecutablePath({
+ browser: SupportedBrowsers.CHROME,
+ channel: convertPuppeteerChannelToBrowsersChannel(channel),
+ });
+ } else {
+ return this.resolveExecutablePath(headless);
+ }
+ }
+}
+
+function convertPuppeteerChannelToBrowsersChannel(
+ channel: ChromeReleaseChannel
+): BrowsersChromeReleaseChannel {
+ switch (channel) {
+ case 'chrome':
+ return BrowsersChromeReleaseChannel.STABLE;
+ case 'chrome-dev':
+ return BrowsersChromeReleaseChannel.DEV;
+ case 'chrome-beta':
+ return BrowsersChromeReleaseChannel.BETA;
+ case 'chrome-canary':
+ return BrowsersChromeReleaseChannel.CANARY;
+ }
+}
+
+/**
+ * Extracts all features from the given command-line flag
+ * (e.g. `--enable-features`, `--enable-features=`).
+ *
+ * Example input:
+ * ["--enable-features=NetworkService,NetworkServiceInProcess", "--enable-features=Foo"]
+ *
+ * Example output:
+ * ["NetworkService", "NetworkServiceInProcess", "Foo"]
+ *
+ * @internal
+ */
+export function getFeatures(flag: string, options: string[] = []): string[] {
+ return options
+ .filter(s => {
+ return s.startsWith(flag.endsWith('=') ? flag : `${flag}=`);
+ })
+ .map(s => {
+ return s.split(new RegExp(`${flag}=\\s*`))[1]?.trim();
+ })
+ .filter(s => {
+ return s;
+ }) as string[];
+}
+
+/**
+ * Removes all elements in-place from the given string array
+ * that match the given command-line flag.
+ *
+ * @internal
+ */
+export function removeMatchingFlags(array: string[], flag: string): string[] {
+ const regex = new RegExp(`^${flag}=.*`);
+ let i = 0;
+ while (i < array.length) {
+ if (regex.test(array[i]!)) {
+ array.splice(i, 1);
+ } else {
+ i++;
+ }
+ }
+ return array;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.test.ts
new file mode 100644
index 0000000000..b0b1f81249
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.test.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {FirefoxLauncher} from './FirefoxLauncher.js';
+
+describe('FirefoxLauncher', function () {
+ describe('getPreferences', function () {
+ it('should return preferences for CDP', async () => {
+ const prefs: Record<string, unknown> = FirefoxLauncher.getPreferences(
+ {
+ test: 1,
+ },
+ undefined
+ );
+ expect(prefs['test']).toBe(1);
+ expect(prefs['fission.bfcacheInParent']).toBe(false);
+ expect(prefs['fission.webContentIsolationStrategy']).toBe(0);
+ expect(prefs).toEqual(
+ FirefoxLauncher.getPreferences(
+ {
+ test: 1,
+ },
+ 'cdp'
+ )
+ );
+ });
+
+ it('should return preferences for WebDriver BiDi', async () => {
+ const prefs: Record<string, unknown> = FirefoxLauncher.getPreferences(
+ {
+ test: 1,
+ },
+ 'webDriverBiDi'
+ );
+ expect(prefs['test']).toBe(1);
+ expect(prefs['fission.bfcacheInParent']).toBe(undefined);
+ expect(prefs['fission.webContentIsolationStrategy']).toBe(0);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts
new file mode 100644
index 0000000000..eb4f375fc7
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts
@@ -0,0 +1,242 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import {rename, unlink, mkdtemp} from 'fs/promises';
+import os from 'os';
+import path from 'path';
+
+import {
+ Browser as SupportedBrowsers,
+ createProfile,
+ Cache,
+ detectBrowserPlatform,
+ Browser,
+} from '@puppeteer/browsers';
+
+import {debugError} from '../common/util.js';
+import {assert} from '../util/assert.js';
+
+import type {
+ BrowserLaunchArgumentOptions,
+ PuppeteerNodeLaunchOptions,
+} from './LaunchOptions.js';
+import {ProductLauncher, type ResolvedLaunchArgs} from './ProductLauncher.js';
+import type {PuppeteerNode} from './PuppeteerNode.js';
+import {rm} from './util/fs.js';
+
+/**
+ * @internal
+ */
+export class FirefoxLauncher extends ProductLauncher {
+ constructor(puppeteer: PuppeteerNode) {
+ super(puppeteer, 'firefox');
+ }
+
+ static getPreferences(
+ extraPrefsFirefox?: Record<string, unknown>,
+ protocol?: 'cdp' | 'webDriverBiDi'
+ ): Record<string, unknown> {
+ return {
+ ...extraPrefsFirefox,
+ ...(protocol === 'webDriverBiDi'
+ ? {}
+ : {
+ // Do not close the window when the last tab gets closed
+ 'browser.tabs.closeWindowWithLastTab': false,
+ // Temporarily force disable BFCache in parent (https://bit.ly/bug-1732263)
+ 'fission.bfcacheInParent': false,
+ }),
+ // Force all web content to use a single content process. TODO: remove
+ // this once Firefox supports mouse event dispatch from the main frame
+ // context. Once this happens, webContentIsolationStrategy should only
+ // be set for CDP. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1773393
+ 'fission.webContentIsolationStrategy': 0,
+ };
+ }
+
+ /**
+ * @internal
+ */
+ override async computeLaunchArguments(
+ options: PuppeteerNodeLaunchOptions = {}
+ ): Promise<ResolvedLaunchArgs> {
+ const {
+ ignoreDefaultArgs = false,
+ args = [],
+ executablePath,
+ pipe = false,
+ extraPrefsFirefox = {},
+ debuggingPort = null,
+ } = options;
+
+ const firefoxArguments = [];
+ if (!ignoreDefaultArgs) {
+ firefoxArguments.push(...this.defaultArgs(options));
+ } else if (Array.isArray(ignoreDefaultArgs)) {
+ firefoxArguments.push(
+ ...this.defaultArgs(options).filter(arg => {
+ return !ignoreDefaultArgs.includes(arg);
+ })
+ );
+ } else {
+ firefoxArguments.push(...args);
+ }
+
+ if (
+ !firefoxArguments.some(argument => {
+ return argument.startsWith('--remote-debugging-');
+ })
+ ) {
+ if (pipe) {
+ assert(
+ debuggingPort === null,
+ 'Browser should be launched with either pipe or debugging port - not both.'
+ );
+ }
+ firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
+ }
+
+ let userDataDir: string | undefined;
+ let isTempUserDataDir = true;
+
+ // Check for the profile argument, which will always be set even
+ // with a custom directory specified via the userDataDir option.
+ const profileArgIndex = firefoxArguments.findIndex(arg => {
+ return ['-profile', '--profile'].includes(arg);
+ });
+
+ if (profileArgIndex !== -1) {
+ userDataDir = firefoxArguments[profileArgIndex + 1];
+ if (!userDataDir || !fs.existsSync(userDataDir)) {
+ throw new Error(`Firefox profile not found at '${userDataDir}'`);
+ }
+
+ // When using a custom Firefox profile it needs to be populated
+ // with required preferences.
+ isTempUserDataDir = false;
+ } else {
+ userDataDir = await mkdtemp(this.getProfilePath());
+ firefoxArguments.push('--profile');
+ firefoxArguments.push(userDataDir);
+ }
+
+ await createProfile(SupportedBrowsers.FIREFOX, {
+ path: userDataDir,
+ preferences: FirefoxLauncher.getPreferences(
+ extraPrefsFirefox,
+ options.protocol
+ ),
+ });
+
+ let firefoxExecutable: string;
+ if (this.puppeteer._isPuppeteerCore || executablePath) {
+ assert(
+ executablePath,
+ `An \`executablePath\` must be specified for \`puppeteer-core\``
+ );
+ firefoxExecutable = executablePath;
+ } else {
+ firefoxExecutable = this.executablePath();
+ }
+
+ return {
+ isTempUserDataDir,
+ userDataDir,
+ args: firefoxArguments,
+ executablePath: firefoxExecutable,
+ };
+ }
+
+ /**
+ * @internal
+ */
+ override async cleanUserDataDir(
+ userDataDir: string,
+ opts: {isTemp: boolean}
+ ): Promise<void> {
+ if (opts.isTemp) {
+ try {
+ await rm(userDataDir);
+ } catch (error) {
+ debugError(error);
+ throw error;
+ }
+ } else {
+ try {
+ // When an existing user profile has been used remove the user
+ // preferences file and restore possibly backuped preferences.
+ await unlink(path.join(userDataDir, 'user.js'));
+
+ const prefsBackupPath = path.join(userDataDir, 'prefs.js.puppeteer');
+ if (fs.existsSync(prefsBackupPath)) {
+ const prefsPath = path.join(userDataDir, 'prefs.js');
+ await unlink(prefsPath);
+ await rename(prefsBackupPath, prefsPath);
+ }
+ } catch (error) {
+ debugError(error);
+ }
+ }
+ }
+
+ override executablePath(): string {
+ // replace 'latest' placeholder with actual downloaded revision
+ if (this.puppeteer.browserRevision === 'latest') {
+ const cache = new Cache(this.puppeteer.defaultDownloadPath!);
+ const installedFirefox = cache.getInstalledBrowsers().find(browser => {
+ return (
+ browser.platform === detectBrowserPlatform() &&
+ browser.browser === Browser.FIREFOX
+ );
+ });
+ if (installedFirefox) {
+ this.actualBrowserRevision = installedFirefox.buildId;
+ }
+ }
+ return this.resolveExecutablePath();
+ }
+
+ override defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] {
+ const {
+ devtools = false,
+ headless = !devtools,
+ args = [],
+ userDataDir = null,
+ } = options;
+
+ const firefoxArguments = ['--no-remote'];
+
+ switch (os.platform()) {
+ case 'darwin':
+ firefoxArguments.push('--foreground');
+ break;
+ case 'win32':
+ firefoxArguments.push('--wait-for-browser');
+ break;
+ }
+ if (userDataDir) {
+ firefoxArguments.push('--profile');
+ firefoxArguments.push(userDataDir);
+ }
+ if (headless) {
+ firefoxArguments.push('--headless');
+ }
+ if (devtools) {
+ firefoxArguments.push('--devtools');
+ }
+ if (
+ args.every(arg => {
+ return arg.startsWith('-');
+ })
+ ) {
+ firefoxArguments.push('about:blank');
+ }
+ firefoxArguments.push(...args);
+ return firefoxArguments;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts
new file mode 100644
index 0000000000..28e0b595df
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts
@@ -0,0 +1,140 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {BrowserConnectOptions} from '../common/ConnectOptions.js';
+import type {Product} from '../common/Product.js';
+
+/**
+ * Launcher options that only apply to Chrome.
+ *
+ * @public
+ */
+export interface BrowserLaunchArgumentOptions {
+ /**
+ * Whether to run the browser in headless mode.
+ *
+ * @remarks
+ * In the future `headless: true` will be equivalent to `headless: 'new'`.
+ * You can read more about the change {@link https://developer.chrome.com/articles/new-headless/ | here}.
+ * Consider opting in early by setting the value to `"new"`.
+ *
+ * @defaultValue `true`
+ */
+ headless?: boolean | 'new';
+ /**
+ * Path to a user data directory.
+ * {@link https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/user_data_dir.md | see the Chromium docs}
+ * for more info.
+ */
+ userDataDir?: string;
+ /**
+ * Whether to auto-open a DevTools panel for each tab. If this is set to
+ * `true`, then `headless` will be forced to `false`.
+ * @defaultValue `false`
+ */
+ devtools?: boolean;
+ /**
+ * Specify the debugging port number to use
+ */
+ debuggingPort?: number;
+ /**
+ * Additional command line arguments to pass to the browser instance.
+ */
+ args?: string[];
+}
+/**
+ * @public
+ */
+export type ChromeReleaseChannel =
+ | 'chrome'
+ | 'chrome-beta'
+ | 'chrome-canary'
+ | 'chrome-dev';
+
+/**
+ * Generic launch options that can be passed when launching any browser.
+ * @public
+ */
+export interface LaunchOptions {
+ /**
+ * Chrome Release Channel
+ */
+ channel?: ChromeReleaseChannel;
+ /**
+ * Path to a browser executable to use instead of the bundled Chromium. Note
+ * that Puppeteer is only guaranteed to work with the bundled Chromium, so use
+ * this setting at your own risk.
+ */
+ executablePath?: string;
+ /**
+ * If `true`, do not use `puppeteer.defaultArgs()` when creating a browser. If
+ * an array is provided, these args will be filtered out. Use this with care -
+ * you probably want the default arguments Puppeteer uses.
+ * @defaultValue `false`
+ */
+ ignoreDefaultArgs?: boolean | string[];
+ /**
+ * Close the browser process on `Ctrl+C`.
+ * @defaultValue `true`
+ */
+ handleSIGINT?: boolean;
+ /**
+ * Close the browser process on `SIGTERM`.
+ * @defaultValue `true`
+ */
+ handleSIGTERM?: boolean;
+ /**
+ * Close the browser process on `SIGHUP`.
+ * @defaultValue `true`
+ */
+ handleSIGHUP?: boolean;
+ /**
+ * Maximum time in milliseconds to wait for the browser to start.
+ * Pass `0` to disable the timeout.
+ * @defaultValue `30_000` (30 seconds).
+ */
+ timeout?: number;
+ /**
+ * If true, pipes the browser process stdout and stderr to `process.stdout`
+ * and `process.stderr`.
+ * @defaultValue `false`
+ */
+ dumpio?: boolean;
+ /**
+ * Specify environment variables that will be visible to the browser.
+ * @defaultValue The contents of `process.env`.
+ */
+ env?: Record<string, string | undefined>;
+ /**
+ * Connect to a browser over a pipe instead of a WebSocket.
+ * @defaultValue `false`
+ */
+ pipe?: boolean;
+ /**
+ * Which browser to launch.
+ * @defaultValue `chrome`
+ */
+ product?: Product;
+ /**
+ * {@link https://searchfox.org/mozilla-release/source/modules/libpref/init/all.js | Additional preferences } that can be passed when launching with Firefox.
+ */
+ extraPrefsFirefox?: Record<string, unknown>;
+ /**
+ * Whether to wait for the initial page to be ready.
+ * Useful when a user explicitly disables that (e.g. `--no-startup-window` for Chrome).
+ * @defaultValue `true`
+ */
+ waitForInitialPage?: boolean;
+}
+
+/**
+ * Utility type exposed to enable users to define options that can be passed to
+ * `puppeteer.launch` without having to list the set of all types.
+ * @public
+ */
+export type PuppeteerNodeLaunchOptions = BrowserLaunchArgumentOptions &
+ LaunchOptions &
+ BrowserConnectOptions;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/NodeWebSocketTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/NodeWebSocketTransport.ts
new file mode 100644
index 0000000000..f4ac592e4f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/NodeWebSocketTransport.ts
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import NodeWebSocket from 'ws';
+
+import type {ConnectionTransport} from '../common/ConnectionTransport.js';
+import {packageVersion} from '../generated/version.js';
+
+/**
+ * @internal
+ */
+export class NodeWebSocketTransport implements ConnectionTransport {
+ static create(
+ url: string,
+ headers?: Record<string, string>
+ ): Promise<NodeWebSocketTransport> {
+ return new Promise((resolve, reject) => {
+ const ws = new NodeWebSocket(url, [], {
+ followRedirects: true,
+ perMessageDeflate: false,
+ maxPayload: 256 * 1024 * 1024, // 256Mb
+ headers: {
+ 'User-Agent': `Puppeteer ${packageVersion}`,
+ ...headers,
+ },
+ });
+
+ ws.addEventListener('open', () => {
+ return resolve(new NodeWebSocketTransport(ws));
+ });
+ ws.addEventListener('error', reject);
+ });
+ }
+
+ #ws: NodeWebSocket;
+ onmessage?: (message: NodeWebSocket.Data) => void;
+ onclose?: () => void;
+
+ constructor(ws: NodeWebSocket) {
+ this.#ws = ws;
+ this.#ws.addEventListener('message', event => {
+ if (this.onmessage) {
+ this.onmessage.call(null, event.data);
+ }
+ });
+ this.#ws.addEventListener('close', () => {
+ if (this.onclose) {
+ this.onclose.call(null);
+ }
+ });
+ // Silently ignore all errors - we don't know what to do with them.
+ this.#ws.addEventListener('error', () => {});
+ }
+
+ send(message: string): void {
+ this.#ws.send(message);
+ }
+
+ close(): void {
+ this.#ws.close();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/PipeTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/PipeTransport.ts
new file mode 100644
index 0000000000..616f164d82
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/PipeTransport.ts
@@ -0,0 +1,86 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {ConnectionTransport} from '../common/ConnectionTransport.js';
+import {EventSubscription} from '../common/EventEmitter.js';
+import {debugError} from '../common/util.js';
+import {assert} from '../util/assert.js';
+import {DisposableStack} from '../util/disposable.js';
+
+/**
+ * @internal
+ */
+export class PipeTransport implements ConnectionTransport {
+ #pipeWrite: NodeJS.WritableStream;
+ #subscriptions = new DisposableStack();
+
+ #isClosed = false;
+ #pendingMessage = '';
+
+ onclose?: () => void;
+ onmessage?: (value: string) => void;
+
+ constructor(
+ pipeWrite: NodeJS.WritableStream,
+ pipeRead: NodeJS.ReadableStream
+ ) {
+ this.#pipeWrite = pipeWrite;
+ this.#subscriptions.use(
+ new EventSubscription(pipeRead, 'data', (buffer: Buffer) => {
+ return this.#dispatch(buffer);
+ })
+ );
+ this.#subscriptions.use(
+ new EventSubscription(pipeRead, 'close', () => {
+ if (this.onclose) {
+ this.onclose.call(null);
+ }
+ })
+ );
+ this.#subscriptions.use(
+ new EventSubscription(pipeRead, 'error', debugError)
+ );
+ this.#subscriptions.use(
+ new EventSubscription(pipeWrite, 'error', debugError)
+ );
+ }
+
+ send(message: string): void {
+ assert(!this.#isClosed, '`PipeTransport` is closed.');
+
+ this.#pipeWrite.write(message);
+ this.#pipeWrite.write('\0');
+ }
+
+ #dispatch(buffer: Buffer): void {
+ assert(!this.#isClosed, '`PipeTransport` is closed.');
+
+ let end = buffer.indexOf('\0');
+ if (end === -1) {
+ this.#pendingMessage += buffer.toString();
+ return;
+ }
+ const message = this.#pendingMessage + buffer.toString(undefined, 0, end);
+ if (this.onmessage) {
+ this.onmessage.call(null, message);
+ }
+
+ let start = end + 1;
+ end = buffer.indexOf('\0', start);
+ while (end !== -1) {
+ if (this.onmessage) {
+ this.onmessage.call(null, buffer.toString(undefined, start, end));
+ }
+ start = end + 1;
+ end = buffer.indexOf('\0', start);
+ }
+ this.#pendingMessage = buffer.toString(undefined, start);
+ }
+
+ close(): void {
+ this.#isClosed = true;
+ this.#subscriptions.dispose();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts
new file mode 100644
index 0000000000..ab3432cd3a
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts
@@ -0,0 +1,451 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {existsSync} from 'fs';
+import {tmpdir} from 'os';
+import {join} from 'path';
+
+import {
+ Browser as InstalledBrowser,
+ CDP_WEBSOCKET_ENDPOINT_REGEX,
+ launch,
+ TimeoutError as BrowsersTimeoutError,
+ WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
+ computeExecutablePath,
+} from '@puppeteer/browsers';
+
+import {
+ firstValueFrom,
+ from,
+ map,
+ race,
+ timer,
+} from '../../third_party/rxjs/rxjs.js';
+import type {Browser, BrowserCloseCallback} from '../api/Browser.js';
+import {CdpBrowser} from '../cdp/Browser.js';
+import {Connection} from '../cdp/Connection.js';
+import {TimeoutError} from '../common/Errors.js';
+import type {Product} from '../common/Product.js';
+import {debugError, DEFAULT_VIEWPORT} from '../common/util.js';
+import type {Viewport} from '../common/Viewport.js';
+
+import type {
+ BrowserLaunchArgumentOptions,
+ ChromeReleaseChannel,
+ PuppeteerNodeLaunchOptions,
+} from './LaunchOptions.js';
+import {NodeWebSocketTransport as WebSocketTransport} from './NodeWebSocketTransport.js';
+import {PipeTransport} from './PipeTransport.js';
+import type {PuppeteerNode} from './PuppeteerNode.js';
+
+/**
+ * @internal
+ */
+export interface ResolvedLaunchArgs {
+ isTempUserDataDir: boolean;
+ userDataDir: string;
+ executablePath: string;
+ args: string[];
+}
+
+/**
+ * Describes a launcher - a class that is able to create and launch a browser instance.
+ *
+ * @public
+ */
+export abstract class ProductLauncher {
+ #product: Product;
+
+ /**
+ * @internal
+ */
+ puppeteer: PuppeteerNode;
+
+ /**
+ * @internal
+ */
+ protected actualBrowserRevision?: string;
+
+ /**
+ * @internal
+ */
+ constructor(puppeteer: PuppeteerNode, product: Product) {
+ this.puppeteer = puppeteer;
+ this.#product = product;
+ }
+
+ get product(): Product {
+ return this.#product;
+ }
+
+ async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> {
+ const {
+ dumpio = false,
+ env = process.env,
+ handleSIGINT = true,
+ handleSIGTERM = true,
+ handleSIGHUP = true,
+ ignoreHTTPSErrors = false,
+ defaultViewport = DEFAULT_VIEWPORT,
+ slowMo = 0,
+ timeout = 30000,
+ waitForInitialPage = true,
+ protocolTimeout,
+ protocol,
+ } = options;
+
+ const launchArgs = await this.computeLaunchArguments(options);
+
+ const usePipe = launchArgs.args.includes('--remote-debugging-pipe');
+
+ const onProcessExit = async () => {
+ await this.cleanUserDataDir(launchArgs.userDataDir, {
+ isTemp: launchArgs.isTempUserDataDir,
+ });
+ };
+
+ const browserProcess = launch({
+ executablePath: launchArgs.executablePath,
+ args: launchArgs.args,
+ handleSIGHUP,
+ handleSIGTERM,
+ handleSIGINT,
+ dumpio,
+ env,
+ pipe: usePipe,
+ onExit: onProcessExit,
+ });
+
+ let browser: Browser;
+ let cdpConnection: Connection;
+ let closing = false;
+
+ const browserCloseCallback: BrowserCloseCallback = async () => {
+ if (closing) {
+ return;
+ }
+ closing = true;
+ await this.closeBrowser(browserProcess, cdpConnection);
+ };
+
+ try {
+ if (this.#product === 'firefox' && protocol === 'webDriverBiDi') {
+ browser = await this.createBiDiBrowser(
+ browserProcess,
+ browserCloseCallback,
+ {
+ timeout,
+ protocolTimeout,
+ slowMo,
+ defaultViewport,
+ ignoreHTTPSErrors,
+ }
+ );
+ } else {
+ if (usePipe) {
+ cdpConnection = await this.createCdpPipeConnection(browserProcess, {
+ timeout,
+ protocolTimeout,
+ slowMo,
+ });
+ } else {
+ cdpConnection = await this.createCdpSocketConnection(browserProcess, {
+ timeout,
+ protocolTimeout,
+ slowMo,
+ });
+ }
+ if (protocol === 'webDriverBiDi') {
+ browser = await this.createBiDiOverCdpBrowser(
+ browserProcess,
+ cdpConnection,
+ browserCloseCallback,
+ {
+ timeout,
+ protocolTimeout,
+ slowMo,
+ defaultViewport,
+ ignoreHTTPSErrors,
+ }
+ );
+ } else {
+ browser = await CdpBrowser._create(
+ this.product,
+ cdpConnection,
+ [],
+ ignoreHTTPSErrors,
+ defaultViewport,
+ browserProcess.nodeProcess,
+ browserCloseCallback,
+ options.targetFilter
+ );
+ }
+ }
+ } catch (error) {
+ void browserCloseCallback();
+ if (error instanceof BrowsersTimeoutError) {
+ throw new TimeoutError(error.message);
+ }
+ throw error;
+ }
+
+ if (waitForInitialPage && protocol !== 'webDriverBiDi') {
+ await this.waitForPageTarget(browser, timeout);
+ }
+
+ return browser;
+ }
+
+ abstract executablePath(channel?: ChromeReleaseChannel): string;
+
+ abstract defaultArgs(object: BrowserLaunchArgumentOptions): string[];
+
+ /**
+ * Set only for Firefox, after the launcher resolves the `latest` revision to
+ * the actual revision.
+ * @internal
+ */
+ getActualBrowserRevision(): string | undefined {
+ return this.actualBrowserRevision;
+ }
+
+ /**
+ * @internal
+ */
+ protected abstract computeLaunchArguments(
+ options: PuppeteerNodeLaunchOptions
+ ): Promise<ResolvedLaunchArgs>;
+
+ /**
+ * @internal
+ */
+ protected abstract cleanUserDataDir(
+ path: string,
+ opts: {isTemp: boolean}
+ ): Promise<void>;
+
+ /**
+ * @internal
+ */
+ protected async closeBrowser(
+ browserProcess: ReturnType<typeof launch>,
+ cdpConnection?: Connection
+ ): Promise<void> {
+ if (cdpConnection) {
+ // Attempt to close the browser gracefully
+ try {
+ await cdpConnection.closeBrowser();
+ await browserProcess.hasClosed();
+ } catch (error) {
+ debugError(error);
+ await browserProcess.close();
+ }
+ } else {
+ // Wait for a possible graceful shutdown.
+ await firstValueFrom(
+ race(
+ from(browserProcess.hasClosed()),
+ timer(5000).pipe(
+ map(() => {
+ return from(browserProcess.close());
+ })
+ )
+ )
+ );
+ }
+ }
+
+ /**
+ * @internal
+ */
+ protected async waitForPageTarget(
+ browser: Browser,
+ timeout: number
+ ): Promise<void> {
+ try {
+ await browser.waitForTarget(
+ t => {
+ return t.type() === 'page';
+ },
+ {timeout}
+ );
+ } catch (error) {
+ await browser.close();
+ throw error;
+ }
+ }
+
+ /**
+ * @internal
+ */
+ protected async createCdpSocketConnection(
+ browserProcess: ReturnType<typeof launch>,
+ opts: {timeout: number; protocolTimeout: number | undefined; slowMo: number}
+ ): Promise<Connection> {
+ const browserWSEndpoint = await browserProcess.waitForLineOutput(
+ CDP_WEBSOCKET_ENDPOINT_REGEX,
+ opts.timeout
+ );
+ const transport = await WebSocketTransport.create(browserWSEndpoint);
+ return new Connection(
+ browserWSEndpoint,
+ transport,
+ opts.slowMo,
+ opts.protocolTimeout
+ );
+ }
+
+ /**
+ * @internal
+ */
+ protected async createCdpPipeConnection(
+ browserProcess: ReturnType<typeof launch>,
+ opts: {timeout: number; protocolTimeout: number | undefined; slowMo: number}
+ ): Promise<Connection> {
+ // stdio was assigned during start(), and the 'pipe' option there adds the
+ // 4th and 5th items to stdio array
+ const {3: pipeWrite, 4: pipeRead} = browserProcess.nodeProcess.stdio;
+ const transport = new PipeTransport(
+ pipeWrite as NodeJS.WritableStream,
+ pipeRead as NodeJS.ReadableStream
+ );
+ return new Connection('', transport, opts.slowMo, opts.protocolTimeout);
+ }
+
+ /**
+ * @internal
+ */
+ protected async createBiDiOverCdpBrowser(
+ browserProcess: ReturnType<typeof launch>,
+ connection: Connection,
+ closeCallback: BrowserCloseCallback,
+ opts: {
+ timeout: number;
+ protocolTimeout: number | undefined;
+ slowMo: number;
+ defaultViewport: Viewport | null;
+ ignoreHTTPSErrors?: boolean;
+ }
+ ): Promise<Browser> {
+ // TODO: use other options too.
+ const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
+ const bidiConnection = await BiDi.connectBidiOverCdp(connection, {
+ acceptInsecureCerts: opts.ignoreHTTPSErrors ?? false,
+ });
+ return await BiDi.BidiBrowser.create({
+ connection: bidiConnection,
+ closeCallback,
+ process: browserProcess.nodeProcess,
+ defaultViewport: opts.defaultViewport,
+ ignoreHTTPSErrors: opts.ignoreHTTPSErrors,
+ });
+ }
+
+ /**
+ * @internal
+ */
+ protected async createBiDiBrowser(
+ browserProcess: ReturnType<typeof launch>,
+ closeCallback: BrowserCloseCallback,
+ opts: {
+ timeout: number;
+ protocolTimeout: number | undefined;
+ slowMo: number;
+ defaultViewport: Viewport | null;
+ ignoreHTTPSErrors?: boolean;
+ }
+ ): Promise<Browser> {
+ const browserWSEndpoint =
+ (await browserProcess.waitForLineOutput(
+ WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
+ opts.timeout
+ )) + '/session';
+ const transport = await WebSocketTransport.create(browserWSEndpoint);
+ const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
+ const bidiConnection = new BiDi.BidiConnection(
+ browserWSEndpoint,
+ transport,
+ opts.slowMo,
+ opts.protocolTimeout
+ );
+ // TODO: use other options too.
+ return await BiDi.BidiBrowser.create({
+ connection: bidiConnection,
+ closeCallback,
+ process: browserProcess.nodeProcess,
+ defaultViewport: opts.defaultViewport,
+ ignoreHTTPSErrors: opts.ignoreHTTPSErrors,
+ });
+ }
+
+ /**
+ * @internal
+ */
+ protected getProfilePath(): string {
+ return join(
+ this.puppeteer.configuration.temporaryDirectory ?? tmpdir(),
+ `puppeteer_dev_${this.product}_profile-`
+ );
+ }
+
+ /**
+ * @internal
+ */
+ protected resolveExecutablePath(headless?: boolean | 'new'): string {
+ let executablePath = this.puppeteer.configuration.executablePath;
+ if (executablePath) {
+ if (!existsSync(executablePath)) {
+ throw new Error(
+ `Tried to find the browser at the configured path (${executablePath}), but no executable was found.`
+ );
+ }
+ return executablePath;
+ }
+
+ function productToBrowser(product?: Product, headless?: boolean | 'new') {
+ switch (product) {
+ case 'chrome':
+ if (headless === true) {
+ return InstalledBrowser.CHROMEHEADLESSSHELL;
+ }
+ return InstalledBrowser.CHROME;
+ case 'firefox':
+ return InstalledBrowser.FIREFOX;
+ }
+ return InstalledBrowser.CHROME;
+ }
+
+ executablePath = computeExecutablePath({
+ cacheDir: this.puppeteer.defaultDownloadPath!,
+ browser: productToBrowser(this.product, headless),
+ buildId: this.puppeteer.browserRevision,
+ });
+
+ if (!existsSync(executablePath)) {
+ if (this.puppeteer.configuration.browserRevision) {
+ throw new Error(
+ `Tried to find the browser at the configured path (${executablePath}) for revision ${this.puppeteer.browserRevision}, but no executable was found.`
+ );
+ }
+ switch (this.product) {
+ case 'chrome':
+ throw new Error(
+ `Could not find Chrome (ver. ${this.puppeteer.browserRevision}). This can occur if either\n` +
+ ' 1. you did not perform an installation before running the script (e.g. `npx puppeteer browsers install chrome`) or\n' +
+ ` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
+ 'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.'
+ );
+ case 'firefox':
+ throw new Error(
+ `Could not find Firefox (rev. ${this.puppeteer.browserRevision}). This can occur if either\n` +
+ ' 1. you did not perform an installation for Firefox before running the script (e.g. `npx puppeteer browsers install firefox`) or\n' +
+ ` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
+ 'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.'
+ );
+ }
+ }
+ return executablePath;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts
new file mode 100644
index 0000000000..e50e09acdb
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts
@@ -0,0 +1,356 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ Browser as SupportedBrowser,
+ resolveBuildId,
+ detectBrowserPlatform,
+ getInstalledBrowsers,
+ uninstall,
+} from '@puppeteer/browsers';
+
+import type {Browser} from '../api/Browser.js';
+import type {Configuration} from '../common/Configuration.js';
+import type {
+ ConnectOptions,
+ BrowserConnectOptions,
+} from '../common/ConnectOptions.js';
+import type {Product} from '../common/Product.js';
+import {type CommonPuppeteerSettings, Puppeteer} from '../common/Puppeteer.js';
+import {PUPPETEER_REVISIONS} from '../revisions.js';
+
+import {ChromeLauncher} from './ChromeLauncher.js';
+import {FirefoxLauncher} from './FirefoxLauncher.js';
+import type {
+ BrowserLaunchArgumentOptions,
+ ChromeReleaseChannel,
+ LaunchOptions,
+} from './LaunchOptions.js';
+import type {ProductLauncher} from './ProductLauncher.js';
+
+/**
+ * @public
+ */
+export interface PuppeteerLaunchOptions
+ extends LaunchOptions,
+ BrowserLaunchArgumentOptions,
+ BrowserConnectOptions {
+ product?: Product;
+ extraPrefsFirefox?: Record<string, unknown>;
+}
+
+/**
+ * Extends the main {@link Puppeteer} class with Node specific behaviour for
+ * fetching and downloading browsers.
+ *
+ * If you're using Puppeteer in a Node environment, this is the class you'll get
+ * when you run `require('puppeteer')` (or the equivalent ES `import`).
+ *
+ * @remarks
+ * The most common method to use is {@link PuppeteerNode.launch | launch}, which
+ * is used to launch and connect to a new browser instance.
+ *
+ * See {@link Puppeteer | the main Puppeteer class} for methods common to all
+ * environments, such as {@link Puppeteer.connect}.
+ *
+ * @example
+ * The following is a typical example of using Puppeteer to drive automation:
+ *
+ * ```ts
+ * import puppeteer from 'puppeteer';
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.goto('https://www.google.com');
+ * // other actions...
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * Once you have created a `page` you have access to a large API to interact
+ * with the page, navigate, or find certain elements in that page.
+ * The {@link Page | `page` documentation} lists all the available methods.
+ *
+ * @public
+ */
+export class PuppeteerNode extends Puppeteer {
+ #_launcher?: ProductLauncher;
+ #lastLaunchedProduct?: Product;
+
+ /**
+ * @internal
+ */
+ defaultBrowserRevision: string;
+
+ /**
+ * @internal
+ */
+ configuration: Configuration = {};
+
+ /**
+ * @internal
+ */
+ constructor(
+ settings: {
+ configuration?: Configuration;
+ } & CommonPuppeteerSettings
+ ) {
+ const {configuration, ...commonSettings} = settings;
+ super(commonSettings);
+ if (configuration) {
+ this.configuration = configuration;
+ }
+ switch (this.configuration.defaultProduct) {
+ case 'firefox':
+ this.defaultBrowserRevision = PUPPETEER_REVISIONS.firefox;
+ break;
+ default:
+ this.configuration.defaultProduct = 'chrome';
+ this.defaultBrowserRevision = PUPPETEER_REVISIONS.chrome;
+ break;
+ }
+
+ this.connect = this.connect.bind(this);
+ this.launch = this.launch.bind(this);
+ this.executablePath = this.executablePath.bind(this);
+ this.defaultArgs = this.defaultArgs.bind(this);
+ this.trimCache = this.trimCache.bind(this);
+ }
+
+ /**
+ * This method attaches Puppeteer to an existing browser instance.
+ *
+ * @param options - Set of configurable options to set on the browser.
+ * @returns Promise which resolves to browser instance.
+ */
+ override connect(options: ConnectOptions): Promise<Browser> {
+ return super.connect(options);
+ }
+
+ /**
+ * Launches a browser instance with given arguments and options when
+ * specified.
+ *
+ * When using with `puppeteer-core`,
+ * {@link LaunchOptions | options.executablePath} or
+ * {@link LaunchOptions | options.channel} must be provided.
+ *
+ * @example
+ * You can use {@link LaunchOptions | options.ignoreDefaultArgs}
+ * to filter out `--mute-audio` from default arguments:
+ *
+ * ```ts
+ * const browser = await puppeteer.launch({
+ * ignoreDefaultArgs: ['--mute-audio'],
+ * });
+ * ```
+ *
+ * @remarks
+ * Puppeteer can also be used to control the Chrome browser, but it works best
+ * with the version of Chrome for Testing downloaded by default.
+ * There is no guarantee it will work with any other version. If Google Chrome
+ * (rather than Chrome for Testing) is preferred, a
+ * {@link https://www.google.com/chrome/browser/canary.html | Chrome Canary}
+ * or
+ * {@link https://www.chromium.org/getting-involved/dev-channel | Dev Channel}
+ * build is suggested. See
+ * {@link https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/ | this article}
+ * for a description of the differences between Chromium and Chrome.
+ * {@link https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md | This article}
+ * describes some differences for Linux users. See
+ * {@link https://developer.chrome.com/blog/chrome-for-testing/ | this doc} for the description
+ * of Chrome for Testing.
+ *
+ * @param options - Options to configure launching behavior.
+ */
+ launch(options: PuppeteerLaunchOptions = {}): Promise<Browser> {
+ const {product = this.defaultProduct} = options;
+ this.#lastLaunchedProduct = product;
+ return this.#launcher.launch(options);
+ }
+
+ /**
+ * @internal
+ */
+ get #launcher(): ProductLauncher {
+ if (
+ this.#_launcher &&
+ this.#_launcher.product === this.lastLaunchedProduct
+ ) {
+ return this.#_launcher;
+ }
+ switch (this.lastLaunchedProduct) {
+ case 'chrome':
+ this.defaultBrowserRevision = PUPPETEER_REVISIONS.chrome;
+ this.#_launcher = new ChromeLauncher(this);
+ break;
+ case 'firefox':
+ this.defaultBrowserRevision = PUPPETEER_REVISIONS.firefox;
+ this.#_launcher = new FirefoxLauncher(this);
+ break;
+ default:
+ throw new Error(`Unknown product: ${this.#lastLaunchedProduct}`);
+ }
+ return this.#_launcher;
+ }
+
+ /**
+ * The default executable path.
+ */
+ executablePath(channel?: ChromeReleaseChannel): string {
+ return this.#launcher.executablePath(channel);
+ }
+
+ /**
+ * @internal
+ */
+ get browserRevision(): string {
+ return (
+ this.#_launcher?.getActualBrowserRevision() ??
+ this.configuration.browserRevision ??
+ this.defaultBrowserRevision!
+ );
+ }
+
+ /**
+ * The default download path for puppeteer. For puppeteer-core, this
+ * code should never be called as it is never defined.
+ *
+ * @internal
+ */
+ get defaultDownloadPath(): string | undefined {
+ return this.configuration.downloadPath ?? this.configuration.cacheDirectory;
+ }
+
+ /**
+ * The name of the browser that was last launched.
+ */
+ get lastLaunchedProduct(): Product {
+ return this.#lastLaunchedProduct ?? this.defaultProduct;
+ }
+
+ /**
+ * The name of the browser that will be launched by default. For
+ * `puppeteer`, this is influenced by your configuration. Otherwise, it's
+ * `chrome`.
+ */
+ get defaultProduct(): Product {
+ return this.configuration.defaultProduct ?? 'chrome';
+ }
+
+ /**
+ * @deprecated Do not use as this field as it does not take into account
+ * multiple browsers of different types. Use
+ * {@link PuppeteerNode.defaultProduct | defaultProduct} or
+ * {@link PuppeteerNode.lastLaunchedProduct | lastLaunchedProduct}.
+ *
+ * @returns The name of the browser that is under automation.
+ */
+ get product(): string {
+ return this.#launcher.product;
+ }
+
+ /**
+ * @param options - Set of configurable options to set on the browser.
+ *
+ * @returns The default flags that Chromium will be launched with.
+ */
+ defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] {
+ return this.#launcher.defaultArgs(options);
+ }
+
+ /**
+ * Removes all non-current Firefox and Chrome binaries in the cache directory
+ * identified by the provided Puppeteer configuration. The current browser
+ * version is determined by resolving PUPPETEER_REVISIONS from Puppeteer
+ * unless `configuration.browserRevision` is provided.
+ *
+ * @remarks
+ *
+ * Note that the method does not check if any other Puppeteer versions
+ * installed on the host that use the same cache directory require the
+ * non-current binaries.
+ *
+ * @public
+ */
+ async trimCache(): Promise<void> {
+ const platform = detectBrowserPlatform();
+ if (!platform) {
+ throw new Error('The current platform is not supported.');
+ }
+
+ const cacheDir =
+ this.configuration.downloadPath ?? this.configuration.cacheDirectory!;
+ const installedBrowsers = await getInstalledBrowsers({
+ cacheDir,
+ });
+
+ const product = this.configuration.defaultProduct!;
+
+ const puppeteerBrowsers: Array<{
+ product: Product;
+ browser: SupportedBrowser;
+ currentBuildId: string;
+ }> = [
+ {
+ product: 'chrome',
+ browser: SupportedBrowser.CHROME,
+ currentBuildId: '',
+ },
+ {
+ product: 'firefox',
+ browser: SupportedBrowser.FIREFOX,
+ currentBuildId: '',
+ },
+ ];
+
+ // Resolve current buildIds.
+ for (const item of puppeteerBrowsers) {
+ item.currentBuildId = await resolveBuildId(
+ item.browser,
+ platform,
+ (product === item.product
+ ? this.configuration.browserRevision
+ : null) || PUPPETEER_REVISIONS[item.product]
+ );
+ }
+
+ const currentBrowserBuilds = new Set(
+ puppeteerBrowsers.map(browser => {
+ return `${browser.browser}_${browser.currentBuildId}`;
+ })
+ );
+
+ const currentBrowsers = new Set(
+ puppeteerBrowsers.map(browser => {
+ return browser.browser;
+ })
+ );
+
+ for (const installedBrowser of installedBrowsers) {
+ // Don't uninstall browsers that are not managed by Puppeteer yet.
+ if (!currentBrowsers.has(installedBrowser.browser)) {
+ continue;
+ }
+ // Keep the browser build used by the current Puppeteer installation.
+ if (
+ currentBrowserBuilds.has(
+ `${installedBrowser.browser}_${installedBrowser.buildId}`
+ )
+ ) {
+ continue;
+ }
+
+ await uninstall({
+ browser: installedBrowser.browser,
+ platform,
+ cacheDir,
+ buildId: installedBrowser.buildId,
+ });
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/ScreenRecorder.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/ScreenRecorder.ts
new file mode 100644
index 0000000000..effb2d63ba
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/ScreenRecorder.ts
@@ -0,0 +1,255 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ChildProcessWithoutNullStreams} from 'child_process';
+import {spawn, spawnSync} from 'child_process';
+import {PassThrough} from 'stream';
+
+import debug from 'debug';
+
+import type {OperatorFunction} from '../../third_party/rxjs/rxjs.js';
+import {
+ bufferCount,
+ concatMap,
+ filter,
+ from,
+ fromEvent,
+ lastValueFrom,
+ map,
+ takeUntil,
+ tap,
+} from '../../third_party/rxjs/rxjs.js';
+import {CDPSessionEvent} from '../api/CDPSession.js';
+import type {BoundingBox} from '../api/ElementHandle.js';
+import type {Page} from '../api/Page.js';
+import {debugError, fromEmitterEvent} from '../common/util.js';
+import {guarded} from '../util/decorators.js';
+import {asyncDisposeSymbol} from '../util/disposable.js';
+
+const CRF_VALUE = 30;
+const DEFAULT_FPS = 30;
+
+const debugFfmpeg = debug('puppeteer:ffmpeg');
+
+/**
+ * @internal
+ */
+export interface ScreenRecorderOptions {
+ speed?: number;
+ crop?: BoundingBox;
+ format?: 'gif' | 'webm';
+ scale?: number;
+ path?: string;
+}
+
+/**
+ * @public
+ */
+export class ScreenRecorder extends PassThrough {
+ #page: Page;
+
+ #process: ChildProcessWithoutNullStreams;
+
+ #controller = new AbortController();
+ #lastFrame: Promise<readonly [Buffer, number]>;
+
+ /**
+ * @internal
+ */
+ constructor(
+ page: Page,
+ width: number,
+ height: number,
+ {speed, scale, crop, format, path}: ScreenRecorderOptions = {}
+ ) {
+ super({allowHalfOpen: false});
+
+ path ??= 'ffmpeg';
+
+ // Tests if `ffmpeg` exists.
+ const {error} = spawnSync(path);
+ if (error) {
+ throw error;
+ }
+
+ this.#process = spawn(
+ path,
+ // See https://trac.ffmpeg.org/wiki/Encode/VP9 for more information on flags.
+ [
+ ['-loglevel', 'error'],
+ // Reduces general buffering.
+ ['-avioflags', 'direct'],
+ // Reduces initial buffering while analyzing input fps and other stats.
+ [
+ '-fpsprobesize',
+ '0',
+ '-probesize',
+ '32',
+ '-analyzeduration',
+ '0',
+ '-fflags',
+ 'nobuffer',
+ ],
+ // Forces input to be read from standard input, and forces png input
+ // image format.
+ ['-f', 'image2pipe', '-c:v', 'png', '-i', 'pipe:0'],
+ // Overwrite output and no audio.
+ ['-y', '-an'],
+ // This drastically reduces stalling when cpu is overbooked. By default
+ // VP9 tries to use all available threads?
+ ['-threads', '1'],
+ // Specifies the frame rate we are giving ffmpeg.
+ ['-framerate', `${DEFAULT_FPS}`],
+ // Specifies the encoding and format we are using.
+ this.#getFormatArgs(format ?? 'webm'),
+ // Disable bitrate.
+ ['-b:v', '0'],
+ // Filters to ensure the images are piped correctly.
+ [
+ '-vf',
+ `${
+ speed ? `setpts=${1 / speed}*PTS,` : ''
+ }crop='min(${width},iw):min(${height},ih):0:0',pad=${width}:${height}:0:0${
+ crop ? `,crop=${crop.width}:${crop.height}:${crop.x}:${crop.y}` : ''
+ }${scale ? `,scale=iw*${scale}:-1` : ''}`,
+ ],
+ 'pipe:1',
+ ].flat(),
+ {stdio: ['pipe', 'pipe', 'pipe']}
+ );
+ this.#process.stdout.pipe(this);
+ this.#process.stderr.on('data', (data: Buffer) => {
+ debugFfmpeg(data.toString('utf8'));
+ });
+
+ this.#page = page;
+
+ const {client} = this.#page.mainFrame();
+ client.once(CDPSessionEvent.Disconnected, () => {
+ void this.stop().catch(debugError);
+ });
+
+ this.#lastFrame = lastValueFrom(
+ fromEmitterEvent(client, 'Page.screencastFrame').pipe(
+ tap(event => {
+ void client.send('Page.screencastFrameAck', {
+ sessionId: event.sessionId,
+ });
+ }),
+ filter(event => {
+ return event.metadata.timestamp !== undefined;
+ }),
+ map(event => {
+ return {
+ buffer: Buffer.from(event.data, 'base64'),
+ timestamp: event.metadata.timestamp!,
+ };
+ }),
+ bufferCount(2, 1) as OperatorFunction<
+ {buffer: Buffer; timestamp: number},
+ [
+ {buffer: Buffer; timestamp: number},
+ {buffer: Buffer; timestamp: number},
+ ]
+ >,
+ concatMap(([{timestamp: previousTimestamp, buffer}, {timestamp}]) => {
+ return from(
+ Array<Buffer>(
+ Math.round(
+ DEFAULT_FPS * Math.max(timestamp - previousTimestamp, 0)
+ )
+ ).fill(buffer)
+ );
+ }),
+ map(buffer => {
+ void this.#writeFrame(buffer);
+ return [buffer, performance.now()] as const;
+ }),
+ takeUntil(fromEvent(this.#controller.signal, 'abort'))
+ ),
+ {defaultValue: [Buffer.from([]), performance.now()] as const}
+ );
+ }
+
+ #getFormatArgs(format: 'webm' | 'gif') {
+ switch (format) {
+ case 'webm':
+ return [
+ // Sets the codec to use.
+ ['-c:v', 'vp9'],
+ // Sets the format
+ ['-f', 'webm'],
+ // Sets the quality. Lower the better.
+ ['-crf', `${CRF_VALUE}`],
+ // Sets the quality and how efficient the compression will be.
+ ['-deadline', 'realtime', '-cpu-used', '8'],
+ ].flat();
+ case 'gif':
+ return [
+ // Sets the frame rate and uses a custom palette generated from the
+ // input.
+ [
+ '-vf',
+ 'fps=5,split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse',
+ ],
+ // Sets the format
+ ['-f', 'gif'],
+ ].flat();
+ }
+ }
+
+ @guarded()
+ async #writeFrame(buffer: Buffer) {
+ const error = await new Promise<Error | null | undefined>(resolve => {
+ this.#process.stdin.write(buffer, resolve);
+ });
+ if (error) {
+ console.log(`ffmpeg failed to write: ${error.message}.`);
+ }
+ }
+
+ /**
+ * Stops the recorder.
+ *
+ * @public
+ */
+ @guarded()
+ async stop(): Promise<void> {
+ if (this.#controller.signal.aborted) {
+ return;
+ }
+ // Stopping the screencast will flush the frames.
+ await this.#page._stopScreencast().catch(debugError);
+
+ this.#controller.abort();
+
+ // Repeat the last frame for the remaining frames.
+ const [buffer, timestamp] = await this.#lastFrame;
+ await Promise.all(
+ Array<Buffer>(
+ Math.max(
+ 1,
+ Math.round((DEFAULT_FPS * (performance.now() - timestamp)) / 1000)
+ )
+ )
+ .fill(buffer)
+ .map(this.#writeFrame.bind(this))
+ );
+
+ // Close stdin to notify FFmpeg we are done.
+ this.#process.stdin.end();
+ await new Promise(resolve => {
+ this.#process.once('close', resolve);
+ });
+ }
+
+ /**
+ * @internal
+ */
+ async [asyncDisposeSymbol](): Promise<void> {
+ await this.stop();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/node.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/node.ts
new file mode 100644
index 0000000000..373449ec0f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/node.ts
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './ChromeLauncher.js';
+export * from './FirefoxLauncher.js';
+export * from './LaunchOptions.js';
+export * from './PipeTransport.js';
+export * from './ProductLauncher.js';
+export * from './PuppeteerNode.js';
+export * from './ScreenRecorder.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/util/fs.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/util/fs.ts
new file mode 100644
index 0000000000..d18c76d6dc
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/util/fs.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+
+const rmOptions = {
+ force: true,
+ recursive: true,
+ maxRetries: 5,
+};
+
+/**
+ * @internal
+ */
+export async function rm(path: string): Promise<void> {
+ await fs.promises.rm(path, rmOptions);
+}
+
+/**
+ * @internal
+ */
+export function rmSync(path: string): void {
+ fs.rmSync(path, rmOptions);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/puppeteer-core.ts b/remote/test/puppeteer/packages/puppeteer-core/src/puppeteer-core.ts
new file mode 100644
index 0000000000..d19162b4a3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/puppeteer-core.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export type {Protocol} from 'devtools-protocol';
+
+export * from './api/api.js';
+export * from './cdp/cdp.js';
+export * from './common/common.js';
+export * from './node/node.js';
+export * from './revisions.js';
+export * from './util/util.js';
+
+/**
+ * @deprecated Use the query handler API defined on {@link Puppeteer}
+ */
+export * from './common/CustomQueryHandler.js';
+
+import {PuppeteerNode} from './node/PuppeteerNode.js';
+
+/**
+ * @public
+ */
+const puppeteer = new PuppeteerNode({
+ isPuppeteerCore: true,
+});
+
+export const {
+ /**
+ * @public
+ */
+ connect,
+ /**
+ * @public
+ */
+ defaultArgs,
+ /**
+ * @public
+ */
+ executablePath,
+ /**
+ * @public
+ */
+ launch,
+} = puppeteer;
+
+export default puppeteer;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts
new file mode 100644
index 0000000000..37360204d8
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts
@@ -0,0 +1,14 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export const PUPPETEER_REVISIONS = Object.freeze({
+ chrome: '121.0.6167.85',
+ 'chrome-headless-shell': '121.0.6167.85',
+ firefox: 'latest',
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/templates/injected.ts.tmpl b/remote/test/puppeteer/packages/puppeteer-core/src/templates/injected.ts.tmpl
new file mode 100644
index 0000000000..aa799e9fdb
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/templates/injected.ts.tmpl
@@ -0,0 +1,8 @@
+/**
+ * JavaScript code that provides the puppeteer utilities. See the
+ * [README](https://github.com/puppeteer/puppeteer/blob/main/src/injected/README.md)
+ * for injection for more information.
+ *
+ * @internal
+ */
+export const source = SOURCE_CODE;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/templates/version.ts.tmpl b/remote/test/puppeteer/packages/puppeteer-core/src/templates/version.ts.tmpl
new file mode 100644
index 0000000000..73b984d2ff
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/templates/version.ts.tmpl
@@ -0,0 +1,4 @@
+/**
+ * @internal
+ */
+export const packageVersion = 'PACKAGE_VERSION';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.cjs.json b/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.cjs.json
new file mode 100644
index 0000000000..897b1a03df
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.cjs.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "CommonJS",
+ "moduleResolution": "Node",
+ "outDir": "../lib/cjs/puppeteer"
+ },
+ "references": [{"path": "../third_party/tsconfig.cjs.json"}]
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.esm.json b/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.esm.json
new file mode 100644
index 0000000000..2cd2ab579f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.esm.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../lib/esm/puppeteer"
+ },
+ "references": [{"path": "../third_party/tsconfig.json"}]
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts
new file mode 100644
index 0000000000..4d96d0cdf4
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {AwaitableIterable} from '../common/types.js';
+
+/**
+ * @internal
+ */
+export class AsyncIterableUtil {
+ static async *map<T, U>(
+ iterable: AwaitableIterable<T>,
+ map: (item: T) => Promise<U>
+ ): AsyncIterable<U> {
+ for await (const value of iterable) {
+ yield await map(value);
+ }
+ }
+
+ static async *flatMap<T, U>(
+ iterable: AwaitableIterable<T>,
+ map: (item: T) => AwaitableIterable<U>
+ ): AsyncIterable<U> {
+ for await (const value of iterable) {
+ yield* map(value);
+ }
+ }
+
+ static async collect<T>(iterable: AwaitableIterable<T>): Promise<T[]> {
+ const result = [];
+ for await (const value of iterable) {
+ result.push(value);
+ }
+ return result;
+ }
+
+ static async first<T>(
+ iterable: AwaitableIterable<T>
+ ): Promise<T | undefined> {
+ for await (const value of iterable) {
+ return value;
+ }
+ return;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.test.ts
new file mode 100644
index 0000000000..b989e3a888
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.test.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+import sinon from 'sinon';
+
+import {Deferred} from './Deferred.js';
+
+describe('DeferredPromise', function () {
+ it('should catch errors', async () => {
+ // Async function before try/catch.
+ async function task() {
+ await new Promise(resolve => {
+ return setTimeout(resolve, 50);
+ });
+ }
+ // Async function that fails.
+ function fails(): Deferred<void> {
+ const deferred = Deferred.create<void>();
+ setTimeout(() => {
+ deferred.reject(new Error('test'));
+ }, 25);
+ return deferred;
+ }
+
+ const expectedToFail = fails();
+ await task();
+ let caught = false;
+ try {
+ await expectedToFail.valueOrThrow();
+ } catch (err) {
+ expect((err as Error).message).toEqual('test');
+ caught = true;
+ }
+ expect(caught).toBeTruthy();
+ });
+
+ it('Deferred.race should cancel timeout', async function () {
+ const clock = sinon.useFakeTimers();
+
+ try {
+ const deferred = Deferred.create<void>();
+ const deferredTimeout = Deferred.create<void>({
+ message: 'Race did not stop timer',
+ timeout: 100,
+ });
+
+ clock.tick(50);
+
+ await Promise.all([
+ Deferred.race([deferred, deferredTimeout]),
+ deferred.resolve(),
+ ]);
+
+ clock.tick(150);
+
+ expect(deferredTimeout.value()).toBeInstanceOf(Error);
+ expect(deferredTimeout.value()?.message).toContain('Timeout cleared');
+ } finally {
+ clock.restore();
+ }
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts
new file mode 100644
index 0000000000..0dfb013bb3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts
@@ -0,0 +1,122 @@
+import {TimeoutError} from '../common/Errors.js';
+
+/**
+ * @internal
+ */
+export interface DeferredOptions {
+ message: string;
+ timeout: number;
+}
+
+/**
+ * Creates and returns a deferred object along with the resolve/reject functions.
+ *
+ * If the deferred has not been resolved/rejected within the `timeout` period,
+ * the deferred gets resolves with a timeout error. `timeout` has to be greater than 0 or
+ * it is ignored.
+ *
+ * @internal
+ */
+export class Deferred<T, V extends Error = Error> {
+ static create<R, X extends Error = Error>(
+ opts?: DeferredOptions
+ ): Deferred<R, X> {
+ return new Deferred<R, X>(opts);
+ }
+
+ static async race<R>(
+ awaitables: Array<Promise<R> | Deferred<R>>
+ ): Promise<R> {
+ const deferredWithTimeout = new Set<Deferred<R>>();
+ try {
+ const promises = awaitables.map(value => {
+ if (value instanceof Deferred) {
+ if (value.#timeoutId) {
+ deferredWithTimeout.add(value);
+ }
+
+ return value.valueOrThrow();
+ }
+
+ return value;
+ });
+ // eslint-disable-next-line no-restricted-syntax
+ return await Promise.race(promises);
+ } finally {
+ for (const deferred of deferredWithTimeout) {
+ // We need to stop the timeout else
+ // Node.JS will keep running the event loop till the
+ // timer executes
+ deferred.reject(new Error('Timeout cleared'));
+ }
+ }
+ }
+
+ #isResolved = false;
+ #isRejected = false;
+ #value: T | V | TimeoutError | undefined;
+ // SAFETY: This is ensured by #taskPromise.
+ #resolve!: (value: void) => void;
+ #taskPromise = new Promise<void>(resolve => {
+ this.#resolve = resolve;
+ });
+ #timeoutId: ReturnType<typeof setTimeout> | undefined;
+ #timeoutError: TimeoutError | undefined;
+
+ constructor(opts?: DeferredOptions) {
+ if (opts && opts.timeout > 0) {
+ this.#timeoutError = new TimeoutError(opts.message);
+ this.#timeoutId = setTimeout(() => {
+ this.reject(this.#timeoutError!);
+ }, opts.timeout);
+ }
+ }
+
+ #finish(value: T | V | TimeoutError) {
+ clearTimeout(this.#timeoutId);
+ this.#value = value;
+ this.#resolve();
+ }
+
+ resolve(value: T): void {
+ if (this.#isRejected || this.#isResolved) {
+ return;
+ }
+ this.#isResolved = true;
+ this.#finish(value);
+ }
+
+ reject(error: V | TimeoutError): void {
+ if (this.#isRejected || this.#isResolved) {
+ return;
+ }
+ this.#isRejected = true;
+ this.#finish(error);
+ }
+
+ resolved(): boolean {
+ return this.#isResolved;
+ }
+
+ finished(): boolean {
+ return this.#isResolved || this.#isRejected;
+ }
+
+ value(): T | V | TimeoutError | undefined {
+ return this.#value;
+ }
+
+ #promise: Promise<T> | undefined;
+ valueOrThrow(): Promise<T> {
+ if (!this.#promise) {
+ this.#promise = (async () => {
+ await this.#taskPromise;
+ if (this.#isRejected) {
+ throw this.#value;
+ }
+ return this.#value as T;
+ })();
+ }
+ return this.#promise;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts
new file mode 100644
index 0000000000..d4ab3044ab
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ProtocolError} from '../common/Errors.js';
+
+/**
+ * @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)
+ );
+}
+
+/**
+ * @internal
+ */
+export function rewriteError(
+ error: ProtocolError,
+ message: string,
+ originalMessage?: string
+): Error {
+ error.message = message;
+ error.originalMessage = originalMessage ?? error.originalMessage;
+ return error;
+}
+
+/**
+ * @internal
+ */
+export function createProtocolErrorMessage(object: {
+ error: {message: string; data: any; code: number};
+}): string {
+ let message = object.error.message;
+ // TODO: remove the type checks when we stop connecting to BiDi with a CDP
+ // client.
+ if (
+ object.error &&
+ typeof object.error === 'object' &&
+ 'data' in object.error
+ ) {
+ message += ` ${object.error.data}`;
+ }
+ return message;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.test.ts
new file mode 100644
index 0000000000..c6da4cdf27
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.test.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+
+import {interpolateFunction} from './Function.js';
+
+describe('Function', function () {
+ describe('interpolateFunction', function () {
+ it('should work', async () => {
+ const test = interpolateFunction(
+ () => {
+ const test = PLACEHOLDER('test') as () => number;
+ return test();
+ },
+ {test: `() => 5`}
+ );
+ expect(test()).toBe(5);
+ });
+ it('should work inlined', async () => {
+ const test = interpolateFunction(
+ () => {
+ // Note the parenthesis will be removed by the typescript compiler.
+ return (PLACEHOLDER('test') as () => number)();
+ },
+ {test: `() => 5`}
+ );
+ expect(test()).toBe(5);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts
new file mode 100644
index 0000000000..41db98830b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
+
+/**
+ * Creates a function from a string.
+ *
+ * @internal
+ */
+export const createFunction = (
+ functionValue: string
+): ((...args: unknown[]) => unknown) => {
+ let fn = createdFunctions.get(functionValue);
+ if (fn) {
+ return fn;
+ }
+ fn = new Function(`return ${functionValue}`)() as (
+ ...args: unknown[]
+ ) => unknown;
+ createdFunctions.set(functionValue, fn);
+ return fn;
+};
+
+/**
+ * @internal
+ */
+export function stringifyFunction(fn: (...args: never) => unknown): string {
+ let value = fn.toString();
+ try {
+ new Function(`(${value})`);
+ } catch {
+ // This means we might have a function shorthand (e.g. `test(){}`). Let's
+ // try prefixing.
+ let prefix = 'function ';
+ if (value.startsWith('async ')) {
+ prefix = `async ${prefix}`;
+ value = value.substring('async '.length);
+ }
+ value = `${prefix}${value}`;
+ try {
+ new Function(`(${value})`);
+ } catch {
+ // We tried hard to serialize, but there's a weird beast here.
+ throw new Error('Passed function cannot be serialized!');
+ }
+ }
+ return value;
+}
+
+/**
+ * Replaces `PLACEHOLDER`s with the given replacements.
+ *
+ * All replacements must be valid JS code.
+ *
+ * @example
+ *
+ * ```ts
+ * interpolateFunction(() => PLACEHOLDER('test'), {test: 'void 0'});
+ * // Equivalent to () => void 0
+ * ```
+ *
+ * @internal
+ */
+export const interpolateFunction = <T extends (...args: never[]) => unknown>(
+ fn: T,
+ replacements: Record<string, string>
+): T => {
+ let value = stringifyFunction(fn);
+ for (const [name, jsValue] of Object.entries(replacements)) {
+ value = value.replace(
+ new RegExp(`PLACEHOLDER\\(\\s*(?:'${name}'|"${name}")\\s*\\)`, 'g'),
+ // Wrapping this ensures tersers that accidently inline PLACEHOLDER calls
+ // are still valid. Without, we may get calls like ()=>{...}() which is
+ // not valid.
+ `(${jsValue})`
+ );
+ }
+ return createFunction(value) as unknown as T;
+};
+
+declare global {
+ /**
+ * Used for interpolation with {@link interpolateFunction}.
+ *
+ * @internal
+ */
+ function PLACEHOLDER<T>(name: string): T;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts
new file mode 100644
index 0000000000..9498bac306
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts
@@ -0,0 +1,41 @@
+import {Deferred} from './Deferred.js';
+import {disposeSymbol} from './disposable.js';
+
+/**
+ * @internal
+ */
+export class Mutex {
+ static Guard = class Guard {
+ #mutex: Mutex;
+ constructor(mutex: Mutex) {
+ this.#mutex = mutex;
+ }
+ [disposeSymbol](): void {
+ return this.#mutex.release();
+ }
+ };
+
+ #locked = false;
+ #acquirers: Array<() => void> = [];
+
+ // This is FIFO.
+ async acquire(): Promise<InstanceType<typeof Mutex.Guard>> {
+ if (!this.#locked) {
+ this.#locked = true;
+ return new Mutex.Guard(this);
+ }
+ const deferred = Deferred.create<void>();
+ this.#acquirers.push(deferred.resolve.bind(deferred));
+ await deferred.valueOrThrow();
+ return new Mutex.Guard(this);
+ }
+
+ release(): void {
+ const resolve = this.#acquirers.shift();
+ if (!resolve) {
+ this.#locked = false;
+ return;
+ }
+ resolve();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts
new file mode 100644
index 0000000000..7800b3be40
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts
@@ -0,0 +1,21 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Asserts that the given value is truthy.
+ * @param value - some conditional statement
+ * @param message - the error message to throw if the value is not truthy.
+ *
+ * @internal
+ */
+export const assert: (value: unknown, message?: string) => asserts value = (
+ value,
+ message
+) => {
+ if (!value) {
+ throw new Error(message);
+ }
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts
new file mode 100644
index 0000000000..4cdaf15d5b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it} from 'node:test';
+
+import expect from 'expect';
+import sinon from 'sinon';
+
+import {invokeAtMostOnceForArguments} from './decorators.js';
+
+describe('decorators', function () {
+ describe('invokeAtMostOnceForArguments', () => {
+ it('should delegate calls', () => {
+ const spy = sinon.spy();
+ class Test {
+ @invokeAtMostOnceForArguments
+ test(obj1: object, obj2: object) {
+ spy(obj1, obj2);
+ }
+ }
+ const t = new Test();
+ expect(spy.callCount).toBe(0);
+ const obj1 = {};
+ const obj2 = {};
+ t.test(obj1, obj2);
+ expect(spy.callCount).toBe(1);
+ });
+
+ it('should prevent repeated calls', () => {
+ const spy = sinon.spy();
+ class Test {
+ @invokeAtMostOnceForArguments
+ test(obj1: object, obj2: object) {
+ spy(obj1, obj2);
+ }
+ }
+ const t = new Test();
+ expect(spy.callCount).toBe(0);
+ const obj1 = {};
+ const obj2 = {};
+ t.test(obj1, obj2);
+ expect(spy.callCount).toBe(1);
+ expect(spy.lastCall.calledWith(obj1, obj2)).toBeTruthy();
+ t.test(obj1, obj2);
+ expect(spy.callCount).toBe(1);
+ expect(spy.lastCall.calledWith(obj1, obj2)).toBeTruthy();
+ const obj3 = {};
+ t.test(obj1, obj3);
+ expect(spy.callCount).toBe(2);
+ expect(spy.lastCall.calledWith(obj1, obj3)).toBeTruthy();
+ });
+
+ it('should throw an error for dynamic argumetns', () => {
+ class Test {
+ @invokeAtMostOnceForArguments
+ test(..._args: unknown[]) {}
+ }
+ const t = new Test();
+ t.test({});
+ expect(() => {
+ t.test({}, {});
+ }).toThrow();
+ });
+
+ it('should throw an error for non object arguments', () => {
+ class Test {
+ @invokeAtMostOnceForArguments
+ test(..._args: unknown[]) {}
+ }
+ const t = new Test();
+ expect(() => {
+ t.test(1);
+ }).toThrow();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts
new file mode 100644
index 0000000000..af21c5fe29
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts
@@ -0,0 +1,140 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Disposed, Moveable} from '../common/types.js';
+
+import {asyncDisposeSymbol, disposeSymbol} from './disposable.js';
+import {Mutex} from './Mutex.js';
+
+const instances = new WeakSet<object>();
+
+export function moveable<
+ Class extends abstract new (...args: never[]) => Moveable,
+>(Class: Class, _: ClassDecoratorContext<Class>): Class {
+ let hasDispose = false;
+ if (Class.prototype[disposeSymbol]) {
+ const dispose = Class.prototype[disposeSymbol];
+ Class.prototype[disposeSymbol] = function (this: InstanceType<Class>) {
+ if (instances.has(this)) {
+ instances.delete(this);
+ return;
+ }
+ return dispose.call(this);
+ };
+ hasDispose = true;
+ }
+ if (Class.prototype[asyncDisposeSymbol]) {
+ const asyncDispose = Class.prototype[asyncDisposeSymbol];
+ Class.prototype[asyncDisposeSymbol] = function (this: InstanceType<Class>) {
+ if (instances.has(this)) {
+ instances.delete(this);
+ return;
+ }
+ return asyncDispose.call(this);
+ };
+ hasDispose = true;
+ }
+ if (hasDispose) {
+ Class.prototype.move = function (
+ this: InstanceType<Class>
+ ): InstanceType<Class> {
+ instances.add(this);
+ return this;
+ };
+ }
+ return Class;
+}
+
+export function throwIfDisposed<This extends Disposed>(
+ message: (value: This) => string = value => {
+ return `Attempted to use disposed ${value.constructor.name}.`;
+ }
+) {
+ return (target: (this: This, ...args: any[]) => any, _: unknown) => {
+ return function (this: This, ...args: any[]): any {
+ if (this.disposed) {
+ throw new Error(message(this));
+ }
+ return target.call(this, ...args);
+ };
+ };
+}
+
+export function inertIfDisposed<This extends Disposed>(
+ target: (this: This, ...args: any[]) => any,
+ _: unknown
+) {
+ return function (this: This, ...args: any[]): any {
+ if (this.disposed) {
+ return;
+ }
+ return target.call(this, ...args);
+ };
+}
+
+/**
+ * The decorator only invokes the target if the target has not been invoked with
+ * the same arguments before. The decorated method throws an error if it's
+ * invoked with a different number of elements: if you decorate a method, it
+ * should have the same number of arguments
+ *
+ * @internal
+ */
+export function invokeAtMostOnceForArguments(
+ target: (this: unknown, ...args: any[]) => any,
+ _: unknown
+): typeof target {
+ const cache = new WeakMap();
+ let cacheDepth = -1;
+ return function (this: unknown, ...args: unknown[]) {
+ if (cacheDepth === -1) {
+ cacheDepth = args.length;
+ }
+ if (cacheDepth !== args.length) {
+ throw new Error(
+ 'Memoized method was called with the wrong number of arguments'
+ );
+ }
+ let freshArguments = false;
+ let cacheIterator = cache;
+ for (const arg of args) {
+ if (cacheIterator.has(arg as object)) {
+ cacheIterator = cacheIterator.get(arg as object)!;
+ } else {
+ freshArguments = true;
+ cacheIterator.set(arg as object, new WeakMap());
+ cacheIterator = cacheIterator.get(arg as object)!;
+ }
+ }
+ if (!freshArguments) {
+ return;
+ }
+ return target.call(this, ...args);
+ };
+}
+
+export function guarded<T extends object>(
+ getKey = function (this: T): object {
+ return this;
+ }
+) {
+ return (
+ target: (this: T, ...args: any[]) => Promise<any>,
+ _: ClassMethodDecoratorContext<T>
+ ): typeof target => {
+ const mutexes = new WeakMap<object, Mutex>();
+ return async function (...args) {
+ const key = getKey.call(this);
+ let mutex = mutexes.get(key);
+ if (!mutex) {
+ mutex = new Mutex();
+ mutexes.set(key, mutex);
+ }
+ await using _ = await mutex.acquire();
+ return await target.call(this, ...args);
+ };
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/disposable.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/disposable.ts
new file mode 100644
index 0000000000..a1848f3860
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/disposable.ts
@@ -0,0 +1,275 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+declare global {
+ interface SymbolConstructor {
+ /**
+ * A method that is used to release resources held by an object. Called by
+ * the semantics of the `using` statement.
+ */
+ readonly dispose: unique symbol;
+
+ /**
+ * A method that is used to asynchronously release resources held by an
+ * object. Called by the semantics of the `await using` statement.
+ */
+ readonly asyncDispose: unique symbol;
+ }
+
+ interface Disposable {
+ [Symbol.dispose](): void;
+ }
+
+ interface AsyncDisposable {
+ [Symbol.asyncDispose](): PromiseLike<void>;
+ }
+}
+
+(Symbol as any).dispose ??= Symbol('dispose');
+(Symbol as any).asyncDispose ??= Symbol('asyncDispose');
+
+/**
+ * @internal
+ */
+export const disposeSymbol: typeof Symbol.dispose = Symbol.dispose;
+
+/**
+ * @internal
+ */
+export const asyncDisposeSymbol: typeof Symbol.asyncDispose =
+ Symbol.asyncDispose;
+
+/**
+ * @internal
+ */
+export class DisposableStack {
+ #disposed = false;
+ #stack: Disposable[] = [];
+
+ /**
+ * Returns a value indicating whether this stack has been disposed.
+ */
+ get disposed(): boolean {
+ return this.#disposed;
+ }
+
+ /**
+ * Disposes each resource in the stack in the reverse order that they were added.
+ */
+ dispose(): void {
+ if (this.#disposed) {
+ return;
+ }
+ this.#disposed = true;
+ for (const resource of this.#stack.reverse()) {
+ resource[disposeSymbol]();
+ }
+ }
+
+ /**
+ * Adds a disposable resource to the stack, returning the resource.
+ *
+ * @param value - The resource to add. `null` and `undefined` will not be added,
+ * but will be returned.
+ * @returns The provided `value`.
+ */
+ use<T extends Disposable | null | undefined>(value: T): T {
+ if (value) {
+ this.#stack.push(value);
+ }
+ return value;
+ }
+
+ /**
+ * Adds a value and associated disposal callback as a resource to the stack.
+ *
+ * @param value - The value to add.
+ * @param onDispose - The callback to use in place of a `[disposeSymbol]()`
+ * method. Will be invoked with `value` as the first parameter.
+ * @returns The provided `value`.
+ */
+ adopt<T>(value: T, onDispose: (value: T) => void): T {
+ this.#stack.push({
+ [disposeSymbol]() {
+ onDispose(value);
+ },
+ });
+ return value;
+ }
+
+ /**
+ * Adds a callback to be invoked when the stack is disposed.
+ */
+ defer(onDispose: () => void): void {
+ this.#stack.push({
+ [disposeSymbol]() {
+ onDispose();
+ },
+ });
+ }
+
+ /**
+ * Move all resources out of this stack and into a new `DisposableStack`, and
+ * marks this stack as disposed.
+ *
+ * @example
+ *
+ * ```ts
+ * class C {
+ * #res1: Disposable;
+ * #res2: Disposable;
+ * #disposables: DisposableStack;
+ * constructor() {
+ * // stack will be disposed when exiting constructor for any reason
+ * using stack = new DisposableStack();
+ *
+ * // get first resource
+ * this.#res1 = stack.use(getResource1());
+ *
+ * // get second resource. If this fails, both `stack` and `#res1` will be disposed.
+ * this.#res2 = stack.use(getResource2());
+ *
+ * // all operations succeeded, move resources out of `stack` so that
+ * // they aren't disposed when constructor exits
+ * this.#disposables = stack.move();
+ * }
+ *
+ * [disposeSymbol]() {
+ * this.#disposables.dispose();
+ * }
+ * }
+ * ```
+ */
+ move(): DisposableStack {
+ if (this.#disposed) {
+ throw new ReferenceError('a disposed stack can not use anything new'); // step 3
+ }
+ const stack = new DisposableStack(); // step 4-5
+ stack.#stack = this.#stack;
+ this.#disposed = true;
+ return stack;
+ }
+
+ [disposeSymbol] = this.dispose;
+
+ readonly [Symbol.toStringTag] = 'DisposableStack';
+}
+
+/**
+ * @internal
+ */
+export class AsyncDisposableStack {
+ #disposed = false;
+ #stack: AsyncDisposable[] = [];
+
+ /**
+ * Returns a value indicating whether this stack has been disposed.
+ */
+ get disposed(): boolean {
+ return this.#disposed;
+ }
+
+ /**
+ * Disposes each resource in the stack in the reverse order that they were added.
+ */
+ async dispose(): Promise<void> {
+ if (this.#disposed) {
+ return;
+ }
+ this.#disposed = true;
+ for (const resource of this.#stack.reverse()) {
+ await resource[asyncDisposeSymbol]();
+ }
+ }
+
+ /**
+ * Adds a disposable resource to the stack, returning the resource.
+ *
+ * @param value - The resource to add. `null` and `undefined` will not be added,
+ * but will be returned.
+ * @returns The provided `value`.
+ */
+ use<T extends AsyncDisposable | null | undefined>(value: T): T {
+ if (value) {
+ this.#stack.push(value);
+ }
+ return value;
+ }
+
+ /**
+ * Adds a value and associated disposal callback as a resource to the stack.
+ *
+ * @param value - The value to add.
+ * @param onDispose - The callback to use in place of a `[disposeSymbol]()`
+ * method. Will be invoked with `value` as the first parameter.
+ * @returns The provided `value`.
+ */
+ adopt<T>(value: T, onDispose: (value: T) => Promise<void>): T {
+ this.#stack.push({
+ [asyncDisposeSymbol]() {
+ return onDispose(value);
+ },
+ });
+ return value;
+ }
+
+ /**
+ * Adds a callback to be invoked when the stack is disposed.
+ */
+ defer(onDispose: () => Promise<void>): void {
+ this.#stack.push({
+ [asyncDisposeSymbol]() {
+ return onDispose();
+ },
+ });
+ }
+
+ /**
+ * Move all resources out of this stack and into a new `DisposableStack`, and
+ * marks this stack as disposed.
+ *
+ * @example
+ *
+ * ```ts
+ * class C {
+ * #res1: Disposable;
+ * #res2: Disposable;
+ * #disposables: DisposableStack;
+ * constructor() {
+ * // stack will be disposed when exiting constructor for any reason
+ * using stack = new DisposableStack();
+ *
+ * // get first resource
+ * this.#res1 = stack.use(getResource1());
+ *
+ * // get second resource. If this fails, both `stack` and `#res1` will be disposed.
+ * this.#res2 = stack.use(getResource2());
+ *
+ * // all operations succeeded, move resources out of `stack` so that
+ * // they aren't disposed when constructor exits
+ * this.#disposables = stack.move();
+ * }
+ *
+ * [disposeSymbol]() {
+ * this.#disposables.dispose();
+ * }
+ * }
+ * ```
+ */
+ move(): AsyncDisposableStack {
+ if (this.#disposed) {
+ throw new ReferenceError('a disposed stack can not use anything new'); // step 3
+ }
+ const stack = new AsyncDisposableStack(); // step 4-5
+ stack.#stack = this.#stack;
+ this.#disposed = true;
+ return stack;
+ }
+
+ [asyncDisposeSymbol] = this.dispose;
+
+ readonly [Symbol.toStringTag] = 'AsyncDisposableStack';
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts
new file mode 100644
index 0000000000..f55610da9e
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './assert.js';
+export * from './Deferred.js';
+export * from './ErrorLike.js';
+export * from './AsyncIterableUtil.js';
+export * from './disposable.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts b/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts
new file mode 100644
index 0000000000..c20aaa8342
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/mitt.ts
@@ -0,0 +1,8 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from 'mitt';
+export {default as default} from 'mitt';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts b/remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts
new file mode 100644
index 0000000000..b8b64788ae
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/rxjs/rxjs.ts
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+export {
+ bufferCount,
+ catchError,
+ concat,
+ concatMap,
+ defaultIfEmpty,
+ defer,
+ delay,
+ EMPTY,
+ filter,
+ first,
+ firstValueFrom,
+ forkJoin,
+ from,
+ fromEvent,
+ identity,
+ ignoreElements,
+ lastValueFrom,
+ map,
+ merge,
+ mergeMap,
+ NEVER,
+ noop,
+ Observable,
+ of,
+ pipe,
+ race,
+ raceWith,
+ retry,
+ startWith,
+ switchMap,
+ takeUntil,
+ tap,
+ throwIfEmpty,
+ timer,
+ zip,
+} from 'rxjs';
+
+export type * from 'rxjs';
+
+import {filter, from, map, mergeMap, type Observable} from 'rxjs';
+
+export function filterAsync<T>(
+ predicate: (value: T) => boolean | PromiseLike<boolean>
+) {
+ return mergeMap<T, Observable<T>>(value => {
+ return from(Promise.resolve(predicate(value))).pipe(
+ filter(isMatch => {
+ return isMatch;
+ }),
+ map(() => {
+ return value;
+ })
+ );
+ });
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.cjs.json b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.cjs.json
new file mode 100644
index 0000000000..a796932cd8
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.cjs.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "../lib/cjs/third_party",
+ "declarationMap": false,
+ "sourceMap": false
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json
new file mode 100644
index 0000000000..25c438c57d
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "declarationMap": false,
+ "outDir": "../lib/esm/third_party",
+ "sourceMap": false,
+ },
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/tools/ensure-correct-devtools-protocol-package.ts b/remote/test/puppeteer/packages/puppeteer-core/tools/ensure-correct-devtools-protocol-package.ts
new file mode 100644
index 0000000000..ca230716b3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/tools/ensure-correct-devtools-protocol-package.ts
@@ -0,0 +1,86 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * This script ensures that the pinned version of devtools-protocol in
+ * package.json is the right version for the current revision of Chrome that
+ * Puppeteer ships with.
+ *
+ * The devtools-protocol package publisher runs every hour and checks if there
+ * are protocol changes. If there are, it will be versioned with the revision
+ * number of the commit that last changed the .pdl files.
+ *
+ * Chrome branches/releases are figured out at a later point in time, so it's
+ * not true that each Chrome revision will have an exact matching revision
+ * version of devtools-protocol. To ensure we're using a devtools-protocol that
+ * is aligned with our revision, we want to find the largest package number
+ * that's \<= the revision that Puppeteer is using.
+ *
+ * This script uses npm's `view` function to list all versions in a range and
+ * find the one closest to our Chrome revision.
+ */
+
+import {execSync} from 'child_process';
+
+import packageJson from '../package.json' assert {type: 'json'};
+import {PUPPETEER_REVISIONS} from '../src/revisions.js';
+
+async function main() {
+ const currentProtocolPackageInstalledVersion =
+ packageJson.dependencies['devtools-protocol'];
+
+ /**
+ * Ensure that the devtools-protocol version is pinned.
+ */
+ if (/^[^0-9]/.test(currentProtocolPackageInstalledVersion)) {
+ console.log(
+ `ERROR: devtools-protocol package is not pinned to a specific version.\n`
+ );
+ process.exit(1);
+ }
+
+ const chromeVersion = PUPPETEER_REVISIONS.chrome;
+ // find the right revision for our Chrome version.
+ const req = await fetch(
+ `https://googlechromelabs.github.io/chrome-for-testing/known-good-versions.json`
+ );
+ const releases = await req.json();
+ const chromeRevision = releases.versions.find(release => {
+ return release.version === chromeVersion;
+ }).revision;
+ console.log(`Revisions for ${chromeVersion}: ${chromeRevision}`);
+
+ const command = `npm view "devtools-protocol@<=0.0.${chromeRevision}" version | tail -1`;
+
+ console.log(
+ 'Checking npm for devtools-protocol revisions:\n',
+ `'${command}'`,
+ '\n'
+ );
+
+ const output = execSync(command, {
+ encoding: 'utf8',
+ });
+
+ const bestRevisionFromNpm = output.split(' ')[1]!.replace(/'|\n/g, '');
+
+ if (currentProtocolPackageInstalledVersion !== bestRevisionFromNpm) {
+ console.log(`ERROR: bad devtools-protocol revision detected:
+
+ Current Puppeteer Chrome revision: ${chromeRevision}
+ Current devtools-protocol version in package.json: ${currentProtocolPackageInstalledVersion}
+ Expected devtools-protocol version: ${bestRevisionFromNpm}`);
+
+ process.exit(1);
+ }
+
+ console.log(
+ `Correct devtools-protocol version found (${bestRevisionFromNpm}).`
+ );
+ process.exit(0);
+}
+
+void main();
diff --git a/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json b/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json
new file mode 100644
index 0000000000..b662532a01
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/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/puppeteer-core/tsdoc.json b/remote/test/puppeteer/packages/puppeteer-core/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/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/puppeteer/.gitignore b/remote/test/puppeteer/packages/puppeteer/.gitignore
new file mode 100644
index 0000000000..42061c01a1
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/.gitignore
@@ -0,0 +1 @@
+README.md \ No newline at end of file
diff --git a/remote/test/puppeteer/packages/puppeteer/CHANGELOG.md b/remote/test/puppeteer/packages/puppeteer/CHANGELOG.md
new file mode 100644
index 0000000000..c3d834c5f5
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/CHANGELOG.md
@@ -0,0 +1,2096 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * @puppeteer/browsers bumped from 0.3.0 to 0.3.1
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.1.1 to 20.1.2
+ * @puppeteer/browsers bumped from 1.0.1 to 1.1.0
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.8.1 to 20.8.2
+ * @puppeteer/browsers bumped from 1.4.4 to 1.4.5
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.0.2 to 21.0.3
+ * @puppeteer/browsers bumped from 1.5.1 to 1.6.0
+
+## [21.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.9.0...puppeteer-v21.10.0) (2024-01-29)
+
+
+### Features
+
+* download chrome-headless-shell by default and use it for the old headless mode ([#11754](https://github.com/puppeteer/puppeteer/issues/11754)) ([ce894a2](https://github.com/puppeteer/puppeteer/commit/ce894a2ffce4bc44bd11f12d1f0543e003a97e02))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.9.0 to 21.10.0
+
+## [21.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.8.0...puppeteer-v21.9.0) (2024-01-24)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.8.0 to 21.9.0
+
+## [21.8.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.7.0...puppeteer-v21.8.0) (2024-01-24)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.7.0 to 21.8.0
+
+## [21.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.6.1...puppeteer-v21.7.0) (2024-01-04)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.6.1 to 21.7.0
+ * @puppeteer/browsers bumped from 1.9.0 to 1.9.1
+
+## [21.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.6.0...puppeteer-v21.6.1) (2023-12-13)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.6.0 to 21.6.1
+
+## [21.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.5.2...puppeteer-v21.6.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))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.5.2 to 21.6.0
+ * @puppeteer/browsers bumped from 1.8.0 to 1.9.0
+
+## [21.5.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.5.1...puppeteer-v21.5.2) (2023-11-15)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.5.1 to 21.5.2
+
+## [21.5.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.5.0...puppeteer-v21.5.1) (2023-11-09)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.5.0 to 21.5.1
+
+## [21.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.4.1...puppeteer-v21.5.0) (2023-11-02)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.4.1 to 21.5.0
+
+## [21.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.4.0...puppeteer-v21.4.1) (2023-10-23)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.4.0 to 21.4.1
+
+## [21.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.8...puppeteer-v21.4.0) (2023-10-20)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.8 to 21.4.0
+ * @puppeteer/browsers bumped from 1.7.1 to 1.8.0
+
+## [21.3.8](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.7...puppeteer-v21.3.8) (2023-10-06)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.7 to 21.3.8
+
+## [21.3.7](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.6...puppeteer-v21.3.7) (2023-10-05)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.6 to 21.3.7
+
+## [21.3.6](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.5...puppeteer-v21.3.6) (2023-09-28)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.5 to 21.3.6
+
+## [21.3.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.4...puppeteer-v21.3.5) (2023-09-26)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.4 to 21.3.5
+
+## [21.3.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.3...puppeteer-v21.3.4) (2023-09-22)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.3 to 21.3.4
+
+## [21.3.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.2...puppeteer-v21.3.3) (2023-09-22)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.2 to 21.3.3
+
+## [21.3.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.1...puppeteer-v21.3.2) (2023-09-22)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.1 to 21.3.2
+
+## [21.3.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.3.0...puppeteer-v21.3.1) (2023-09-19)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.3.0 to 21.3.1
+
+## [21.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.2.1...puppeteer-v21.3.0) (2023-09-19)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.2.1 to 21.3.0
+
+## [21.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.2.0...puppeteer-v21.2.1) (2023-09-13)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.2.0 to 21.2.1
+ * @puppeteer/browsers bumped from 1.7.0 to 1.7.1
+
+## [21.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.1.1...puppeteer-v21.2.0) (2023-09-12)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.1.1 to 21.2.0
+
+## [21.1.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.1.0...puppeteer-v21.1.1) (2023-08-28)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.1.0 to 21.1.1
+
+## [21.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.0.3...puppeteer-v21.1.0) (2023-08-18)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.0.3 to 21.1.0
+ * @puppeteer/browsers bumped from 1.6.0 to 1.7.0
+
+## [21.0.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.0.1...puppeteer-v21.0.2) (2023-08-08)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.0.1 to 21.0.2
+ * @puppeteer/browsers bumped from 1.5.0 to 1.5.1
+
+## [21.0.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v21.0.0...puppeteer-v21.0.1) (2023-08-03)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 21.0.0 to 21.0.1
+
+## [21.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.9.0...puppeteer-v21.0.0) (2023-08-02)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.9.0 to 21.0.0
+ * @puppeteer/browsers bumped from 1.4.6 to 1.5.0
+
+## [20.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.8.3...puppeteer-v20.9.0) (2023-07-20)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.8.3 to 20.9.0
+ * @puppeteer/browsers bumped from 1.4.5 to 1.4.6
+
+## [20.8.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.8.2...puppeteer-v20.8.3) (2023-07-18)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.8.2 to 20.8.3
+
+## [20.8.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.8.0...puppeteer-v20.8.1) (2023-07-11)
+
+
+### Bug Fixes
+
+* remove test metadata files ([#10520](https://github.com/puppeteer/puppeteer/issues/10520)) ([cbf4f2a](https://github.com/puppeteer/puppeteer/commit/cbf4f2a66912f24849ae8c88fc1423851dcc4aa7))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.8.0 to 20.8.1
+ * @puppeteer/browsers bumped from 1.4.3 to 1.4.4
+
+## [20.8.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.7.4...puppeteer-v20.8.0) (2023-07-06)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.7.4 to 20.8.0
+
+## [20.7.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.7.3...puppeteer-v20.7.4) (2023-06-29)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.7.3 to 20.7.4
+ * @puppeteer/browsers bumped from 1.4.2 to 1.4.3
+
+## [20.7.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.7.2...puppeteer-v20.7.3) (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))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.7.2 to 20.7.3
+ * @puppeteer/browsers bumped from 1.4.1 to 1.4.2
+
+## [20.7.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.7.1...puppeteer-v20.7.2) (2023-06-16)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.7.1 to 20.7.2
+
+## [20.7.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.7.0...puppeteer-v20.7.1) (2023-06-13)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.7.0 to 20.7.1
+
+## [20.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.6.0...puppeteer-v20.7.0) (2023-06-13)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.6.0 to 20.7.0
+
+## [20.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.5.0...puppeteer-v20.6.0) (2023-06-09)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.5.0 to 20.6.0
+
+## [20.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.4.0...puppeteer-v20.5.0) (2023-05-31)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.4.0 to 20.5.0
+ * @puppeteer/browsers bumped from 1.4.0 to 1.4.1
+
+## [20.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.3.0...puppeteer-v20.4.0) (2023-05-24)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.3.0 to 20.4.0
+ * @puppeteer/browsers bumped from 1.3.0 to 1.4.0
+
+## [20.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.2.1...puppeteer-v20.3.0) (2023-05-22)
+
+
+### Features
+
+* add an ability to trim cache for Puppeteer ([#10199](https://github.com/puppeteer/puppeteer/issues/10199)) ([1ad32ec](https://github.com/puppeteer/puppeteer/commit/1ad32ec9948ca3e07e15548a562c8f3c633b3dc3))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.2.1 to 20.3.0
+
+## [20.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.2.0...puppeteer-v20.2.1) (2023-05-15)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.2.0 to 20.2.1
+ * @puppeteer/browsers bumped from 1.2.0 to 1.3.0
+
+## [20.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.1.2...puppeteer-v20.2.0) (2023-05-11)
+
+
+### Bug Fixes
+
+* downloadPath should be used by the install script ([#10163](https://github.com/puppeteer/puppeteer/issues/10163)) ([4398f66](https://github.com/puppeteer/puppeteer/commit/4398f66f281f1ffe5be81b529fc4751edfaf761d))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.1.2 to 20.2.0
+ * @puppeteer/browsers bumped from 1.1.0 to 1.2.0
+
+## [20.1.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.1.0...puppeteer-v20.1.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))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.1.0 to 20.1.1
+ * @puppeteer/browsers bumped from 1.0.0 to 1.0.1
+
+## [20.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.0.0...puppeteer-v20.1.0) (2023-05-03)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 20.0.0 to 20.1.0
+
+## [20.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.11.1...puppeteer-v20.0.0) (2023-05-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054))
+
+### Features
+
+* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054)) ([df4d60c](https://github.com/puppeteer/puppeteer/commit/df4d60c187aa11c4ad783827242e9511f4ec2aab))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.11.1 to 20.0.0
+ * @puppeteer/browsers bumped from 0.5.0 to 1.0.0
+
+## [19.11.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.11.0...puppeteer-v19.11.1) (2023-04-25)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.11.0 to 19.11.1
+
+## [19.11.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.10.1...puppeteer-v19.11.0) (2023-04-24)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.10.1 to 19.11.0
+
+## [19.10.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.10.0...puppeteer-v19.10.1) (2023-04-21)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.10.0 to 19.10.1
+ * @puppeteer/browsers bumped from 0.4.1 to 0.5.0
+
+## [19.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.9.1...puppeteer-v19.10.0) (2023-04-20)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.9.1 to 19.10.0
+
+## [19.9.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.9.0...puppeteer-v19.9.1) (2023-04-17)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.9.0 to 19.9.1
+
+## [19.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.5...puppeteer-v19.9.0) (2023-04-13)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.8.5 to 19.9.0
+ * @puppeteer/browsers bumped from 0.4.0 to 0.4.1
+
+## [19.8.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.4...puppeteer-v19.8.5) (2023-04-06)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.8.4 to 19.8.5
+ * @puppeteer/browsers bumped from 0.3.3 to 0.4.0
+
+## [19.8.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.3...puppeteer-v19.8.4) (2023-04-06)
+
+
+### Bug Fixes
+
+* consider downloadHost as baseUrl ([#9973](https://github.com/puppeteer/puppeteer/issues/9973)) ([05a44af](https://github.com/puppeteer/puppeteer/commit/05a44afe5affcac9fe0f0a2e83f17807c99b2f0c))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.8.3 to 19.8.4
+ * @puppeteer/browsers bumped from 0.3.2 to 0.3.3
+
+## [19.8.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.2...puppeteer-v19.8.3) (2023-04-03)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.8.1 to 19.8.3
+ * @puppeteer/browsers bumped from 0.3.1 to 0.3.2
+
+## [19.8.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.0...puppeteer-v19.8.1) (2023-03-28)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.8.0 to 19.8.1
+
+## [19.8.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.5...puppeteer-v19.8.0) (2023-03-24)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.7.5 to 19.8.0
+
+## [19.7.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.4...puppeteer-v19.7.5) (2023-03-14)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.7.4 to 19.7.5
+
+## [19.7.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.3...puppeteer-v19.7.4) (2023-03-10)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.7.3 to 19.7.4
+
+## [19.7.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.2...puppeteer-v19.7.3) (2023-03-06)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.7.2 to 19.7.3
+
+## [19.7.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.1...puppeteer-v19.7.2) (2023-02-20)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.7.1 to 19.7.2
+
+## [19.7.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.0...puppeteer-v19.7.1) (2023-02-15)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.7.0 to 19.7.1
+
+## [19.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.3...puppeteer-v19.7.0) (2023-02-13)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.6.3 to 19.7.0
+
+## [19.6.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.2...puppeteer-v19.6.3) (2023-02-01)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.6.2 to 19.6.3
+
+## [19.6.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.1...puppeteer-v19.6.2) (2023-01-27)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.6.1 to 19.6.2
+
+## [19.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.0...puppeteer-v19.6.1) (2023-01-26)
+
+
+### Bug Fixes
+
+* don't clean up previous browser versions ([#9568](https://github.com/puppeteer/puppeteer/issues/9568)) ([344bc2a](https://github.com/puppeteer/puppeteer/commit/344bc2af62e4068fe2cb8162d4b6c8242aac843b)), closes [#9533](https://github.com/puppeteer/puppeteer/issues/9533)
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.6.0 to 19.6.1
+
+## [19.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.5.2...puppeteer-v19.6.0) (2023-01-23)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.5.2 to 19.6.0
+
+## [19.5.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.5.1...puppeteer-v19.5.2) (2023-01-11)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.5.1 to 19.5.2
+
+## [19.5.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.5.0...puppeteer-v19.5.1) (2023-01-11)
+
+
+### Bug Fixes
+
+* use puppeteer node for installation script ([#9489](https://github.com/puppeteer/puppeteer/issues/9489)) ([9bf90d9](https://github.com/puppeteer/puppeteer/commit/9bf90d9f4b5aeab06f8b433714712cad3259d36e))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.5.0 to 19.5.1
+
+## [19.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.4.1...puppeteer-v19.5.0) (2023-01-05)
+
+
+### Features
+
+* Default to not downloading if explicit browser path is set ([#9440](https://github.com/puppeteer/puppeteer/issues/9440)) ([d2536d7](https://github.com/puppeteer/puppeteer/commit/d2536d7cf5fa731250bbfd0d18959cacc8afffac)), closes [#9419](https://github.com/puppeteer/puppeteer/issues/9419)
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.4.1 to 19.5.0
+
+## [19.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.4.0...puppeteer-v19.4.1) (2022-12-16)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.4.0 to 19.4.1
+
+## [19.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.3.0...puppeteer-v19.4.0) (2022-12-07)
+
+
+### Features
+
+* **chromium:** roll to Chromium 109.0.5412.0 (r1069273) ([#9364](https://github.com/puppeteer/puppeteer/issues/9364)) ([1875da6](https://github.com/puppeteer/puppeteer/commit/1875da61916df1fbcf98047858c01075bd9af189)), closes [#9233](https://github.com/puppeteer/puppeteer/issues/9233)
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.3.0 to 19.4.0
+
+## [19.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.2.2...puppeteer-v19.3.0) (2022-11-23)
+
+
+### Miscellaneous Chores
+
+* **puppeteer:** Synchronize puppeteer versions
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.2.2 to 19.3.0
+
+## [19.2.2](https://github.com/puppeteer/puppeteer/compare/v19.2.1...v19.2.2) (2022-11-03)
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.2.1 to ^19.2.2
+
+## [19.2.1](https://github.com/puppeteer/puppeteer/compare/v19.2.0...v19.2.1) (2022-10-28)
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.2.0 to ^19.2.1
+
+## [19.2.0](https://github.com/puppeteer/puppeteer/compare/v19.1.2...v19.2.0) (2022-10-26)
+
+
+### Features
+
+* **chromium:** roll to Chromium 108.0.5351.0 (r1056772) ([#9153](https://github.com/puppeteer/puppeteer/issues/9153)) ([e78a4e8](https://github.com/puppeteer/puppeteer/commit/e78a4e89c22bb1180e72d180c16b39673ff9125e))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.1.1 to ^19.2.0
+
+## [19.1.2](https://github.com/puppeteer/puppeteer/compare/v19.1.1...v19.1.2) (2022-10-25)
+
+
+### Bug Fixes
+
+* skip browser download ([#9160](https://github.com/puppeteer/puppeteer/issues/9160)) ([2245d7d](https://github.com/puppeteer/puppeteer/commit/2245d7d6ed0630ee1ad985dcbd48354772924750))
+
+## [19.1.1](https://github.com/puppeteer/puppeteer/compare/v19.1.0...v19.1.1) (2022-10-21)
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.1.0 to ^19.1.1
+
+## [19.1.0](https://github.com/puppeteer/puppeteer/compare/v19.0.0...v19.1.0) (2022-10-21)
+
+
+### Features
+
+* use configuration files ([#9140](https://github.com/puppeteer/puppeteer/issues/9140)) ([ec20174](https://github.com/puppeteer/puppeteer/commit/ec201744f077987b288e3dff52c0906fe700f6fb)), closes [#9128](https://github.com/puppeteer/puppeteer/issues/9128)
+
+
+### Bug Fixes
+
+* update `BrowserFetcher` deprecation message ([#9141](https://github.com/puppeteer/puppeteer/issues/9141)) ([efcbc97](https://github.com/puppeteer/puppeteer/commit/efcbc97c60e4cfd49a9ed25a900f6133d06b290b))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 19.0.0 to ^19.1.0
+
+## [19.0.0](https://github.com/puppeteer/puppeteer/compare/v18.2.1...v19.0.0) (2022-10-14)
+
+
+### ⚠ BREAKING CHANGES
+
+* use `~/.cache/puppeteer` for browser downloads (#9095)
+* deprecate `createBrowserFetcher` in favor of `BrowserFetcher` (#9079)
+* refactor custom query handler API (#9078)
+* remove `puppeteer.devices` in favor of `KnownDevices` (#9075)
+* deprecate indirect network condition imports (#9074)
+
+### Features
+
+* deprecate `createBrowserFetcher` in favor of `BrowserFetcher` ([#9079](https://github.com/puppeteer/puppeteer/issues/9079)) ([7294dfe](https://github.com/puppeteer/puppeteer/commit/7294dfe9c6c3b224f95ba6d59b5ef33d379fd09a)), closes [#8999](https://github.com/puppeteer/puppeteer/issues/8999)
+* use `~/.cache/puppeteer` for browser downloads ([#9095](https://github.com/puppeteer/puppeteer/issues/9095)) ([3df375b](https://github.com/puppeteer/puppeteer/commit/3df375baedad64b8773bb1e1e6f81b604ed18989))
+
+
+### Bug Fixes
+
+* deprecate indirect network condition imports ([#9074](https://github.com/puppeteer/puppeteer/issues/9074)) ([41d0122](https://github.com/puppeteer/puppeteer/commit/41d0122b94f41b308536c48ced345dec8c272a49))
+* refactor custom query handler API ([#9078](https://github.com/puppeteer/puppeteer/issues/9078)) ([1847704](https://github.com/puppeteer/puppeteer/commit/1847704789e2888c755de8c739d567364b8ad645))
+* remove `puppeteer.devices` in favor of `KnownDevices` ([#9075](https://github.com/puppeteer/puppeteer/issues/9075)) ([87c08fd](https://github.com/puppeteer/puppeteer/commit/87c08fd86a79b63308ad8d46c5f7acd1927505f8))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 18.2.1 to ^19.0.0
+
+## [18.2.1](https://github.com/puppeteer/puppeteer/compare/v18.2.0...v18.2.1) (2022-10-06)
+
+
+### Bug Fixes
+
+* add README to package during prepack ([#9057](https://github.com/puppeteer/puppeteer/issues/9057)) ([9374e23](https://github.com/puppeteer/puppeteer/commit/9374e23d3da5e40378461ed08db24649730a445a))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 18.2.0 to ^18.2.1
+
+## [18.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v18.1.0...puppeteer-v18.2.0) (2022-10-05)
+
+
+### Features
+
+* separate puppeteer and puppeteer-core ([#9023](https://github.com/puppeteer/puppeteer/issues/9023)) ([f42336c](https://github.com/puppeteer/puppeteer/commit/f42336cf83982332829ca7e14ee48d8676e11545))
+
+
+### Dependencies
+
+* The following workspace dependencies were updated
+ * dependencies
+ * puppeteer-core bumped from 18.1.0 to ^18.2.0
+
+## [18.1.0](https://github.com/puppeteer/puppeteer/compare/v18.0.5...v18.1.0) (2022-10-05)
+
+
+### Features
+
+* **chromium:** roll to Chromium 107.0.5296.0 (r1045629) ([#9039](https://github.com/puppeteer/puppeteer/issues/9039)) ([022fbde](https://github.com/puppeteer/puppeteer/commit/022fbde85e067e8c419cf42dd571f9a1187c343c))
+
+## [18.0.5](https://github.com/puppeteer/puppeteer/compare/v18.0.4...v18.0.5) (2022-09-22)
+
+
+### Bug Fixes
+
+* add missing npm config environment variable ([#8996](https://github.com/puppeteer/puppeteer/issues/8996)) ([7c1be20](https://github.com/puppeteer/puppeteer/commit/7c1be20aef46aaf5029732a580ec65aa8008aa9c))
+
+## [18.0.4](https://github.com/puppeteer/puppeteer/compare/v18.0.3...v18.0.4) (2022-09-21)
+
+
+### Bug Fixes
+
+* hardcode binding names ([#8993](https://github.com/puppeteer/puppeteer/issues/8993)) ([7e20554](https://github.com/puppeteer/puppeteer/commit/7e2055433e79ef20f6dcdf02f92e1d64564b7d33))
+
+## [18.0.3](https://github.com/puppeteer/puppeteer/compare/v18.0.2...v18.0.3) (2022-09-20)
+
+
+### Bug Fixes
+
+* change injected.ts imports ([#8987](https://github.com/puppeteer/puppeteer/issues/8987)) ([10a114d](https://github.com/puppeteer/puppeteer/commit/10a114d36f2add90860950f61b3f8b93258edb5c))
+
+## [18.0.2](https://github.com/puppeteer/puppeteer/compare/v18.0.1...v18.0.2) (2022-09-19)
+
+
+### Bug Fixes
+
+* mark internal objects ([#8984](https://github.com/puppeteer/puppeteer/issues/8984)) ([181a148](https://github.com/puppeteer/puppeteer/commit/181a148269fce1575f5e37056929ecdec0517586))
+
+## [18.0.1](https://github.com/puppeteer/puppeteer/compare/v18.0.0...v18.0.1) (2022-09-19)
+
+
+### Bug Fixes
+
+* internal lazy params ([#8982](https://github.com/puppeteer/puppeteer/issues/8982)) ([d504597](https://github.com/puppeteer/puppeteer/commit/d5045976a6dd321bbd265b84c2474ff1ad5d0b77))
+
+## [18.0.0](https://github.com/puppeteer/puppeteer/compare/v17.1.3...v18.0.0) (2022-09-19)
+
+
+### ⚠ BREAKING CHANGES
+
+* fix bounding box visibility conditions (#8954)
+
+### Features
+
+* add text query handler ([#8956](https://github.com/puppeteer/puppeteer/issues/8956)) ([633e7cf](https://github.com/puppeteer/puppeteer/commit/633e7cfdf99d42f420d0af381394bd1f6ac7bcd1))
+
+
+### Bug Fixes
+
+* fix bounding box visibility conditions ([#8954](https://github.com/puppeteer/puppeteer/issues/8954)) ([ac9929d](https://github.com/puppeteer/puppeteer/commit/ac9929d80f6f7d4905a39183ae235500e29b4f53))
+* suppress init errors if the target is closed ([#8947](https://github.com/puppeteer/puppeteer/issues/8947)) ([cfaaa5e](https://github.com/puppeteer/puppeteer/commit/cfaaa5e2c07e5f98baeb7de99e303aa840a351e8))
+* use win64 version of chromium when on arm64 windows ([#8927](https://github.com/puppeteer/puppeteer/issues/8927)) ([64843b8](https://github.com/puppeteer/puppeteer/commit/64843b88853210314677ab1b434729513ce615a7))
+
+## [17.1.3](https://github.com/puppeteer/puppeteer/compare/v17.1.2...v17.1.3) (2022-09-08)
+
+
+### Bug Fixes
+
+* FirefoxLauncher should not use BrowserFetcher in puppeteer-core ([#8920](https://github.com/puppeteer/puppeteer/issues/8920)) ([f2e8de7](https://github.com/puppeteer/puppeteer/commit/f2e8de777fc5d547778fdc6cac658add84ed4082)), closes [#8919](https://github.com/puppeteer/puppeteer/issues/8919)
+* linux arm64 check on windows arm ([#8917](https://github.com/puppeteer/puppeteer/issues/8917)) ([f02b926](https://github.com/puppeteer/puppeteer/commit/f02b926245e28b5671087c051dbdbb3165696f08)), closes [#8915](https://github.com/puppeteer/puppeteer/issues/8915)
+
+## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07)
+
+
+### Bug Fixes
+
+* add missing code coverage ranges that span only a single character ([#8911](https://github.com/puppeteer/puppeteer/issues/8911)) ([0c577b9](https://github.com/puppeteer/puppeteer/commit/0c577b9bf8855dc0ccb6098cd43a25c528f6d7f5))
+* add Page.getDefaultTimeout getter ([#8903](https://github.com/puppeteer/puppeteer/issues/8903)) ([3240095](https://github.com/puppeteer/puppeteer/commit/32400954c50cbddc48468ad118c3f8a47653b9d3)), closes [#8901](https://github.com/puppeteer/puppeteer/issues/8901)
+* don't detect project root for puppeteer-core ([#8907](https://github.com/puppeteer/puppeteer/issues/8907)) ([b4f5ea1](https://github.com/puppeteer/puppeteer/commit/b4f5ea1167a60c870194c70d22f5372ada5b7c4c)), closes [#8896](https://github.com/puppeteer/puppeteer/issues/8896)
+* support scale for screenshot clips ([#8908](https://github.com/puppeteer/puppeteer/issues/8908)) ([260e428](https://github.com/puppeteer/puppeteer/commit/260e4282275ab1d05c86e5643e2a02c01f269a9c)), closes [#5329](https://github.com/puppeteer/puppeteer/issues/5329)
+* work around a race in waitForFileChooser ([#8905](https://github.com/puppeteer/puppeteer/issues/8905)) ([053d960](https://github.com/puppeteer/puppeteer/commit/053d960fb593e514e7914d7da9af436afc39a12f)), closes [#6040](https://github.com/puppeteer/puppeteer/issues/6040)
+
+## [17.1.1](https://github.com/puppeteer/puppeteer/compare/v17.1.0...v17.1.1) (2022-09-05)
+
+
+### Bug Fixes
+
+* restore deferred promise debugging ([#8895](https://github.com/puppeteer/puppeteer/issues/8895)) ([7b42250](https://github.com/puppeteer/puppeteer/commit/7b42250c7bb91ac873307acda493726ffc4c54a8))
+
+## [17.1.0](https://github.com/puppeteer/puppeteer/compare/v17.0.0...v17.1.0) (2022-09-02)
+
+
+### Features
+
+* **chromium:** roll to Chromium 106.0.5249.0 (r1036745) ([#8869](https://github.com/puppeteer/puppeteer/issues/8869)) ([6e9a47a](https://github.com/puppeteer/puppeteer/commit/6e9a47a6faa06d241dec0bcf7bcdf49370517008))
+
+
+### Bug Fixes
+
+* allow getting a frame from an elementhandle ([#8875](https://github.com/puppeteer/puppeteer/issues/8875)) ([3732757](https://github.com/puppeteer/puppeteer/commit/3732757450b4363041ccbacc3b236289a156abb0))
+* typos in documentation ([#8858](https://github.com/puppeteer/puppeteer/issues/8858)) ([8d95a9b](https://github.com/puppeteer/puppeteer/commit/8d95a9bc920b98820aa655ad4eb2d8fd9b2b893a))
+* use the timeout setting in waitForFileChooser ([#8856](https://github.com/puppeteer/puppeteer/issues/8856)) ([f477b46](https://github.com/puppeteer/puppeteer/commit/f477b46f212da9206102da695697760eea539f05))
+
+## [17.0.0](https://github.com/puppeteer/puppeteer/compare/v16.2.0...v17.0.0) (2022-08-26)
+
+
+### ⚠ BREAKING CHANGES
+
+* remove `root` from `WaitForSelectorOptions` (#8848)
+* internalize execution context (#8844)
+
+### Bug Fixes
+
+* allow multiple navigations to happen in LifecycleWatcher ([#8826](https://github.com/puppeteer/puppeteer/issues/8826)) ([341b669](https://github.com/puppeteer/puppeteer/commit/341b669a5e45ecbb9ffb0f28c45b520660f27ad2)), closes [#8811](https://github.com/puppeteer/puppeteer/issues/8811)
+* internalize execution context ([#8844](https://github.com/puppeteer/puppeteer/issues/8844)) ([2f33237](https://github.com/puppeteer/puppeteer/commit/2f33237d0443de77d58dca4454b0c9a1d2b57d03))
+* remove `root` from `WaitForSelectorOptions` ([#8848](https://github.com/puppeteer/puppeteer/issues/8848)) ([1155c8e](https://github.com/puppeteer/puppeteer/commit/1155c8eac85b176c3334cc3d98adfe7d943dfbe6))
+* remove deferred promise timeouts ([#8835](https://github.com/puppeteer/puppeteer/issues/8835)) ([202ffce](https://github.com/puppeteer/puppeteer/commit/202ffce0aa4f34dba35fbb8e7d740af16efee35f)), closes [#8832](https://github.com/puppeteer/puppeteer/issues/8832)
+
+## [16.2.0](https://github.com/puppeteer/puppeteer/compare/v16.1.1...v16.2.0) (2022-08-18)
+
+
+### Features
+
+* add Khmer (Cambodian) language support ([#8809](https://github.com/puppeteer/puppeteer/issues/8809)) ([34f8737](https://github.com/puppeteer/puppeteer/commit/34f873721804d57a5faf3eab8ef50340c69ed180))
+
+
+### Bug Fixes
+
+* handle service workers in extensions ([#8807](https://github.com/puppeteer/puppeteer/issues/8807)) ([2a0eefb](https://github.com/puppeteer/puppeteer/commit/2a0eefb99f0ae00dacc9e768a253308c0d18a4c3)), closes [#8800](https://github.com/puppeteer/puppeteer/issues/8800)
+
+## [16.1.1](https://github.com/puppeteer/puppeteer/compare/v16.1.0...v16.1.1) (2022-08-16)
+
+
+### Bug Fixes
+
+* custom sessions should not emit targetcreated events ([#8788](https://github.com/puppeteer/puppeteer/issues/8788)) ([3fad05d](https://github.com/puppeteer/puppeteer/commit/3fad05d333b79f41a7b58582c4ca493200bb5a79)), closes [#8787](https://github.com/puppeteer/puppeteer/issues/8787)
+* deprecate `ExecutionContext` ([#8792](https://github.com/puppeteer/puppeteer/issues/8792)) ([b5da718](https://github.com/puppeteer/puppeteer/commit/b5da718e2e4a2004a36cf23cad555e1fc3b50333))
+* deprecate `root` in `WaitForSelectorOptions` ([#8795](https://github.com/puppeteer/puppeteer/issues/8795)) ([65a5ce8](https://github.com/puppeteer/puppeteer/commit/65a5ce8464c56fcc55e5ac3ed490f31311bbe32a))
+* deprecate `waitForTimeout` ([#8793](https://github.com/puppeteer/puppeteer/issues/8793)) ([8f612d5](https://github.com/puppeteer/puppeteer/commit/8f612d5ff855d48ae4b38bdaacf2a8fbda8e9ce8))
+* make sure there is a check for targets when timeout=0 ([#8765](https://github.com/puppeteer/puppeteer/issues/8765)) ([c23cdb7](https://github.com/puppeteer/puppeteer/commit/c23cdb73a7b113c1dd29f7e4a7a61326422c4080)), closes [#8763](https://github.com/puppeteer/puppeteer/issues/8763)
+* resolve navigation flakiness ([#8768](https://github.com/puppeteer/puppeteer/issues/8768)) ([2580347](https://github.com/puppeteer/puppeteer/commit/2580347b50091d172b2a5591138a2e41ede072fe)), closes [#8644](https://github.com/puppeteer/puppeteer/issues/8644)
+* specify Puppeteer version for Chromium 105.0.5173.0 ([#8766](https://github.com/puppeteer/puppeteer/issues/8766)) ([b5064b7](https://github.com/puppeteer/puppeteer/commit/b5064b7b8bd3bd9eb481b6807c65d9d06d23b9dd))
+* use targetFilter in puppeteer.launch ([#8774](https://github.com/puppeteer/puppeteer/issues/8774)) ([ee2540b](https://github.com/puppeteer/puppeteer/commit/ee2540baefeced44f6b336f2b979af5c3a4cb040)), closes [#8772](https://github.com/puppeteer/puppeteer/issues/8772)
+
+## [16.1.0](https://github.com/puppeteer/puppeteer/compare/v16.0.0...v16.1.0) (2022-08-06)
+
+
+### Features
+
+* use an `xpath` query handler ([#8730](https://github.com/puppeteer/puppeteer/issues/8730)) ([5cf9b4d](https://github.com/puppeteer/puppeteer/commit/5cf9b4de8d50bd056db82bcaa23279b72c9313c5))
+
+
+### Bug Fixes
+
+* resolve target manager init if no existing targets detected ([#8748](https://github.com/puppeteer/puppeteer/issues/8748)) ([8cb5043](https://github.com/puppeteer/puppeteer/commit/8cb5043868f69cdff7f34f1cfe0c003ff09e281b)), closes [#8747](https://github.com/puppeteer/puppeteer/issues/8747)
+* specify the target filter in setDiscoverTargets ([#8742](https://github.com/puppeteer/puppeteer/issues/8742)) ([49193cb](https://github.com/puppeteer/puppeteer/commit/49193cbf1c17f16f0ca59a9fd2ebf306f812f52b))
+
+## [16.0.0](https://github.com/puppeteer/puppeteer/compare/v15.5.0...v16.0.0) (2022-08-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* With Chromium, Puppeteer will now attach to page/iframe targets immediately to allow reliable configuration of targets.
+
+### Features
+
+* add Dockerfile ([#8315](https://github.com/puppeteer/puppeteer/issues/8315)) ([936ed86](https://github.com/puppeteer/puppeteer/commit/936ed8607ec0c3798d2b22b590d0be0ad361a888))
+* detect Firefox in connect() automatically ([#8718](https://github.com/puppeteer/puppeteer/issues/8718)) ([2abd772](https://github.com/puppeteer/puppeteer/commit/2abd772c9c3d2b86deb71541eaac41aceef94356))
+* use CDP's auto-attach mechanism ([#8520](https://github.com/puppeteer/puppeteer/issues/8520)) ([2cbfdeb](https://github.com/puppeteer/puppeteer/commit/2cbfdeb0ca388a45cedfae865266230e1291bd29))
+
+
+### Bug Fixes
+
+* address flakiness in frame handling ([#8688](https://github.com/puppeteer/puppeteer/issues/8688)) ([6f81b23](https://github.com/puppeteer/puppeteer/commit/6f81b23728a511f7b89eaa2b8f850b22d6c4ab24))
+* disable AcceptCHFrame ([#8706](https://github.com/puppeteer/puppeteer/issues/8706)) ([96d9608](https://github.com/puppeteer/puppeteer/commit/96d9608d1de17877414a649a0737661894dd96c8)), closes [#8479](https://github.com/puppeteer/puppeteer/issues/8479)
+* use loaderId to reduce test flakiness ([#8717](https://github.com/puppeteer/puppeteer/issues/8717)) ([d2f6db2](https://github.com/puppeteer/puppeteer/commit/d2f6db20735342bb3f419e85adbd51ed10470044))
+
+## [15.5.0](https://github.com/puppeteer/puppeteer/compare/v15.4.2...v15.5.0) (2022-07-21)
+
+
+### Features
+
+* **chromium:** roll to Chromium 105.0.5173.0 (r1022525) ([#8682](https://github.com/puppeteer/puppeteer/issues/8682)) ([f1b8ad3](https://github.com/puppeteer/puppeteer/commit/f1b8ad3269286800d31818ea4b6b3ee23f7437c3))
+
+## [15.4.2](https://github.com/puppeteer/puppeteer/compare/v15.4.1...v15.4.2) (2022-07-21)
+
+
+### Bug Fixes
+
+* taking a screenshot with null viewport should be possible ([#8680](https://github.com/puppeteer/puppeteer/issues/8680)) ([2abb9f0](https://github.com/puppeteer/puppeteer/commit/2abb9f0c144779d555ecbf337a759440d0282cba)), closes [#8673](https://github.com/puppeteer/puppeteer/issues/8673)
+
+## [15.4.1](https://github.com/puppeteer/puppeteer/compare/v15.4.0...v15.4.1) (2022-07-21)
+
+
+### Bug Fixes
+
+* import URL ([#8670](https://github.com/puppeteer/puppeteer/issues/8670)) ([34ab5ca](https://github.com/puppeteer/puppeteer/commit/34ab5ca50353ffb6a6345a8984b724a6f42fb726))
+
+## [15.4.0](https://github.com/puppeteer/puppeteer/compare/v15.3.2...v15.4.0) (2022-07-13)
+
+
+### Features
+
+* expose the page getter on Frame ([#8657](https://github.com/puppeteer/puppeteer/issues/8657)) ([af08c5c](https://github.com/puppeteer/puppeteer/commit/af08c5c90380c853e8257a51298bfed4b0635779))
+
+
+### Bug Fixes
+
+* ignore *.tsbuildinfo ([#8662](https://github.com/puppeteer/puppeteer/issues/8662)) ([edcdf21](https://github.com/puppeteer/puppeteer/commit/edcdf217cefbf31aee5a2f571abac429dd81f3a0))
+
+## [15.3.2](https://github.com/puppeteer/puppeteer/compare/v15.3.1...v15.3.2) (2022-07-08)
+
+
+### Bug Fixes
+
+* cache dynamic imports ([#8652](https://github.com/puppeteer/puppeteer/issues/8652)) ([1de0383](https://github.com/puppeteer/puppeteer/commit/1de0383abf6be31cf06faede3e59b087a2958227))
+* expose a RemoteObject getter ([#8642](https://github.com/puppeteer/puppeteer/issues/8642)) ([d0c4291](https://github.com/puppeteer/puppeteer/commit/d0c42919956bd36ad7993a0fc1de86e886e39f62)), closes [#8639](https://github.com/puppeteer/puppeteer/issues/8639)
+* **page:** fix page.#scrollIntoViewIfNeeded method ([#8631](https://github.com/puppeteer/puppeteer/issues/8631)) ([b47f066](https://github.com/puppeteer/puppeteer/commit/b47f066c2c068825e3b65cfe17b6923c77ad30b9))
+
+## [15.3.1](https://github.com/puppeteer/puppeteer/compare/v15.3.0...v15.3.1) (2022-07-06)
+
+
+### Bug Fixes
+
+* extends `ElementHandle` to `Node`s ([#8552](https://github.com/puppeteer/puppeteer/issues/8552)) ([5ff205d](https://github.com/puppeteer/puppeteer/commit/5ff205dc8b659eb8864b4b1862105d21dd334c8f))
+
+## [15.3.0](https://github.com/puppeteer/puppeteer/compare/v15.2.0...v15.3.0) (2022-07-01)
+
+
+### Features
+
+* add documentation ([#8593](https://github.com/puppeteer/puppeteer/issues/8593)) ([066f440](https://github.com/puppeteer/puppeteer/commit/066f440ba7bdc9aca9423d7205adf36f2858bd78))
+
+
+### Bug Fixes
+
+* remove unused imports ([#8613](https://github.com/puppeteer/puppeteer/issues/8613)) ([0cf4832](https://github.com/puppeteer/puppeteer/commit/0cf4832878731ffcfc84570315f326eb851d7629))
+
+## [15.2.0](https://github.com/puppeteer/puppeteer/compare/v15.1.1...v15.2.0) (2022-06-29)
+
+
+### Features
+
+* add fromSurface option to page.screenshot ([#8496](https://github.com/puppeteer/puppeteer/issues/8496)) ([79e1198](https://github.com/puppeteer/puppeteer/commit/79e11985ba44b72b1ad6b8cd861fe316f1945e64))
+* export public types only ([#8584](https://github.com/puppeteer/puppeteer/issues/8584)) ([7001322](https://github.com/puppeteer/puppeteer/commit/7001322cd1cf9f77ee2c370d50a6707e7aaad72d))
+
+
+### Bug Fixes
+
+* clean up tmp profile dirs when browser is closed ([#8580](https://github.com/puppeteer/puppeteer/issues/8580)) ([9787a1d](https://github.com/puppeteer/puppeteer/commit/9787a1d8df7768017b36d42327faab402695c4bb))
+
+## [15.1.1](https://github.com/puppeteer/puppeteer/compare/v15.1.0...v15.1.1) (2022-06-25)
+
+
+### Bug Fixes
+
+* export `ElementHandle` ([e0198a7](https://github.com/puppeteer/puppeteer/commit/e0198a79e06c8bb72dde554db0246a3db5fec4c2))
+
+## [15.1.0](https://github.com/puppeteer/puppeteer/compare/v15.0.2...v15.1.0) (2022-06-24)
+
+
+### Features
+
+* **chromium:** roll to Chromium 104.0.5109.0 (r1011831) ([#8569](https://github.com/puppeteer/puppeteer/issues/8569)) ([fb7d31e](https://github.com/puppeteer/puppeteer/commit/fb7d31e3698428560e1f654d33782d241192f48f))
+
+## [15.0.2](https://github.com/puppeteer/puppeteer/compare/v15.0.1...v15.0.2) (2022-06-24)
+
+
+### Bug Fixes
+
+* CSS coverage should work with empty stylesheets ([#8570](https://github.com/puppeteer/puppeteer/issues/8570)) ([383e855](https://github.com/puppeteer/puppeteer/commit/383e8558477fae7708734ab2160ef50f385e2983)), closes [#8535](https://github.com/puppeteer/puppeteer/issues/8535)
+
+## [15.0.1](https://github.com/puppeteer/puppeteer/compare/v15.0.0...v15.0.1) (2022-06-24)
+
+
+### Bug Fixes
+
+* infer unioned handles ([#8562](https://github.com/puppeteer/puppeteer/issues/8562)) ([8100cbb](https://github.com/puppeteer/puppeteer/commit/8100cbb29569541541f61001983efb9a80d89890))
+
+## [15.0.0](https://github.com/puppeteer/puppeteer/compare/v14.4.1...v15.0.0) (2022-06-23)
+
+
+### ⚠ BREAKING CHANGES
+
+* type inference for evaluation types (#8547)
+
+### Features
+
+* add experimental `client` to `HTTPRequest` ([#8556](https://github.com/puppeteer/puppeteer/issues/8556)) ([ec79f3a](https://github.com/puppeteer/puppeteer/commit/ec79f3a58a44c9ea60a82f9cd2df4c8f19e82ab8))
+* type inference for evaluation types ([#8547](https://github.com/puppeteer/puppeteer/issues/8547)) ([26c3acb](https://github.com/puppeteer/puppeteer/commit/26c3acbb0795eb66f29479f442e156832f794f01))
+
+## [14.4.1](https://github.com/puppeteer/puppeteer/compare/v14.4.0...v14.4.1) (2022-06-17)
+
+
+### Bug Fixes
+
+* avoid `instanceof Object` check in `isErrorLike` ([#8527](https://github.com/puppeteer/puppeteer/issues/8527)) ([6cd5cd0](https://github.com/puppeteer/puppeteer/commit/6cd5cd043997699edca6e3458f90adc1118cf4a5))
+* export `devices`, `errors`, and more ([cba58a1](https://github.com/puppeteer/puppeteer/commit/cba58a12c4e2043f6a5acf7d4754e4a7b7f6e198))
+
+## [14.4.0](https://github.com/puppeteer/puppeteer/compare/v14.3.0...v14.4.0) (2022-06-13)
+
+
+### Features
+
+* export puppeteer methods ([#8493](https://github.com/puppeteer/puppeteer/issues/8493)) ([465a7c4](https://github.com/puppeteer/puppeteer/commit/465a7c405f01fcef99380ffa69d86042a1f5618f))
+* support node-like environments ([#8490](https://github.com/puppeteer/puppeteer/issues/8490)) ([f64ec20](https://github.com/puppeteer/puppeteer/commit/f64ec2051b9b2d12225abba6ffe9551da9751bf7))
+
+
+### Bug Fixes
+
+* parse empty options in \<select\> ([#8489](https://github.com/puppeteer/puppeteer/issues/8489)) ([b30f3f4](https://github.com/puppeteer/puppeteer/commit/b30f3f44cdabd9545c4661cd755b9d49e5c144cd))
+* use error-like ([#8504](https://github.com/puppeteer/puppeteer/issues/8504)) ([4d35990](https://github.com/puppeteer/puppeteer/commit/4d359906a44e4ddd5ec54a523cfd9076048d3433))
+* use OS-independent abs. path check ([#8505](https://github.com/puppeteer/puppeteer/issues/8505)) ([bfd4e68](https://github.com/puppeteer/puppeteer/commit/bfd4e68f25bec6e00fd5cbf261813f8297d362ee))
+
+## [14.3.0](https://github.com/puppeteer/puppeteer/compare/v14.2.1...v14.3.0) (2022-06-07)
+
+
+### Features
+
+* use absolute URL for EVALUATION_SCRIPT_URL ([#8481](https://github.com/puppeteer/puppeteer/issues/8481)) ([e142560](https://github.com/puppeteer/puppeteer/commit/e14256010d2d84d613cd3c6e7999b0705115d4bf)), closes [#8424](https://github.com/puppeteer/puppeteer/issues/8424)
+
+
+### Bug Fixes
+
+* don't throw on bad access ([#8472](https://github.com/puppeteer/puppeteer/issues/8472)) ([e837866](https://github.com/puppeteer/puppeteer/commit/e8378666c671e5703aec4f52912de2aac94e1828))
+* Kill browser process when killing process group fails ([#8477](https://github.com/puppeteer/puppeteer/issues/8477)) ([7dc8e37](https://github.com/puppeteer/puppeteer/commit/7dc8e37a23d025bb2c31efb9c060c7f6e00179b4))
+* only lookup `localhost` for DNS lookups ([1b025b4](https://github.com/puppeteer/puppeteer/commit/1b025b4c8466fe64da0fa2050eaa02b7764770b1))
+* robustly check for launch executable ([#8468](https://github.com/puppeteer/puppeteer/issues/8468)) ([b54dc55](https://github.com/puppeteer/puppeteer/commit/b54dc55f7622ee2b75afd3bd9fe118dd2f144f40))
+
+## [14.2.1](https://github.com/puppeteer/puppeteer/compare/v14.2.0...v14.2.1) (2022-06-02)
+
+
+### Bug Fixes
+
+* use isPageTargetCallback in Browser::pages() ([#8460](https://github.com/puppeteer/puppeteer/issues/8460)) ([5c9050a](https://github.com/puppeteer/puppeteer/commit/5c9050aea0fe8d57114130fe38bd33ed2b4955d6))
+
+## [14.2.0](https://github.com/puppeteer/puppeteer/compare/v14.1.2...v14.2.0) (2022-06-01)
+
+
+### Features
+
+* **chromium:** roll to Chromium 103.0.5059.0 (r1002410) ([#8410](https://github.com/puppeteer/puppeteer/issues/8410)) ([54efc2c](https://github.com/puppeteer/puppeteer/commit/54efc2c949be1d6ef22f4d2630620e33d14d2597))
+* support node 18 ([#8447](https://github.com/puppeteer/puppeteer/issues/8447)) ([f2d8276](https://github.com/puppeteer/puppeteer/commit/f2d8276d6e745a7547b8ce54c3f50934bb70de0b))
+* use strict typescript ([#8401](https://github.com/puppeteer/puppeteer/issues/8401)) ([b4e751f](https://github.com/puppeteer/puppeteer/commit/b4e751f29cb6fd4c3cc41fe702de83721f0eb6dc))
+
+
+### Bug Fixes
+
+* multiple same request event listener ([#8404](https://github.com/puppeteer/puppeteer/issues/8404)) ([9211015](https://github.com/puppeteer/puppeteer/commit/92110151d9a33f26abc07bc805f4f2f3943697a0))
+* NodeNext incompatibility in package.json ([#8445](https://github.com/puppeteer/puppeteer/issues/8445)) ([c4898a7](https://github.com/puppeteer/puppeteer/commit/c4898a7a2e69681baac55366848da6688f0d8790))
+* process documentation during publishing ([#8433](https://github.com/puppeteer/puppeteer/issues/8433)) ([d111d19](https://github.com/puppeteer/puppeteer/commit/d111d19f788d88d984dcf4ad7542f59acd2f4c1e))
+
+## [14.1.2](https://github.com/puppeteer/puppeteer/compare/v14.1.1...v14.1.2) (2022-05-30)
+
+
+### Bug Fixes
+
+* do not use loaderId for lifecycle events ([#8395](https://github.com/puppeteer/puppeteer/issues/8395)) ([c96c915](https://github.com/puppeteer/puppeteer/commit/c96c915b535dcf414038677bd3d3ed6b980a4901))
+* fix release-please bot ([#8400](https://github.com/puppeteer/puppeteer/issues/8400)) ([5c235c7](https://github.com/puppeteer/puppeteer/commit/5c235c701fc55380f09d09ac2cf63f2c94b60e3d))
+* use strict TS in Input.ts ([#8392](https://github.com/puppeteer/puppeteer/issues/8392)) ([af92a24](https://github.com/puppeteer/puppeteer/commit/af92a24ba9fc8efea1ba41f96d87515cf760da65))
+
+### [14.1.1](https://github.com/puppeteer/puppeteer/compare/v14.1.0...v14.1.1) (2022-05-19)
+
+
+### Bug Fixes
+
+* kill browser process when 'taskkill' fails on Windows ([#8352](https://github.com/puppeteer/puppeteer/issues/8352)) ([dccfadb](https://github.com/puppeteer/puppeteer/commit/dccfadb90e8947cae3f33d7a209b6f5752f97b46))
+* only check loading iframe in lifecycling ([#8348](https://github.com/puppeteer/puppeteer/issues/8348)) ([7438030](https://github.com/puppeteer/puppeteer/commit/74380303ac6cc6e2d84948a10920d56e665ccebe))
+* recompile before funit and unit commands ([#8363](https://github.com/puppeteer/puppeteer/issues/8363)) ([8735b78](https://github.com/puppeteer/puppeteer/commit/8735b784ba7838c1002b521a7f9f23bb27263d03)), closes [#8362](https://github.com/puppeteer/puppeteer/issues/8362)
+
+## [14.1.0](https://github.com/puppeteer/puppeteer/compare/v14.0.0...v14.1.0) (2022-05-13)
+
+
+### Features
+
+* add waitForXPath to ElementHandle ([#8329](https://github.com/puppeteer/puppeteer/issues/8329)) ([7eaadaf](https://github.com/puppeteer/puppeteer/commit/7eaadafe197279a7d1753e7274d2e24dfc11abdf))
+* allow handling other targets as pages internally ([#8336](https://github.com/puppeteer/puppeteer/issues/8336)) ([3b66a2c](https://github.com/puppeteer/puppeteer/commit/3b66a2c47ee36785a6a72c9afedd768fab3d040a))
+
+
+### Bug Fixes
+
+* disable AvoidUnnecessaryBeforeUnloadCheckSync to fix navigations ([#8330](https://github.com/puppeteer/puppeteer/issues/8330)) ([4854ad5](https://github.com/puppeteer/puppeteer/commit/4854ad5b15c9bdf93c06dcb758393e7cbacd7469))
+* If currentNode and root are the same, do not include them in the result ([#8332](https://github.com/puppeteer/puppeteer/issues/8332)) ([a61144d](https://github.com/puppeteer/puppeteer/commit/a61144d43780b5c32197427d7682b9b6c433f2bb))
+
+## [14.0.0](https://github.com/puppeteer/puppeteer/compare/v13.7.0...v14.0.0) (2022-05-09)
+
+
+### ⚠ BREAKING CHANGES
+
+* strict mode fixes for HTTPRequest/Response classes (#8297)
+* Node 12 is no longer supported.
+
+### Features
+
+* add support for Apple Silicon chromium builds ([#7546](https://github.com/puppeteer/puppeteer/issues/7546)) ([baa017d](https://github.com/puppeteer/puppeteer/commit/baa017db92b1fecf2e3584d5b3161371ae60f55b)), closes [#6622](https://github.com/puppeteer/puppeteer/issues/6622)
+* **chromium:** roll to Chromium 102.0.5002.0 (r991974) ([#8319](https://github.com/puppeteer/puppeteer/issues/8319)) ([be4c930](https://github.com/puppeteer/puppeteer/commit/be4c930c60164f681a966d0f8cb745f6c263fe2b))
+* support ES modules ([#8306](https://github.com/puppeteer/puppeteer/issues/8306)) ([6841bd6](https://github.com/puppeteer/puppeteer/commit/6841bd68d85e3b3952c5e7ce454ac4d23f84262d))
+
+
+### Bug Fixes
+
+* apparent typo SUPPORTER_PLATFORMS ([#8294](https://github.com/puppeteer/puppeteer/issues/8294)) ([e09287f](https://github.com/puppeteer/puppeteer/commit/e09287f4e9a1ff3c637dd165d65f221394970e2c))
+* make sure inner OOPIFs can be attached to ([#8304](https://github.com/puppeteer/puppeteer/issues/8304)) ([5539598](https://github.com/puppeteer/puppeteer/commit/553959884f4edb4deab760fa8ca38fc1c85c05c5))
+* strict mode fixes for HTTPRequest/Response classes ([#8297](https://github.com/puppeteer/puppeteer/issues/8297)) ([2804ae8](https://github.com/puppeteer/puppeteer/commit/2804ae8cdbc4c90bf942510bce656275a2d409e1)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769)
+* tests failing in headful ([#8273](https://github.com/puppeteer/puppeteer/issues/8273)) ([e841d7f](https://github.com/puppeteer/puppeteer/commit/e841d7f9f3f407c02dbc48e107b545b91db104e6))
+
+
+* drop Node 12 support ([#8299](https://github.com/puppeteer/puppeteer/issues/8299)) ([274bd6b](https://github.com/puppeteer/puppeteer/commit/274bd6b3b98c305ed014909d8053e4c54187971b))
+
+## [13.7.0](https://github.com/puppeteer/puppeteer/compare/v13.6.0...v13.7.0) (2022-04-28)
+
+
+### Features
+
+* add `back` and `forward` mouse buttons ([#8284](https://github.com/puppeteer/puppeteer/issues/8284)) ([7a51bff](https://github.com/puppeteer/puppeteer/commit/7a51bff47f6436fc29d0df7eb74f12f69102ca5b))
+* support chrome headless mode ([#8260](https://github.com/puppeteer/puppeteer/issues/8260)) ([1308d9a](https://github.com/puppeteer/puppeteer/commit/1308d9aa6a5920b20da02dca8db03c63e43c8b84))
+
+
+### Bug Fixes
+
+* doc typo ([#8263](https://github.com/puppeteer/puppeteer/issues/8263)) ([952a2ae](https://github.com/puppeteer/puppeteer/commit/952a2ae0bc4f059f8e8b4d1de809d0a486a74551))
+* use different test names for browser specific tests in launcher.spec.ts ([#8250](https://github.com/puppeteer/puppeteer/issues/8250)) ([c6cf1a9](https://github.com/puppeteer/puppeteer/commit/c6cf1a9f27621c8a619cfbdc9d0821541768ac94))
+
+## [13.6.0](https://github.com/puppeteer/puppeteer/compare/v13.5.2...v13.6.0) (2022-04-19)
+
+
+### Features
+
+* **chromium:** roll to Chromium 101.0.4950.0 (r982053) ([#8213](https://github.com/puppeteer/puppeteer/issues/8213)) ([ec74bd8](https://github.com/puppeteer/puppeteer/commit/ec74bd811d9b7fbaf600068e86f13a63d7b0bc6f))
+* respond multiple headers with same key ([#8183](https://github.com/puppeteer/puppeteer/issues/8183)) ([c1dcd85](https://github.com/puppeteer/puppeteer/commit/c1dcd857e3bc17769f02474a41bbedee01f471dc))
+
+
+### Bug Fixes
+
+* also kill Firefox when temporary profile is used ([#8233](https://github.com/puppeteer/puppeteer/issues/8233)) ([b6504d7](https://github.com/puppeteer/puppeteer/commit/b6504d7186336a2fc0b41c3878c843b7409ba5fb))
+* consider existing frames when waiting for a frame ([#8200](https://github.com/puppeteer/puppeteer/issues/8200)) ([0955225](https://github.com/puppeteer/puppeteer/commit/0955225b51421663288523a3dfb63103b51775b4))
+* disable bfcache in the launcher ([#8196](https://github.com/puppeteer/puppeteer/issues/8196)) ([9ac7318](https://github.com/puppeteer/puppeteer/commit/9ac7318506ac858b3465e9b4ede8ad75fbbcee11)), closes [#8182](https://github.com/puppeteer/puppeteer/issues/8182)
+* enable page.spec event handler test for firefox ([#8214](https://github.com/puppeteer/puppeteer/issues/8214)) ([2b45027](https://github.com/puppeteer/puppeteer/commit/2b45027d256f85f21a0c824183696b237e00ad33))
+* forget queuedEventGroup when emitting response in responseReceivedExtraInfo ([#8234](https://github.com/puppeteer/puppeteer/issues/8234)) ([#8239](https://github.com/puppeteer/puppeteer/issues/8239)) ([91a8e73](https://github.com/puppeteer/puppeteer/commit/91a8e73b1196e4128b1e7c25e08080f2faaf3cf7))
+* forget request will be sent from the _requestWillBeSentMap list. ([#8226](https://github.com/puppeteer/puppeteer/issues/8226)) ([4b786c9](https://github.com/puppeteer/puppeteer/commit/4b786c904cbfe3f059322292f3b788b8a5ebd9bf))
+* ignore favicon requests in page.spec event handler tests ([#8208](https://github.com/puppeteer/puppeteer/issues/8208)) ([04e5c88](https://github.com/puppeteer/puppeteer/commit/04e5c889973432c6163a8539cdec23c0e8726bff))
+* **network.spec.ts:** typo in the word should ([#8223](https://github.com/puppeteer/puppeteer/issues/8223)) ([e93faad](https://github.com/puppeteer/puppeteer/commit/e93faadc21b7fcb1e03b69c451c28b769f9cde51))
+
+### [13.5.2](https://github.com/puppeteer/puppeteer/compare/v13.5.1...v13.5.2) (2022-03-31)
+
+
+### Bug Fixes
+
+* chromium downloading hung at 99% ([#8169](https://github.com/puppeteer/puppeteer/issues/8169)) ([8f13470](https://github.com/puppeteer/puppeteer/commit/8f13470af06045857f32496f03e77b14f3ecff98))
+* get extra headers from Fetch.requestPaused event ([#8162](https://github.com/puppeteer/puppeteer/issues/8162)) ([37ede68](https://github.com/puppeteer/puppeteer/commit/37ede6877017a8dc6c946a3dff4ec6d79c3ebc59))
+
+### [13.5.1](https://github.com/puppeteer/puppeteer/compare/v13.5.0...v13.5.1) (2022-03-09)
+
+
+### Bug Fixes
+
+* waitForNavigation in OOPIFs ([#8117](https://github.com/puppeteer/puppeteer/issues/8117)) ([34775e5](https://github.com/puppeteer/puppeteer/commit/34775e58316be49d8bc5a13209a1f570bc66b448))
+
+## [13.5.0](https://github.com/puppeteer/puppeteer/compare/v13.4.1...v13.5.0) (2022-03-07)
+
+
+### Features
+
+* **chromium:** roll to Chromium 100.0.4889.0 (r970485) ([#8108](https://github.com/puppeteer/puppeteer/issues/8108)) ([d12f427](https://github.com/puppeteer/puppeteer/commit/d12f42754f7013b5ec0a2198cf2d9cf945d3cb38))
+
+
+### Bug Fixes
+
+* Inherit browser-level proxy settings from incognito context ([#7770](https://github.com/puppeteer/puppeteer/issues/7770)) ([3feca32](https://github.com/puppeteer/puppeteer/commit/3feca325a9472ee36f7e866ebe375c7f083e0e36))
+* **page:** page.createIsolatedWorld error catching has been added ([#7848](https://github.com/puppeteer/puppeteer/issues/7848)) ([309e8b8](https://github.com/puppeteer/puppeteer/commit/309e8b80da0519327bc37b44a3ebb6f2e2d357a7))
+* **tests:** ensure all tests honour BINARY envvar ([#8092](https://github.com/puppeteer/puppeteer/issues/8092)) ([3b8b9ad](https://github.com/puppeteer/puppeteer/commit/3b8b9adde5d18892af96329b6f9303979f9c04f5))
+
+### [13.4.1](https://github.com/puppeteer/puppeteer/compare/v13.4.0...v13.4.1) (2022-03-01)
+
+
+### Bug Fixes
+
+* regression in --user-data-dir handling ([#8060](https://github.com/puppeteer/puppeteer/issues/8060)) ([85decdc](https://github.com/puppeteer/puppeteer/commit/85decdc28d7d2128e6d2946a72f4d99dd5dbb48a))
+
+## [13.4.0](https://github.com/puppeteer/puppeteer/compare/v13.3.2...v13.4.0) (2022-02-22)
+
+
+### Features
+
+* add support for async waitForTarget ([#7885](https://github.com/puppeteer/puppeteer/issues/7885)) ([dbf0639](https://github.com/puppeteer/puppeteer/commit/dbf0639822d0b2736993de52c0bfe1dbf4e58f25))
+* export `Frame._client` through getter ([#8041](https://github.com/puppeteer/puppeteer/issues/8041)) ([e9278fc](https://github.com/puppeteer/puppeteer/commit/e9278fcfcffe2558de63ce7542483445bcb6e74f))
+* **HTTPResponse:** expose timing information ([#8025](https://github.com/puppeteer/puppeteer/issues/8025)) ([30b3d49](https://github.com/puppeteer/puppeteer/commit/30b3d49b0de46d812b7485e708174a07c73dbdd0))
+
+
+### Bug Fixes
+
+* change kill to signal the whole process group to terminate ([#6859](https://github.com/puppeteer/puppeteer/issues/6859)) ([0eb9c78](https://github.com/puppeteer/puppeteer/commit/0eb9c7861717ebba7012c03e76b7a46063e4e5dd))
+* element screenshot issue in headful mode ([#8018](https://github.com/puppeteer/puppeteer/issues/8018)) ([5346e70](https://github.com/puppeteer/puppeteer/commit/5346e70ffc15b33c1949657cf1b465f1acc5d84d)), closes [#7999](https://github.com/puppeteer/puppeteer/issues/7999)
+* ensure dom binding is not called after detach ([#8024](https://github.com/puppeteer/puppeteer/issues/8024)) ([5c308b0](https://github.com/puppeteer/puppeteer/commit/5c308b0704123736ddb085f97596c201ea18cf4a)), closes [#7814](https://github.com/puppeteer/puppeteer/issues/7814)
+* use both __dirname and require.resolve to support different bundlers ([#8046](https://github.com/puppeteer/puppeteer/issues/8046)) ([e6a6295](https://github.com/puppeteer/puppeteer/commit/e6a6295d9a7480bb59ee58a2cc7785171fa0fa2c)), closes [#8044](https://github.com/puppeteer/puppeteer/issues/8044)
+
+### [13.3.2](https://github.com/puppeteer/puppeteer/compare/v13.3.1...v13.3.2) (2022-02-14)
+
+
+### Bug Fixes
+
+* always use ENV executable path when present ([#7985](https://github.com/puppeteer/puppeteer/issues/7985)) ([6d6ea9b](https://github.com/puppeteer/puppeteer/commit/6d6ea9bf59daa3fb851b3da8baa27887e0aa2c28))
+* use require.resolve instead of __dirname ([#8003](https://github.com/puppeteer/puppeteer/issues/8003)) ([bbb186d](https://github.com/puppeteer/puppeteer/commit/bbb186d88cb99e4914299c983c822fa41a80f356))
+
+### [13.3.1](https://github.com/puppeteer/puppeteer/compare/v13.3.0...v13.3.1) (2022-02-10)
+
+
+### Bug Fixes
+
+* **puppeteer:** revert: esm modules ([#7986](https://github.com/puppeteer/puppeteer/issues/7986)) ([179eded](https://github.com/puppeteer/puppeteer/commit/179ededa1400c35c1f2edc015548e0f2a1bcee14))
+
+## [13.3.0](https://github.com/puppeteer/puppeteer/compare/v13.2.0...v13.3.0) (2022-02-09)
+
+
+### Features
+
+* **puppeteer:** export esm modules in package.json ([#7964](https://github.com/puppeteer/puppeteer/issues/7964)) ([523b487](https://github.com/puppeteer/puppeteer/commit/523b487e8802824cecff86d256b4f7dbc4c47c8a))
+
+## [13.2.0](https://github.com/puppeteer/puppeteer/compare/v13.1.3...v13.2.0) (2022-02-07)
+
+
+### Features
+
+* add more models to DeviceDescriptors ([#7904](https://github.com/puppeteer/puppeteer/issues/7904)) ([6a655cb](https://github.com/puppeteer/puppeteer/commit/6a655cb647e12eaf1055be0b298908d83bebac25))
+* **chromium:** roll to Chromium 99.0.4844.16 (r961656) ([#7960](https://github.com/puppeteer/puppeteer/issues/7960)) ([96c3f94](https://github.com/puppeteer/puppeteer/commit/96c3f943b2f6e26bd871ecfcce71b6a33e214ebf))
+
+
+### Bug Fixes
+
+* make projectRoot optional in Puppeteer and launchers ([#7967](https://github.com/puppeteer/puppeteer/issues/7967)) ([9afdc63](https://github.com/puppeteer/puppeteer/commit/9afdc6300b80f01091dc4cb42d4ebe952c7d60f0))
+* migrate more files to strict-mode TypeScript ([#7950](https://github.com/puppeteer/puppeteer/issues/7950)) ([aaac8d9](https://github.com/puppeteer/puppeteer/commit/aaac8d9c44327a2c503ffd6c97b7f21e8010c3e4))
+* typos in documentation ([#7968](https://github.com/puppeteer/puppeteer/issues/7968)) ([41ab4e9](https://github.com/puppeteer/puppeteer/commit/41ab4e9127df64baa6c43ecde2f7ddd702ba7b0c))
+
+### [13.1.3](https://github.com/puppeteer/puppeteer/compare/v13.1.2...v13.1.3) (2022-01-31)
+
+
+### Bug Fixes
+
+* issue with reading versions.js in doclint ([#7940](https://github.com/puppeteer/puppeteer/issues/7940)) ([06ba963](https://github.com/puppeteer/puppeteer/commit/06ba9632a4c63859244068d32c312817d90daf63))
+* make more files work in strict-mode TypeScript ([#7936](https://github.com/puppeteer/puppeteer/issues/7936)) ([0636513](https://github.com/puppeteer/puppeteer/commit/0636513e34046f4d40b5e88beb2b18b16dab80aa))
+* page.pdf producing an invalid pdf ([#7868](https://github.com/puppeteer/puppeteer/issues/7868)) ([afea509](https://github.com/puppeteer/puppeteer/commit/afea509544fb99bfffe5b0bebe6f3575c53802f0)), closes [#7757](https://github.com/puppeteer/puppeteer/issues/7757)
+
+### [13.1.2](https://github.com/puppeteer/puppeteer/compare/v13.1.1...v13.1.2) (2022-01-25)
+
+
+### Bug Fixes
+
+* **package.json:** update node-fetch package ([#7924](https://github.com/puppeteer/puppeteer/issues/7924)) ([e4c48d3](https://github.com/puppeteer/puppeteer/commit/e4c48d3b8c2a812752094ed8163e4f2f32c4b6cb))
+* types in Browser.ts to be compatible with strict mode Typescript ([#7918](https://github.com/puppeteer/puppeteer/issues/7918)) ([a8ec0aa](https://github.com/puppeteer/puppeteer/commit/a8ec0aadc9c90d224d568d9e418d14261e6e85b1)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769)
+* types in Connection.ts to be compatible with strict mode Typescript ([#7919](https://github.com/puppeteer/puppeteer/issues/7919)) ([d80d602](https://github.com/puppeteer/puppeteer/commit/d80d6027ea8e1b7fcdaf045398629cf8e6512658)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769)
+
+### [13.1.1](https://github.com/puppeteer/puppeteer/compare/v13.1.0...v13.1.1) (2022-01-18)
+
+
+### Bug Fixes
+
+* use content box for OOPIF offset calculations ([#7911](https://github.com/puppeteer/puppeteer/issues/7911)) ([344feb5](https://github.com/puppeteer/puppeteer/commit/344feb53c28ce018a4c600d408468f6d9d741eee))
+
+## [13.1.0](https://github.com/puppeteer/puppeteer/compare/v13.0.1...v13.1.0) (2022-01-17)
+
+
+### Features
+
+* **chromium:** roll to Chromium 98.0.4758.0 (r950341) ([#7907](https://github.com/puppeteer/puppeteer/issues/7907)) ([a55c86f](https://github.com/puppeteer/puppeteer/commit/a55c86fac504b5e89ba23735fb3a1b1d54a4e1e5))
+
+
+### Bug Fixes
+
+* apply OOPIF offsets to bounding box and box model calls ([#7906](https://github.com/puppeteer/puppeteer/issues/7906)) ([a566263](https://github.com/puppeteer/puppeteer/commit/a566263ba28e58ff648bffbdb628606f75d5876f))
+* correctly compute clickable points for elements inside OOPIFs ([#7900](https://github.com/puppeteer/puppeteer/issues/7900)) ([486bbe0](https://github.com/puppeteer/puppeteer/commit/486bbe010d5ee5c446d9e8daf61a080232379c3f)), closes [#7849](https://github.com/puppeteer/puppeteer/issues/7849)
+* error for pre-existing OOPIFs ([#7899](https://github.com/puppeteer/puppeteer/issues/7899)) ([d7937b8](https://github.com/puppeteer/puppeteer/commit/d7937b806d331bf16c2016aaf16e932b1334eac8)), closes [#7844](https://github.com/puppeteer/puppeteer/issues/7844) [#7896](https://github.com/puppeteer/puppeteer/issues/7896)
+
+### [13.0.1](https://github.com/puppeteer/puppeteer/compare/v13.0.0...v13.0.1) (2021-12-22)
+
+
+### Bug Fixes
+
+* disable a test failing on Firefox ([#7846](https://github.com/puppeteer/puppeteer/issues/7846)) ([36207c5](https://github.com/puppeteer/puppeteer/commit/36207c5efe8ca21f4b3fc5b00212700326a701d2))
+* make sure ElementHandle.waitForSelector is evaluated in the right context ([#7843](https://github.com/puppeteer/puppeteer/issues/7843)) ([8d8e874](https://github.com/puppeteer/puppeteer/commit/8d8e874b072b17fc763f33d08e51c046b7435244))
+* predicate arguments for waitForFunction ([#7845](https://github.com/puppeteer/puppeteer/issues/7845)) ([1c44551](https://github.com/puppeteer/puppeteer/commit/1c44551f1b5bb19455b4a1eb7061715717ec880e)), closes [#7836](https://github.com/puppeteer/puppeteer/issues/7836)
+
+## [13.0.0](https://github.com/puppeteer/puppeteer/compare/v12.0.1...v13.0.0) (2021-12-10)
+
+
+### ⚠ BREAKING CHANGES
+
+* typo in 'already-handled' constant of the request interception API (#7813)
+
+### Features
+
+* expose HTTPRequest intercept resolution state and clarify docs ([#7796](https://github.com/puppeteer/puppeteer/issues/7796)) ([dc23b75](https://github.com/puppeteer/puppeteer/commit/dc23b7535cb958c00d1eecfe85b4ee26e52e2e39))
+* implement Element.waitForSelector ([#7825](https://github.com/puppeteer/puppeteer/issues/7825)) ([c034294](https://github.com/puppeteer/puppeteer/commit/c03429444d05b39549489ad3da67d93b2be59f51))
+
+
+### Bug Fixes
+
+* handle multiple/duplicate Fetch.requestPaused events ([#7802](https://github.com/puppeteer/puppeteer/issues/7802)) ([636b086](https://github.com/puppeteer/puppeteer/commit/636b0863a169da132e333eb53b17eb2601daabe6)), closes [#7475](https://github.com/puppeteer/puppeteer/issues/7475) [#6696](https://github.com/puppeteer/puppeteer/issues/6696) [#7225](https://github.com/puppeteer/puppeteer/issues/7225)
+* revert "feat(typescript): allow using puppeteer without dom lib" ([02c9af6](https://github.com/puppeteer/puppeteer/commit/02c9af62d64060a83f53368640f343ae2e30e38a)), closes [#6998](https://github.com/puppeteer/puppeteer/issues/6998)
+* typo in 'already-handled' constant of the request interception API ([#7813](https://github.com/puppeteer/puppeteer/issues/7813)) ([8242422](https://github.com/puppeteer/puppeteer/commit/824242246de9e158aacb85f71350a79cb386ed92)), closes [#7745](https://github.com/puppeteer/puppeteer/issues/7745) [#7747](https://github.com/puppeteer/puppeteer/issues/7747) [#7780](https://github.com/puppeteer/puppeteer/issues/7780)
+
+### [12.0.1](https://github.com/puppeteer/puppeteer/compare/v12.0.0...v12.0.1) (2021-11-29)
+
+
+### Bug Fixes
+
+* handle extraInfo events even if event.hasExtraInfo === false ([#7808](https://github.com/puppeteer/puppeteer/issues/7808)) ([6ee2feb](https://github.com/puppeteer/puppeteer/commit/6ee2feb1eafdd399f0af50cdc4517f21bcb55121)), closes [#7805](https://github.com/puppeteer/puppeteer/issues/7805)
+
+## [12.0.0](https://github.com/puppeteer/puppeteer/compare/v11.0.0...v12.0.0) (2021-11-26)
+
+
+### ⚠ BREAKING CHANGES
+
+* **chromium:** roll to Chromium 97.0.4692.0 (r938248)
+
+### Features
+
+* **chromium:** roll to Chromium 97.0.4692.0 (r938248) ([ac162c5](https://github.com/puppeteer/puppeteer/commit/ac162c561ee43dd69eff38e1b354a41bb42c9eba)), closes [#7458](https://github.com/puppeteer/puppeteer/issues/7458)
+* support for custom user data (profile) directory for Firefox ([#7684](https://github.com/puppeteer/puppeteer/issues/7684)) ([790c7a0](https://github.com/puppeteer/puppeteer/commit/790c7a0eb92291efebaa37e80c72f5cb5f46bbdb))
+
+
+### Bug Fixes
+
+* **ariaqueryhandler:** allow single quotes in aria attribute selector ([#7750](https://github.com/puppeteer/puppeteer/issues/7750)) ([b0319ec](https://github.com/puppeteer/puppeteer/commit/b0319ecc89f8ea3d31ab9aee5e1cd33d2a4e62be)), closes [#7721](https://github.com/puppeteer/puppeteer/issues/7721)
+* clearer jsdoc for behavior of `headless` when `devtools` is true ([#7748](https://github.com/puppeteer/puppeteer/issues/7748)) ([9f9b4ed](https://github.com/puppeteer/puppeteer/commit/9f9b4ed72ab0bb43d002a0024122d6f5eab231aa))
+* null check for frame in FrameManager ([#7773](https://github.com/puppeteer/puppeteer/issues/7773)) ([23ee295](https://github.com/puppeteer/puppeteer/commit/23ee295f348d114617f2a86d0bb792936f413ac5)), closes [#7749](https://github.com/puppeteer/puppeteer/issues/7749)
+* only kill the process when there is no browser instance available ([#7762](https://github.com/puppeteer/puppeteer/issues/7762)) ([51e6169](https://github.com/puppeteer/puppeteer/commit/51e61696c1c20cc09bd4fc068ae1dfa259c41745)), closes [#7668](https://github.com/puppeteer/puppeteer/issues/7668)
+* parse statusText from the extraInfo event ([#7798](https://github.com/puppeteer/puppeteer/issues/7798)) ([a26b12b](https://github.com/puppeteer/puppeteer/commit/a26b12b7c775c36271cd4c98e39bbd59f4356320)), closes [#7458](https://github.com/puppeteer/puppeteer/issues/7458)
+* try to remove the temporary user data directory after the process has been killed ([#7761](https://github.com/puppeteer/puppeteer/issues/7761)) ([fc94a28](https://github.com/puppeteer/puppeteer/commit/fc94a28778cfdb3cb8bcd882af3ebcdacf85c94e))
+
+## [11.0.0](https://github.com/puppeteer/puppeteer/compare/v10.4.0...v11.0.0) (2021-11-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* **oop iframes:** integrate OOP iframes with the frame manager (#7556)
+
+### Features
+
+* improve error message for response.buffer() ([#7669](https://github.com/puppeteer/puppeteer/issues/7669)) ([03c9ecc](https://github.com/puppeteer/puppeteer/commit/03c9ecca400a02684cd60229550dbad1190a5b6e))
+* **oop iframes:** integrate OOP iframes with the frame manager ([#7556](https://github.com/puppeteer/puppeteer/issues/7556)) ([4d9dc8c](https://github.com/puppeteer/puppeteer/commit/4d9dc8c0e613f22d4cdf237e8bd0b0da3c588edb)), closes [#2548](https://github.com/puppeteer/puppeteer/issues/2548)
+* add custom debugging port option ([#4993](https://github.com/puppeteer/puppeteer/issues/4993)) ([26145e9](https://github.com/puppeteer/puppeteer/commit/26145e9a24af7caed6ece61031f2cafa6abd505f))
+* add initiator to HTTPRequest ([#7614](https://github.com/puppeteer/puppeteer/issues/7614)) ([a271145](https://github.com/puppeteer/puppeteer/commit/a271145b0663ef9de1903dd0eb9fd5366465bed7))
+* allow to customize tmpdir ([#7243](https://github.com/puppeteer/puppeteer/issues/7243)) ([b1f6e86](https://github.com/puppeteer/puppeteer/commit/b1f6e8692b0bc7e8551b2a78169c830cd80a7acb))
+* handle unhandled promise rejections in tests ([#7722](https://github.com/puppeteer/puppeteer/issues/7722)) ([07febca](https://github.com/puppeteer/puppeteer/commit/07febca04b391893cfc872250e4391da142d4fe2))
+
+
+### Bug Fixes
+
+* add support for relative install paths to BrowserFetcher ([#7613](https://github.com/puppeteer/puppeteer/issues/7613)) ([eebf452](https://github.com/puppeteer/puppeteer/commit/eebf452d38b79bb2ea1a1ba84c3d2ea6f2f9f899)), closes [#7592](https://github.com/puppeteer/puppeteer/issues/7592)
+* add webp to screenshot quality option allow list ([#7631](https://github.com/puppeteer/puppeteer/issues/7631)) ([b20c2bf](https://github.com/puppeteer/puppeteer/commit/b20c2bfa24cbdd4a1b9cefca2e0a9407e442baf5))
+* prevent Target closed errors on streams ([#7728](https://github.com/puppeteer/puppeteer/issues/7728)) ([5b792de](https://github.com/puppeteer/puppeteer/commit/5b792de7a97611441777d1ac99cb95516301d7dc))
+* request an animation frame to fix flaky clickablePoint test ([#7587](https://github.com/puppeteer/puppeteer/issues/7587)) ([7341d9f](https://github.com/puppeteer/puppeteer/commit/7341d9fadd1466a5b2f2bde8631f3b02cf9a7d8a))
+* setup husky properly ([#7727](https://github.com/puppeteer/puppeteer/issues/7727)) ([8b712e7](https://github.com/puppeteer/puppeteer/commit/8b712e7b642b58193437f26d4e104a9e412f388d)), closes [#7726](https://github.com/puppeteer/puppeteer/issues/7726)
+* updated troubleshooting.md to meet latest dependencies changes ([#7656](https://github.com/puppeteer/puppeteer/issues/7656)) ([edb0197](https://github.com/puppeteer/puppeteer/commit/edb01972b9606d8b05b979a588eda0d622315981))
+* **launcher:** launcher.launch() should pass 'timeout' option [#5180](https://github.com/puppeteer/puppeteer/issues/5180) ([#7596](https://github.com/puppeteer/puppeteer/issues/7596)) ([113489d](https://github.com/puppeteer/puppeteer/commit/113489d3b58e2907374a4e6e5133bf46630695d1))
+* **page:** fallback to default in exposeFunction when using imported module ([#6365](https://github.com/puppeteer/puppeteer/issues/6365)) ([44c9ec6](https://github.com/puppeteer/puppeteer/commit/44c9ec67c57dccf3e186c86f14f3a8da9a8eb971))
+* **page:** fix page.off method for request event ([#7624](https://github.com/puppeteer/puppeteer/issues/7624)) ([d0cb943](https://github.com/puppeteer/puppeteer/commit/d0cb9436a302418086f6763e0e58ae3732a20b62)), closes [#7572](https://github.com/puppeteer/puppeteer/issues/7572)
+
+## [10.4.0](https://github.com/puppeteer/puppeteer/compare/v10.2.0...v10.4.0) (2021-09-21)
+
+
+### Features
+
+* add webp to screenshot options ([#7565](https://github.com/puppeteer/puppeteer/issues/7565)) ([43a9268](https://github.com/puppeteer/puppeteer/commit/43a926832505a57922016907a264165676424557))
+* **page:** expose page.client() ([#7582](https://github.com/puppeteer/puppeteer/issues/7582)) ([99ca842](https://github.com/puppeteer/puppeteer/commit/99ca842124a1edef5e66426621885141a9feaca5))
+* **page:** mark page.client() as internal ([#7585](https://github.com/puppeteer/puppeteer/issues/7585)) ([8451951](https://github.com/puppeteer/puppeteer/commit/84519514831f304f9076ca235fe474f797616b2c))
+* add ability to specify offsets for JSHandle.click ([#7573](https://github.com/puppeteer/puppeteer/issues/7573)) ([2b5c001](https://github.com/puppeteer/puppeteer/commit/2b5c0019dc3744196c5858edeaa901dff9973ef5))
+* add durableStorage to allowed permissions ([#5295](https://github.com/puppeteer/puppeteer/issues/5295)) ([eda5171](https://github.com/puppeteer/puppeteer/commit/eda51712790b9260626dc53cfb58a72805c45582))
+* add id option to addScriptTag ([#5477](https://github.com/puppeteer/puppeteer/issues/5477)) ([300be5d](https://github.com/puppeteer/puppeteer/commit/300be5d167b6e7e532e725fdb86966081a5d0093))
+* add more Android models to DeviceDescriptors ([#7210](https://github.com/puppeteer/puppeteer/issues/7210)) ([b5020dc](https://github.com/puppeteer/puppeteer/commit/b5020dc04121b265c77662237dfb177d6de06053)), closes [/github.com/aerokube/moon-deploy/blob/master/moon-local.yaml#L199](https://github.com/puppeteer//github.com/aerokube/moon-deploy/blob/master/moon-local.yaml/issues/L199)
+* add proxy and bypass list parameters to createIncognitoBrowserContext ([#7516](https://github.com/puppeteer/puppeteer/issues/7516)) ([8e45a1c](https://github.com/puppeteer/puppeteer/commit/8e45a1c882207cc36e87be2a917b661eb841c4bf)), closes [#678](https://github.com/puppeteer/puppeteer/issues/678)
+* add threshold to Page.isIntersectingViewport ([#6497](https://github.com/puppeteer/puppeteer/issues/6497)) ([54c4318](https://github.com/puppeteer/puppeteer/commit/54c43180161c3c512e4698e7f2e85ce3c6f0ab50))
+* add unit test support for bisect ([#7553](https://github.com/puppeteer/puppeteer/issues/7553)) ([a0b1f6b](https://github.com/puppeteer/puppeteer/commit/a0b1f6b401abae2fbc5a8987061644adfaa7b482))
+* add User-Agent with Puppeteer version to WebSocket request ([#5614](https://github.com/puppeteer/puppeteer/issues/5614)) ([6a2bf0a](https://github.com/puppeteer/puppeteer/commit/6a2bf0aabaa4df72c7838f5a6cd742e8f9c72be6))
+* extend husky checks ([#7574](https://github.com/puppeteer/puppeteer/issues/7574)) ([7316086](https://github.com/puppeteer/puppeteer/commit/73160869417275200be19bd37372b6218dbc5f63))
+* **api:** implement `Page.waitForNetworkIdle()` ([#5140](https://github.com/puppeteer/puppeteer/issues/5140)) ([3c6029c](https://github.com/puppeteer/puppeteer/commit/3c6029c702291ca7ef637b66e78d72e03156fe58))
+* **coverage:** option for raw V8 script coverage ([#6454](https://github.com/puppeteer/puppeteer/issues/6454)) ([cb4470a](https://github.com/puppeteer/puppeteer/commit/cb4470a6d9b0a7f73836458bb3d5779eb85ac5f2))
+* support timeout for page.pdf() call ([#7508](https://github.com/puppeteer/puppeteer/issues/7508)) ([f90af66](https://github.com/puppeteer/puppeteer/commit/f90af6639d801e764bdb479b9543b7f8f2b926df))
+* **typescript:** allow using puppeteer without dom lib ([#6998](https://github.com/puppeteer/puppeteer/issues/6998)) ([723052d](https://github.com/puppeteer/puppeteer/commit/723052d5bb3c3d1d3908508467512bea4d8fdc80)), closes [#6989](https://github.com/puppeteer/puppeteer/issues/6989)
+
+
+### Bug Fixes
+
+* **docs:** deploy includes website documentation ([#7469](https://github.com/puppeteer/puppeteer/issues/7469)) ([6fde41c](https://github.com/puppeteer/puppeteer/commit/6fde41c6b6657986df1bbce3f2e0f7aa499f2be4))
+* **docs:** names in version 9.1.1 ([#7517](https://github.com/puppeteer/puppeteer/issues/7517)) ([44b22bb](https://github.com/puppeteer/puppeteer/commit/44b22bbc2629e3c75c1494b299a66790b371fb0a))
+* **frame:** fix Frame.waitFor's XPath pattern detection ([#5184](https://github.com/puppeteer/puppeteer/issues/5184)) ([caa2b73](https://github.com/puppeteer/puppeteer/commit/caa2b732fe58f32ec03f2a9fa8568f20188203c5))
+* **install:** respect environment proxy config when downloading Firef… ([#6577](https://github.com/puppeteer/puppeteer/issues/6577)) ([9399c97](https://github.com/puppeteer/puppeteer/commit/9399c9786fba4e45e1c5485ddbb197d2d4f1735f)), closes [#6573](https://github.com/puppeteer/puppeteer/issues/6573)
+* added names in V9.1.1 ([#7547](https://github.com/puppeteer/puppeteer/issues/7547)) ([d132b8b](https://github.com/puppeteer/puppeteer/commit/d132b8b041696e6d5b9a99d0be1acf1cf943efef))
+* **test:** tweak waitForNetworkIdle delay in test between downloads ([#7564](https://github.com/puppeteer/puppeteer/issues/7564)) ([a21b737](https://github.com/puppeteer/puppeteer/commit/a21b7376e7feaf23066d67948d52480516f42496))
+* **types:** allow evaluate functions to take a readonly array as an argument ([#7072](https://github.com/puppeteer/puppeteer/issues/7072)) ([491614c](https://github.com/puppeteer/puppeteer/commit/491614c7f8cfa50b902d0275064e611c2a48c3b2))
+* update firefox prefs documentation link ([#7539](https://github.com/puppeteer/puppeteer/issues/7539)) ([2aec355](https://github.com/puppeteer/puppeteer/commit/2aec35553bc6e0305f40837bb3665ddbd02aa889))
+* use non-deprecated tracing categories api ([#7413](https://github.com/puppeteer/puppeteer/issues/7413)) ([040a0e5](https://github.com/puppeteer/puppeteer/commit/040a0e561b4f623f7929130b90be129f94ebb642))
+
+## [10.2.0](https://github.com/puppeteer/puppeteer/compare/v10.1.0...v10.2.0) (2021-08-04)
+
+
+### Features
+
+* **api:** make `page.isDragInterceptionEnabled` a method ([#7419](https://github.com/puppeteer/puppeteer/issues/7419)) ([dd470c7](https://github.com/puppeteer/puppeteer/commit/dd470c7a226a8422a938a7b0fffa58ffc6b78512)), closes [#7150](https://github.com/puppeteer/puppeteer/issues/7150)
+* **chromium:** roll to Chromium 93.0.4577.0 (r901912) ([#7387](https://github.com/puppeteer/puppeteer/issues/7387)) ([e10faad](https://github.com/puppeteer/puppeteer/commit/e10faad4f239b1120491bb54fcba0216acd3a646))
+* add channel parameter for puppeteer.launch ([#7389](https://github.com/puppeteer/puppeteer/issues/7389)) ([d70f60e](https://github.com/puppeteer/puppeteer/commit/d70f60e0619b8659d191fa492e3db4bc221ae982))
+* add cooperative request intercepts ([#6735](https://github.com/puppeteer/puppeteer/issues/6735)) ([b5e6474](https://github.com/puppeteer/puppeteer/commit/b5e6474374ae6a88fc73cdb1a9906764c2ac5d70))
+* add support for useragentdata ([#7378](https://github.com/puppeteer/puppeteer/issues/7378)) ([7200b1a](https://github.com/puppeteer/puppeteer/commit/7200b1a6fb9dfdfb65d50f0000339333e71b1b2a))
+
+
+### Bug Fixes
+
+* **browser-runner:** reject promise on error ([#7338](https://github.com/puppeteer/puppeteer/issues/7338)) ([5eb20e2](https://github.com/puppeteer/puppeteer/commit/5eb20e29a21ea0e0368fa8937ef38f7c7693ab34))
+* add script to remove html comments from docs markdown ([#7394](https://github.com/puppeteer/puppeteer/issues/7394)) ([ea3df80](https://github.com/puppeteer/puppeteer/commit/ea3df80ed136a03d7698d2319106af5df8d48b58))
+
+## [10.1.0](https://github.com/puppeteer/puppeteer/compare/v10.0.0...v10.1.0) (2021-06-29)
+
+
+### Features
+
+* add a streaming version for page.pdf ([e3699e2](https://github.com/puppeteer/puppeteer/commit/e3699e248bc9c1f7a6ead9a07d68ae8b65905443))
+* add drag-and-drop support ([#7150](https://github.com/puppeteer/puppeteer/issues/7150)) ([a91b8ac](https://github.com/puppeteer/puppeteer/commit/a91b8aca3728b2c2e310e9446897d729bf983377))
+* add page.emulateCPUThrottling ([#7343](https://github.com/puppeteer/puppeteer/issues/7343)) ([4ce4110](https://github.com/puppeteer/puppeteer/commit/4ce41106288938b9d366c550e7a424812920683d))
+
+
+### Bug Fixes
+
+* remove redundant await while fetching target ([#7351](https://github.com/puppeteer/puppeteer/issues/7351)) ([083b297](https://github.com/puppeteer/puppeteer/commit/083b297a6741c6b1dd23867f441130655fac8f7d))
+
+## [10.0.0](https://github.com/puppeteer/puppeteer/compare/v9.1.1...v10.0.0) (2021-05-31)
+
+
+### ⚠ BREAKING CHANGES
+
+* Node.js 10 is no longer supported.
+
+### Features
+
+* **chromium:** roll to Chromium 92.0.4512.0 (r884014) ([#7288](https://github.com/puppeteer/puppeteer/issues/7288)) ([f863f4b](https://github.com/puppeteer/puppeteer/commit/f863f4bfe015e57ea1f9fbb322f1cedee468b857))
+* **requestinterception:** remove cacheSafe flag ([#7217](https://github.com/puppeteer/puppeteer/issues/7217)) ([d01aa6c](https://github.com/puppeteer/puppeteer/commit/d01aa6c84a1e41f15ffed3a8d36ad26a404a7187))
+* expose other sessions from connection ([#6863](https://github.com/puppeteer/puppeteer/issues/6863)) ([cb285a2](https://github.com/puppeteer/puppeteer/commit/cb285a237921259eac99ade1d8b5550e068a55eb))
+* **launcher:** add new launcher option `waitForInitialPage` ([#7105](https://github.com/puppeteer/puppeteer/issues/7105)) ([2605309](https://github.com/puppeteer/puppeteer/commit/2605309f74b43da160cda4d214016e4422bf7676)), closes [#3630](https://github.com/puppeteer/puppeteer/issues/3630)
+
+
+### Bug Fixes
+
+* added comments for browsercontext, startCSSCoverage, and startJSCoverage. ([#7264](https://github.com/puppeteer/puppeteer/issues/7264)) ([b750397](https://github.com/puppeteer/puppeteer/commit/b75039746ac6bddf1411538242b5e70b0f2e6e8a))
+* modified comment for method product, platform and newPage ([#7262](https://github.com/puppeteer/puppeteer/issues/7262)) ([159d283](https://github.com/puppeteer/puppeteer/commit/159d2835450697dabea6f9adf6e67d158b5b8ae3))
+* **requestinterception:** fix font loading issue ([#7060](https://github.com/puppeteer/puppeteer/issues/7060)) ([c9978d2](https://github.com/puppeteer/puppeteer/commit/c9978d20d5584c9fd2dc902e4b4ac86ed8ea5d6e)), closes [/github.com/puppeteer/puppeteer/pull/6996#issuecomment-811546501](https://github.com/puppeteer//github.com/puppeteer/puppeteer/pull/6996/issues/issuecomment-811546501) [/github.com/puppeteer/puppeteer/pull/6996#issuecomment-813797393](https://github.com/puppeteer//github.com/puppeteer/puppeteer/pull/6996/issues/issuecomment-813797393) [#7038](https://github.com/puppeteer/puppeteer/issues/7038)
+
+
+* drop support for Node.js 10 ([#7200](https://github.com/puppeteer/puppeteer/issues/7200)) ([97c9fe2](https://github.com/puppeteer/puppeteer/commit/97c9fe2520723d45a5a86da06b888ae888d400be)), closes [#6753](https://github.com/puppeteer/puppeteer/issues/6753)
+
+### [9.1.1](https://github.com/puppeteer/puppeteer/compare/v9.1.0...v9.1.1) (2021-05-05)
+
+
+### Bug Fixes
+
+* make targetFilter synchronous ([#7203](https://github.com/puppeteer/puppeteer/issues/7203)) ([bcc85a0](https://github.com/puppeteer/puppeteer/commit/bcc85a0969077d122e5d8d2fb5c1061999a8ae48))
+
+## [9.1.0](https://github.com/puppeteer/puppeteer/compare/v9.0.0...v9.1.0) (2021-05-03)
+
+
+### Features
+
+* add option to filter targets ([#7192](https://github.com/puppeteer/puppeteer/issues/7192)) ([ec3fc2e](https://github.com/puppeteer/puppeteer/commit/ec3fc2e035bb5ca14a576180fff612e1ecf6bad7))
+
+
+### Bug Fixes
+
+* change rm -rf to rimraf ([#7168](https://github.com/puppeteer/puppeteer/issues/7168)) ([ad6b736](https://github.com/puppeteer/puppeteer/commit/ad6b736039436fcc5c0a262e5b575aa041427be3))
+
+## [9.0.0](https://github.com/puppeteer/puppeteer/compare/v8.0.0...v9.0.0) (2021-04-21)
+
+
+### ⚠ BREAKING CHANGES
+
+* **filechooser:** FileChooser.cancel() is now synchronous.
+
+### Features
+
+* **chromium:** roll to Chromium 91.0.4469.0 (r869685) ([#7110](https://github.com/puppeteer/puppeteer/issues/7110)) ([715e7a8](https://github.com/puppeteer/puppeteer/commit/715e7a8d62901d1c7ec602425c2fce8d8148b742))
+* **launcher:** fix installation error on Apple M1 chips ([#7099](https://github.com/puppeteer/puppeteer/issues/7099)) ([c239d9e](https://github.com/puppeteer/puppeteer/commit/c239d9edc72d85697b4875c98fff3ec592848082)), closes [#6622](https://github.com/puppeteer/puppeteer/issues/6622)
+* **network:** request interception and caching compatibility ([#6996](https://github.com/puppeteer/puppeteer/issues/6996)) ([8695759](https://github.com/puppeteer/puppeteer/commit/8695759a223bc1bd31baecb00dc28721216e4c6f))
+* **page:** emit the event after removing the Worker ([#7080](https://github.com/puppeteer/puppeteer/issues/7080)) ([e34a6d5](https://github.com/puppeteer/puppeteer/commit/e34a6d53183c3e1f63a375ba6a26bee0dcfcf542))
+* **types:** improve type of predicate function ([#6997](https://github.com/puppeteer/puppeteer/issues/6997)) ([943477c](https://github.com/puppeteer/puppeteer/commit/943477cc1eb4b129870142873b3554737d5ef252)), closes [/github.com/DefinitelyTyped/DefinitelyTyped/blob/c43191a8f7a7d2a47bbff0bc3a7d95ecc64d2269/types/puppeteer/index.d.ts#L1883-L1885](https://github.com/puppeteer//github.com/DefinitelyTyped/DefinitelyTyped/blob/c43191a8f7a7d2a47bbff0bc3a7d95ecc64d2269/types/puppeteer/index.d.ts/issues/L1883-L1885)
+* accept captureBeyondViewport as optional screenshot param ([#7063](https://github.com/puppeteer/puppeteer/issues/7063)) ([0e092d2](https://github.com/puppeteer/puppeteer/commit/0e092d2ea0ec18ad7f07ad3507deb80f96086e7a))
+* **page:** add omitBackground option for page.pdf method ([#6981](https://github.com/puppeteer/puppeteer/issues/6981)) ([dc8ab6d](https://github.com/puppeteer/puppeteer/commit/dc8ab6d8ca1661f8e56d329e6d9c49c891e8b975))
+
+
+### Bug Fixes
+
+* **aria:** fix parsing of ARIA selectors ([#7037](https://github.com/puppeteer/puppeteer/issues/7037)) ([4426135](https://github.com/puppeteer/puppeteer/commit/4426135692ae3ee7ed2841569dd9375e7ca8286c))
+* **page:** fix mouse.click method ([#7097](https://github.com/puppeteer/puppeteer/issues/7097)) ([ba7c367](https://github.com/puppeteer/puppeteer/commit/ba7c367de33ace7753fd9d8b8cc894b2c14ab6c2)), closes [#6462](https://github.com/puppeteer/puppeteer/issues/6462) [#3347](https://github.com/puppeteer/puppeteer/issues/3347)
+* make `$` and `$$` selectors generic ([#6883](https://github.com/puppeteer/puppeteer/issues/6883)) ([b349c91](https://github.com/puppeteer/puppeteer/commit/b349c91e7df76630b7411d6645e649945c4609bd))
+* type page event listeners correctly ([#6891](https://github.com/puppeteer/puppeteer/issues/6891)) ([866d34e](https://github.com/puppeteer/puppeteer/commit/866d34ee1122e89eab00743246676845bb065968))
+* **typescript:** allow defaultViewport to be 'null' ([#6942](https://github.com/puppeteer/puppeteer/issues/6942)) ([e31e68d](https://github.com/puppeteer/puppeteer/commit/e31e68dfa12dd50482b700472bc98876b9031829)), closes [#6885](https://github.com/puppeteer/puppeteer/issues/6885)
+* make screenshots work in puppeteer-web ([#6936](https://github.com/puppeteer/puppeteer/issues/6936)) ([5f24f60](https://github.com/puppeteer/puppeteer/commit/5f24f608194fd4252da7b288461427cabc9dabb3))
+* **filechooser:** cancel is sync ([#6937](https://github.com/puppeteer/puppeteer/issues/6937)) ([2ba61e0](https://github.com/puppeteer/puppeteer/commit/2ba61e04e923edaac09c92315212552f2d4ce676))
+* **network:** don't disable cache for auth challenge ([#6962](https://github.com/puppeteer/puppeteer/issues/6962)) ([1c2479a](https://github.com/puppeteer/puppeteer/commit/1c2479a6cd4bd09a577175ffd31c40ca6f4279b8))
+
+## [8.0.0](https://github.com/puppeteer/puppeteer/compare/v7.1.0...v8.0.0) (2021-02-26)
+
+
+### ⚠ BREAKING CHANGES
+
+* renamed type `ChromeArgOptions` to `BrowserLaunchArgumentOptions`
+* renamed type `BrowserOptions` to `BrowserConnectOptions`
+
+### Features
+
+* **chromium:** roll Chromium to r856583 ([#6927](https://github.com/puppeteer/puppeteer/issues/6927)) ([0c688bd](https://github.com/puppeteer/puppeteer/commit/0c688bd75ef1d1fc3afd14cbe8966757ecda68fb))
+
+
+### Bug Fixes
+
+* explicit HTTPRequest.resourceType type defs ([#6882](https://github.com/puppeteer/puppeteer/issues/6882)) ([ff26c62](https://github.com/puppeteer/puppeteer/commit/ff26c62647b60cd0d8d7ea66ee998adaadc3fcc2)), closes [#6854](https://github.com/puppeteer/puppeteer/issues/6854)
+* expose `Viewport` type ([#6881](https://github.com/puppeteer/puppeteer/issues/6881)) ([be7c229](https://github.com/puppeteer/puppeteer/commit/be7c22933c1dcf5eee797d61463171bd0ef44582))
+* improve TS types for launching browsers ([#6888](https://github.com/puppeteer/puppeteer/issues/6888)) ([98c8145](https://github.com/puppeteer/puppeteer/commit/98c81458c27f378eb66c38e1620e79e2ffde418e))
+* move CI npm config out of .npmrc ([#6901](https://github.com/puppeteer/puppeteer/issues/6901)) ([f7de60b](https://github.com/puppeteer/puppeteer/commit/f7de60be22d9bc6433ada7bfefeaa7f6f6f62047))
+
+## [7.1.0](https://github.com/puppeteer/puppeteer/compare/v7.0.4...v7.1.0) (2021-02-12)
+
+
+### Features
+
+* **page:** add color-gamut support to Page.emulateMediaFeatures ([#6857](https://github.com/puppeteer/puppeteer/issues/6857)) ([ad59357](https://github.com/puppeteer/puppeteer/commit/ad5935738d869cfce386a0d28b4bc6131457f962)), closes [#6761](https://github.com/puppeteer/puppeteer/issues/6761)
+
+
+### Bug Fixes
+
+* add favicon test asset ([#6868](https://github.com/puppeteer/puppeteer/issues/6868)) ([a63f53c](https://github.com/puppeteer/puppeteer/commit/a63f53c9380545550503f5539494c72c607e19ac))
+* expose `ScreenshotOptions` type in type defs ([#6869](https://github.com/puppeteer/puppeteer/issues/6869)) ([63d48b2](https://github.com/puppeteer/puppeteer/commit/63d48b2ecba317b6c0a3acad87a7a3671c769dbc)), closes [#6866](https://github.com/puppeteer/puppeteer/issues/6866)
+* expose puppeteer.Permission type ([#6856](https://github.com/puppeteer/puppeteer/issues/6856)) ([a5e174f](https://github.com/puppeteer/puppeteer/commit/a5e174f696eb192c541db64a603ea5cdf385a643))
+* jsonValue() type is generic ([#6865](https://github.com/puppeteer/puppeteer/issues/6865)) ([bdaba78](https://github.com/puppeteer/puppeteer/commit/bdaba7829da366aabbc81885d84bb2401ab3eaff))
+* wider compat TS types and CI checks to ensure correct type defs ([#6855](https://github.com/puppeteer/puppeteer/issues/6855)) ([6a0eb78](https://github.com/puppeteer/puppeteer/commit/6a0eb7841fd82493903b0b9fa153d2de181350eb))
+
+### [7.0.4](https://github.com/puppeteer/puppeteer/compare/v7.0.3...v7.0.4) (2021-02-09)
+
+
+### Bug Fixes
+
+* make publish bot run full build, not just tsc ([#6848](https://github.com/puppeteer/puppeteer/issues/6848)) ([f718b14](https://github.com/puppeteer/puppeteer/commit/f718b14b64df8be492d344ddd35e40961ff750c5))
+
+### [7.0.3](https://github.com/puppeteer/puppeteer/compare/v7.0.2...v7.0.3) (2021-02-09)
+
+
+### Bug Fixes
+
+* include lib/types.d.ts in files list ([#6844](https://github.com/puppeteer/puppeteer/issues/6844)) ([e34f317](https://github.com/puppeteer/puppeteer/commit/e34f317b37533256a063c1238609b488d263b998))
+
+### [7.0.2](https://github.com/puppeteer/puppeteer/compare/v7.0.1...v7.0.2) (2021-02-09)
+
+
+### Bug Fixes
+
+* much better TypeScript definitions ([#6837](https://github.com/puppeteer/puppeteer/issues/6837)) ([f1b46ab](https://github.com/puppeteer/puppeteer/commit/f1b46ab5faa262f893c17923579d0cf52268a764))
+* **domworld:** reset bindings when context changes ([#6766](https://github.com/puppeteer/puppeteer/issues/6766)) ([#6836](https://github.com/puppeteer/puppeteer/issues/6836)) ([4e8d074](https://github.com/puppeteer/puppeteer/commit/4e8d074c2f8384a2f283f5edf9ef69c40bd8464f))
+* **launcher:** output correct error message for browser ([#6815](https://github.com/puppeteer/puppeteer/issues/6815)) ([6c61874](https://github.com/puppeteer/puppeteer/commit/6c618747979c3a08f2727e9e22fe45cade8c926a))
+
+### [7.0.1](https://github.com/puppeteer/puppeteer/compare/v7.0.0...v7.0.1) (2021-02-04)
+
+
+### Bug Fixes
+
+* **typescript:** ship .d.ts file in npm package ([#6811](https://github.com/puppeteer/puppeteer/issues/6811)) ([a7e3c2e](https://github.com/puppeteer/puppeteer/commit/a7e3c2e09e9163eee2f15221aafa4400e6a75f91))
+
+## [7.0.0](https://github.com/puppeteer/puppeteer/compare/v6.0.0...v7.0.0) (2021-02-03)
+
+
+### ⚠ BREAKING CHANGES
+
+* - `page.screenshot` makes a screenshot with the clip dimensions, not cutting it by the ViewPort size.
+* **chromium:** - `page.screenshot` cuts screenshot content by the ViewPort size, not ViewPort position.
+
+### Features
+
+* use `captureBeyondViewport` in `Page.captureScreenshot` ([#6805](https://github.com/puppeteer/puppeteer/issues/6805)) ([401d84e](https://github.com/puppeteer/puppeteer/commit/401d84e4a3508f9ca5c24dbfcad2a71571b1b8eb))
+* **chromium:** roll Chromium to r848005 ([#6801](https://github.com/puppeteer/puppeteer/issues/6801)) ([890d5c2](https://github.com/puppeteer/puppeteer/commit/890d5c2e57cdee7d73915a878bda86b72e26b608))
+
+## [6.0.0](https://github.com/puppeteer/puppeteer/compare/v5.5.0...v6.0.0) (2021-02-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* **chromium:** The built-in `aria/` selector query handler doesn’t return ignored elements anymore.
+
+### Features
+
+* **chromium:** roll Chromium to r843427 ([#6797](https://github.com/puppeteer/puppeteer/issues/6797)) ([8f9fbdb](https://github.com/puppeteer/puppeteer/commit/8f9fbdbae68254600a9c73ab05f36146c975dba6)), closes [#6758](https://github.com/puppeteer/puppeteer/issues/6758)
+* add page.emulateNetworkConditions ([#6759](https://github.com/puppeteer/puppeteer/issues/6759)) ([5ea76e9](https://github.com/puppeteer/puppeteer/commit/5ea76e9333c42ab5a751ca01aa5676a662f6c063))
+* **types:** expose typedefs to consumers ([#6745](https://github.com/puppeteer/puppeteer/issues/6745)) ([ebd087a](https://github.com/puppeteer/puppeteer/commit/ebd087a31661a1b701650d0be3e123cc5a813bd8))
+* add iPhone 11 models to DeviceDescriptors ([#6467](https://github.com/puppeteer/puppeteer/issues/6467)) ([50b810d](https://github.com/puppeteer/puppeteer/commit/50b810dab7fae5950ba086295462788f91ff1e6f))
+* support fetching and launching on Apple M1 ([9a8479a](https://github.com/puppeteer/puppeteer/commit/9a8479a52a7d8b51690b0732b2a10816cd1b8aef)), closes [#6495](https://github.com/puppeteer/puppeteer/issues/6495) [#6634](https://github.com/puppeteer/puppeteer/issues/6634) [#6641](https://github.com/puppeteer/puppeteer/issues/6641) [#6614](https://github.com/puppeteer/puppeteer/issues/6614)
+* support promise as return value for page.waitForResponse predicate ([#6624](https://github.com/puppeteer/puppeteer/issues/6624)) ([b57f3fc](https://github.com/puppeteer/puppeteer/commit/b57f3fcd5393c68f51d82e670b004f5b116dcbc3))
+
+
+### Bug Fixes
+
+* **domworld:** fix waitfor bindings ([#6766](https://github.com/puppeteer/puppeteer/issues/6766)) ([#6775](https://github.com/puppeteer/puppeteer/issues/6775)) ([cac540b](https://github.com/puppeteer/puppeteer/commit/cac540be3ab8799a1d77b0951b16bc22ea1c2adb))
+* **launcher:** rename TranslateUI to Translate to match Chrome ([#6692](https://github.com/puppeteer/puppeteer/issues/6692)) ([d901696](https://github.com/puppeteer/puppeteer/commit/d901696e0d8901bcb23cf676a5e5ac562f821a0d))
+* do not use old utility world ([#6528](https://github.com/puppeteer/puppeteer/issues/6528)) ([fb85911](https://github.com/puppeteer/puppeteer/commit/fb859115c0e2829bae1d1b32edbf642988e2ef76)), closes [#6527](https://github.com/puppeteer/puppeteer/issues/6527)
+* update to https-proxy-agent@^5.0.0 to fix `ERR_INVALID_PROTOCOL` ([#6555](https://github.com/puppeteer/puppeteer/issues/6555)) ([3bf5a55](https://github.com/puppeteer/puppeteer/commit/3bf5a552890ee80cc4326b1e430424b0fdad4363))
+
+## [5.5.0](https://github.com/puppeteer/puppeteer/compare/v5.4.1...v5.5.0) (2020-11-16)
+
+
+### Features
+
+* **chromium:** roll Chromium to r818858 ([#6526](https://github.com/puppeteer/puppeteer/issues/6526)) ([b549256](https://github.com/puppeteer/puppeteer/commit/b54925695200cad32f470f8eb407259606447a85))
+
+
+### Bug Fixes
+
+* **common:** fix generic type of `_isClosedPromise` ([#6579](https://github.com/puppeteer/puppeteer/issues/6579)) ([122f074](https://github.com/puppeteer/puppeteer/commit/122f074f92f47a7b9aa08091851e51a07632d23b))
+* **domworld:** fix missing binding for waittasks ([#6562](https://github.com/puppeteer/puppeteer/issues/6562)) ([67da1cf](https://github.com/puppeteer/puppeteer/commit/67da1cf866703f5f581c9cce4923697ac38129ef))
diff --git a/remote/test/puppeteer/packages/puppeteer/api-extractor.docs.json b/remote/test/puppeteer/packages/puppeteer/api-extractor.docs.json
new file mode 100644
index 0000000000..88fcdbfd38
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/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/puppeteer/puppeteer.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/puppeteer/api-extractor.json b/remote/test/puppeteer/packages/puppeteer/api-extractor.json
new file mode 100644
index 0000000000..486b3929e7
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/api-extractor.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/puppeteer.d.ts",
+ "bundledPackages": ["puppeteer-core"],
+
+ "apiReport": {
+ "enabled": false
+ },
+
+ "docModel": {
+ "enabled": false
+ },
+
+ "dtsRollup": {
+ "enabled": true,
+ "untrimmedFilePath": "",
+ "alphaTrimmedFilePath": "lib/types.d.ts"
+ },
+
+ "tsdocMetadata": {
+ "enabled": false
+ },
+
+ "messages": {
+ "compilerMessageReporting": {
+ "default": {
+ "logLevel": "warning"
+ }
+ },
+
+ "extractorMessageReporting": {
+ "ae-wrong-input-file-type": {
+ "logLevel": "none"
+ },
+ "ae-internal-missing-underscore": {
+ "logLevel": "none"
+ },
+ "default": {
+ "logLevel": "warning"
+ }
+ },
+
+ "tsdocMessageReporting": {
+ "default": {
+ "logLevel": "warning"
+ }
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer/install.mjs b/remote/test/puppeteer/packages/puppeteer/install.mjs
new file mode 100755
index 0000000000..2724e129d9
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/install.mjs
@@ -0,0 +1,35 @@
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * This file is part of public API.
+ *
+ * By default, the `puppeteer` package runs this script during the installation
+ * process unless one of the env flags is provided.
+ * `puppeteer-core` package doesn't include this step at all. However, it's
+ * still possible to install a supported browser using this script when
+ * necessary.
+ */
+
+async function importInstaller() {
+ try {
+ return await import('puppeteer/internal/node/install.js');
+ } catch {
+ console.warn(
+ 'Skipping browser installation because the Puppeteer build is not available. Run `npm install` again after you have re-built Puppeteer.'
+ );
+ process.exit(0);
+ }
+}
+
+try {
+ const {downloadBrowser} = await importInstaller();
+ downloadBrowser();
+} catch (error) {
+ console.warn('Browser download failed', error);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer/package.json b/remote/test/puppeteer/packages/puppeteer/package.json
new file mode 100644
index 0000000000..0419e4b459
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/package.json
@@ -0,0 +1,133 @@
+{
+ "name": "puppeteer",
+ "version": "21.10.0",
+ "description": "A high-level API to control headless Chrome over the DevTools Protocol",
+ "keywords": [
+ "puppeteer",
+ "chrome",
+ "headless",
+ "automation"
+ ],
+ "type": "commonjs",
+ "bin": "./lib/esm/puppeteer/node/cli.js",
+ "main": "./lib/cjs/puppeteer/puppeteer.js",
+ "types": "./lib/types.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/types.d.ts",
+ "import": "./lib/esm/puppeteer/puppeteer.js",
+ "require": "./lib/cjs/puppeteer/puppeteer.js"
+ },
+ "./internal/*": {
+ "import": "./lib/esm/puppeteer/*",
+ "require": "./lib/cjs/puppeteer/*"
+ },
+ "./*": {
+ "import": "./*",
+ "require": "./*"
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer"
+ },
+ "engines": {
+ "node": ">=16.13.2"
+ },
+ "scripts": {
+ "build:docs": "wireit",
+ "build": "wireit",
+ "clean": "../../tools/clean.js",
+ "postinstall": "node install.mjs",
+ "prepack": "wireit"
+ },
+ "wireit": {
+ "prepack": {
+ "command": "tsx ../../tools/cp.ts ../../README.md README.md",
+ "files": [
+ "../../README.md"
+ ],
+ "output": [
+ "README.md"
+ ]
+ },
+ "build": {
+ "dependencies": [
+ "build:tsc",
+ "build:types"
+ ]
+ },
+ "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/puppeteer/puppeteer-core.d.ts",
+ "tsconfig.json"
+ ],
+ "dependencies": [
+ "build:tsc"
+ ]
+ },
+ "build:tsc": {
+ "command": "tsc -b && tsx ../../tools/chmod.ts 755 lib/cjs/puppeteer/node/cli.js lib/esm/puppeteer/node/cli.js",
+ "clean": "if-file-deleted",
+ "dependencies": [
+ "../puppeteer-core:build",
+ "../browsers:build",
+ "generate:package-json"
+ ],
+ "files": [
+ "src/**"
+ ],
+ "output": [
+ "lib/{cjs,esm}/**",
+ "!lib/esm/package.json"
+ ]
+ },
+ "build:types": {
+ "command": "api-extractor run --local && eslint --cache-location .eslintcache --cache --ext=ts --no-ignore --no-eslintrc -c=../../.eslintrc.types.cjs --fix lib/types.d.ts",
+ "files": [
+ "../../.eslintrc.types.cjs",
+ "api-extractor.json",
+ "lib/esm/puppeteer/types.d.ts",
+ "tsconfig.json"
+ ],
+ "output": [
+ "lib/types.d.ts"
+ ],
+ "dependencies": [
+ "build:tsc"
+ ]
+ }
+ },
+ "files": [
+ "lib",
+ "src",
+ "install.mjs",
+ "!*.test.ts",
+ "!*.test.js",
+ "!*.test.d.ts",
+ "!*.test.js.map",
+ "!*.test.d.ts.map",
+ "!*.tsbuildinfo"
+ ],
+ "author": "The Chromium Authors",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "cosmiconfig": "9.0.0",
+ "puppeteer-core": "21.10.0",
+ "@puppeteer/browsers": "1.9.1"
+ },
+ "devDependencies": {
+ "@types/node": "18.17.15"
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer/src/getConfiguration.ts b/remote/test/puppeteer/packages/puppeteer/src/getConfiguration.ts
new file mode 100644
index 0000000000..28cf026eb7
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/src/getConfiguration.ts
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {homedir} from 'os';
+import {join} from 'path';
+
+import {cosmiconfigSync} from 'cosmiconfig';
+import type {Configuration, Product} from 'puppeteer-core';
+
+/**
+ * @internal
+ */
+function isSupportedProduct(product: unknown): product is Product {
+ switch (product) {
+ case 'chrome':
+ case 'firefox':
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * @internal
+ */
+export const getConfiguration = (): Configuration => {
+ const result = cosmiconfigSync('puppeteer', {
+ searchStrategy: 'global',
+ }).search();
+ const configuration: Configuration = result ? result.config : {};
+
+ configuration.logLevel = (process.env['PUPPETEER_LOGLEVEL'] ??
+ process.env['npm_config_LOGLEVEL'] ??
+ process.env['npm_package_config_LOGLEVEL'] ??
+ configuration.logLevel ??
+ 'warn') as 'silent' | 'error' | 'warn';
+
+ // Merging environment variables.
+ configuration.defaultProduct = (process.env['PUPPETEER_PRODUCT'] ??
+ process.env['npm_config_puppeteer_product'] ??
+ process.env['npm_package_config_puppeteer_product'] ??
+ configuration.defaultProduct ??
+ 'chrome') as Product;
+
+ configuration.executablePath =
+ process.env['PUPPETEER_EXECUTABLE_PATH'] ??
+ process.env['npm_config_puppeteer_executable_path'] ??
+ process.env['npm_package_config_puppeteer_executable_path'] ??
+ configuration.executablePath;
+
+ // Default to skipDownload if executablePath is set
+ if (configuration.executablePath) {
+ configuration.skipDownload = true;
+ }
+
+ // Set skipDownload explicitly or from default
+ configuration.skipDownload = Boolean(
+ process.env['PUPPETEER_SKIP_DOWNLOAD'] ??
+ process.env['npm_config_puppeteer_skip_download'] ??
+ process.env['npm_package_config_puppeteer_skip_download'] ??
+ configuration.skipDownload
+ );
+
+ // Set skipChromeDownload explicitly or from default
+ configuration.skipChromeDownload = Boolean(
+ process.env['PUPPETEER_SKIP_CHROME_DOWNLOAD'] ??
+ process.env['npm_config_puppeteer_skip_chrome_download'] ??
+ process.env['npm_package_config_puppeteer_skip_chrome_download'] ??
+ configuration.skipChromeDownload
+ );
+
+ // Set skipChromeDownload explicitly or from default
+ configuration.skipChromeHeadlessShellDownload = Boolean(
+ process.env['PUPPETEER_SKIP_CHROME_HEADLESS_SHELL_DOWNLOAD'] ??
+ process.env['npm_config_puppeteer_skip_chrome_headless_shell_download'] ??
+ process.env[
+ 'npm_package_config_puppeteer_skip_chrome_headless_shell_download'
+ ] ??
+ configuration.skipChromeHeadlessShellDownload
+ );
+
+ // Prepare variables used in browser downloading
+ if (!configuration.skipDownload) {
+ configuration.browserRevision =
+ process.env['PUPPETEER_BROWSER_REVISION'] ??
+ process.env['npm_config_puppeteer_browser_revision'] ??
+ process.env['npm_package_config_puppeteer_browser_revision'] ??
+ configuration.browserRevision;
+
+ const downloadHost =
+ process.env['PUPPETEER_DOWNLOAD_HOST'] ??
+ process.env['npm_config_puppeteer_download_host'] ??
+ process.env['npm_package_config_puppeteer_download_host'];
+
+ if (downloadHost && configuration.logLevel === 'warn') {
+ console.warn(
+ `PUPPETEER_DOWNLOAD_HOST is deprecated. Use PUPPETEER_DOWNLOAD_BASE_URL instead.`
+ );
+ }
+
+ configuration.downloadBaseUrl =
+ process.env['PUPPETEER_DOWNLOAD_BASE_URL'] ??
+ process.env['npm_config_puppeteer_download_base_url'] ??
+ process.env['npm_package_config_puppeteer_download_base_url'] ??
+ configuration.downloadBaseUrl ??
+ downloadHost;
+
+ configuration.downloadPath =
+ process.env['PUPPETEER_DOWNLOAD_PATH'] ??
+ process.env['npm_config_puppeteer_download_path'] ??
+ process.env['npm_package_config_puppeteer_download_path'] ??
+ configuration.downloadPath;
+ }
+
+ configuration.cacheDirectory =
+ process.env['PUPPETEER_CACHE_DIR'] ??
+ process.env['npm_config_puppeteer_cache_dir'] ??
+ process.env['npm_package_config_puppeteer_cache_dir'] ??
+ configuration.cacheDirectory ??
+ join(homedir(), '.cache', 'puppeteer');
+ configuration.temporaryDirectory =
+ process.env['PUPPETEER_TMP_DIR'] ??
+ process.env['npm_config_puppeteer_tmp_dir'] ??
+ process.env['npm_package_config_puppeteer_tmp_dir'] ??
+ configuration.temporaryDirectory;
+
+ configuration.experiments ??= {};
+
+ // Validate configuration.
+ if (!isSupportedProduct(configuration.defaultProduct)) {
+ throw new Error(`Unsupported product ${configuration.defaultProduct}`);
+ }
+
+ return configuration;
+};
diff --git a/remote/test/puppeteer/packages/puppeteer/src/node/cli.ts b/remote/test/puppeteer/packages/puppeteer/src/node/cli.ts
new file mode 100644
index 0000000000..9a25c59327
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/src/node/cli.ts
@@ -0,0 +1,32 @@
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {CLI, Browser} from '@puppeteer/browsers';
+import {PUPPETEER_REVISIONS} from 'puppeteer-core/internal/revisions.js';
+
+import puppeteer from '../puppeteer.js';
+
+// TODO: deprecate downloadPath in favour of cacheDirectory.
+const cacheDir =
+ puppeteer.configuration.downloadPath ??
+ puppeteer.configuration.cacheDirectory!;
+
+void new CLI({
+ cachePath: cacheDir,
+ scriptName: 'puppeteer',
+ prefixCommand: {
+ cmd: 'browsers',
+ description: 'Manage browsers of this Puppeteer installation',
+ },
+ allowCachePathOverride: false,
+ pinnedBrowsers: {
+ [Browser.CHROME]: PUPPETEER_REVISIONS.chrome,
+ [Browser.FIREFOX]: PUPPETEER_REVISIONS.firefox,
+ [Browser.CHROMEHEADLESSSHELL]: PUPPETEER_REVISIONS['chrome-headless-shell'],
+ },
+}).run(process.argv);
diff --git a/remote/test/puppeteer/packages/puppeteer/src/node/install.ts b/remote/test/puppeteer/packages/puppeteer/src/node/install.ts
new file mode 100644
index 0000000000..76bad868b8
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/src/node/install.ts
@@ -0,0 +1,184 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ install,
+ Browser,
+ resolveBuildId,
+ makeProgressCallback,
+ detectBrowserPlatform,
+} from '@puppeteer/browsers';
+import type {Product} from 'puppeteer-core';
+import {PUPPETEER_REVISIONS} from 'puppeteer-core/internal/revisions.js';
+
+import {getConfiguration} from '../getConfiguration.js';
+
+/**
+ * @internal
+ */
+const supportedProducts = {
+ chrome: 'Chrome',
+ firefox: 'Firefox Nightly',
+} as const;
+
+/**
+ * @internal
+ */
+export async function downloadBrowser(): Promise<void> {
+ overrideProxy();
+
+ const configuration = getConfiguration();
+ if (configuration.skipDownload) {
+ logPolitely('**INFO** Skipping browser download as instructed.');
+ return;
+ }
+
+ const downloadBaseUrl = configuration.downloadBaseUrl;
+
+ const platform = detectBrowserPlatform();
+ if (!platform) {
+ throw new Error('The current platform is not supported.');
+ }
+
+ const product = configuration.defaultProduct!;
+ const browser = productToBrowser(product);
+
+ const unresolvedBuildId =
+ configuration.browserRevision || PUPPETEER_REVISIONS[product] || 'latest';
+ const unresolvedShellBuildId =
+ configuration.browserRevision ||
+ PUPPETEER_REVISIONS['chrome-headless-shell'] ||
+ 'latest';
+
+ // TODO: deprecate downloadPath in favour of cacheDirectory.
+ const cacheDir = configuration.downloadPath ?? configuration.cacheDirectory!;
+
+ try {
+ const installationJobs = [];
+
+ if (configuration.skipChromeDownload) {
+ logPolitely('**INFO** Skipping Chrome download as instructed.');
+ } else {
+ const buildId = await resolveBuildId(
+ browser,
+ platform,
+ unresolvedBuildId
+ );
+ installationJobs.push(
+ install({
+ browser,
+ cacheDir,
+ platform,
+ buildId,
+ downloadProgressCallback: makeProgressCallback(browser, buildId),
+ baseUrl: downloadBaseUrl,
+ })
+ .then(result => {
+ logPolitely(
+ `${supportedProducts[product]} (${result.buildId}) downloaded to ${result.path}`
+ );
+ })
+ .catch(error => {
+ throw new Error(
+ `ERROR: Failed to set up ${supportedProducts[product]} v${buildId}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.`,
+ {
+ cause: error,
+ }
+ );
+ })
+ );
+ }
+
+ if (browser === Browser.CHROME) {
+ if (configuration.skipChromeHeadlessShellDownload) {
+ logPolitely('**INFO** Skipping Chrome download as instructed.');
+ } else {
+ const shellBuildId = await resolveBuildId(
+ browser,
+ platform,
+ unresolvedShellBuildId
+ );
+
+ installationJobs.push(
+ install({
+ browser: Browser.CHROMEHEADLESSSHELL,
+ cacheDir,
+ platform,
+ buildId: shellBuildId,
+ downloadProgressCallback: makeProgressCallback(
+ browser,
+ shellBuildId
+ ),
+ baseUrl: downloadBaseUrl,
+ })
+ .then(result => {
+ logPolitely(
+ `${Browser.CHROMEHEADLESSSHELL} (${result.buildId}) downloaded to ${result.path}`
+ );
+ })
+ .catch(error => {
+ throw new Error(
+ `ERROR: Failed to set up ${Browser.CHROMEHEADLESSSHELL} v${shellBuildId}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.`,
+ {
+ cause: error,
+ }
+ );
+ })
+ );
+ }
+ }
+
+ await Promise.all(installationJobs);
+ } catch (error) {
+ console.error(error);
+ process.exit(1);
+ }
+}
+
+function productToBrowser(product?: Product) {
+ switch (product) {
+ case 'chrome':
+ return Browser.CHROME;
+ case 'firefox':
+ return Browser.FIREFOX;
+ }
+ return Browser.CHROME;
+}
+
+/**
+ * @internal
+ */
+function logPolitely(toBeLogged: unknown): void {
+ const logLevel = process.env['npm_config_loglevel'] || '';
+ const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1;
+
+ // eslint-disable-next-line no-console
+ if (!logLevelDisplay) {
+ console.log(toBeLogged);
+ }
+}
+
+/**
+ * @internal
+ */
+function overrideProxy() {
+ // Override current environment proxy settings with npm configuration, if any.
+ const NPM_HTTPS_PROXY =
+ process.env['npm_config_https_proxy'] || process.env['npm_config_proxy'];
+ const NPM_HTTP_PROXY =
+ process.env['npm_config_http_proxy'] || process.env['npm_config_proxy'];
+ const NPM_NO_PROXY = process.env['npm_config_no_proxy'];
+
+ if (NPM_HTTPS_PROXY) {
+ process.env['HTTPS_PROXY'] = NPM_HTTPS_PROXY;
+ }
+ if (NPM_HTTP_PROXY) {
+ process.env['HTTP_PROXY'] = NPM_HTTP_PROXY;
+ }
+ if (NPM_NO_PROXY) {
+ process.env['NO_PROXY'] = NPM_NO_PROXY;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer/src/puppeteer.ts b/remote/test/puppeteer/packages/puppeteer/src/puppeteer.ts
new file mode 100644
index 0000000000..4f4321bc6c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/src/puppeteer.ts
@@ -0,0 +1,48 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export type {Protocol} from 'puppeteer-core';
+
+export * from 'puppeteer-core/internal/puppeteer-core.js';
+
+import {PuppeteerNode} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+
+import {getConfiguration} from './getConfiguration.js';
+
+const configuration = getConfiguration();
+
+/**
+ * @public
+ */
+const puppeteer = new PuppeteerNode({
+ isPuppeteerCore: false,
+ configuration,
+});
+
+export const {
+ /**
+ * @public
+ */
+ connect,
+ /**
+ * @public
+ */
+ defaultArgs,
+ /**
+ * @public
+ */
+ executablePath,
+ /**
+ * @public
+ */
+ launch,
+ /**
+ * @public
+ */
+ trimCache,
+} = puppeteer;
+
+export default puppeteer;
diff --git a/remote/test/puppeteer/packages/puppeteer/src/tsconfig.cjs.json b/remote/test/puppeteer/packages/puppeteer/src/tsconfig.cjs.json
new file mode 100644
index 0000000000..0cb78dca7f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/src/tsconfig.cjs.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "../lib/cjs/puppeteer"
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer/src/tsconfig.esm.json b/remote/test/puppeteer/packages/puppeteer/src/tsconfig.esm.json
new file mode 100644
index 0000000000..a848929f4f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/src/tsconfig.esm.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../lib/esm/puppeteer"
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer/tsconfig.json b/remote/test/puppeteer/packages/puppeteer/tsconfig.json
new file mode 100644
index 0000000000..11314a80e3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "files": [],
+ "compilerOptions": {
+ // API extractor doesn't work well with NodeNext module resolution, so we
+ // just stick with ol'fashion path resolution.
+ "baseUrl": ".",
+ "paths": {
+ "puppeteer-core/internal/*": ["../puppeteer-core/lib/esm/puppeteer/*"],
+ },
+ },
+ "references": [
+ {"path": "src/tsconfig.esm.json"},
+ {"path": "src/tsconfig.cjs.json"},
+ ],
+}
diff --git a/remote/test/puppeteer/packages/puppeteer/tsdoc.json b/remote/test/puppeteer/packages/puppeteer/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer/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/testserver/CHANGELOG.md b/remote/test/puppeteer/packages/testserver/CHANGELOG.md
new file mode 100644
index 0000000000..bb971ef46d
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+## [0.6.0](https://github.com/puppeteer/puppeteer/compare/testserver-v0.5.0...testserver-v0.6.0) (2022-10-05)
+
+
+### Features
+
+* separate puppeteer and puppeteer-core ([#9023](https://github.com/puppeteer/puppeteer/issues/9023)) ([f42336c](https://github.com/puppeteer/puppeteer/commit/f42336cf83982332829ca7e14ee48d8676e11545))
diff --git a/remote/test/puppeteer/packages/testserver/LICENSE b/remote/test/puppeteer/packages/testserver/LICENSE
new file mode 100644
index 0000000000..afdfe50e72
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/remote/test/puppeteer/packages/testserver/README.md b/remote/test/puppeteer/packages/testserver/README.md
new file mode 100644
index 0000000000..d22b2da449
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/README.md
@@ -0,0 +1,18 @@
+# TestServer
+
+This test server is used internally by Puppeteer to test Puppeteer itself.
+
+### Example
+
+```ts
+const {TestServer} = require('@pptr/testserver');
+
+(async(() => {
+ const httpServer = await TestServer.create(__dirname, 8000),
+ const httpsServer = await TestServer.createHTTPS(__dirname, 8001)
+ httpServer.setRoute('/hello', (req, res) => {
+ res.end('Hello, world!');
+ });
+ console.log('HTTP and HTTPS servers are running!');
+})();
+```
diff --git a/remote/test/puppeteer/packages/testserver/cert.pem b/remote/test/puppeteer/packages/testserver/cert.pem
new file mode 100644
index 0000000000..fd3838535a
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/cert.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDWDCCAkCgAwIBAgIUM8Tmw+D1j+eVz9x9So4zRVqFsKowDQYJKoZIhvcNAQEL
+BQAwGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMB4XDTIwMDUxMzA4MDQyOVoX
+DTMwMDUxMTA4MDQyOVowGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApWbbhgc6CnWywd8xGETT1mfLi3wi
+KIbpAUHghLF4sj0jXz8vLh/4oicpQ12d6bsz+IAi7qrdXNh11P5nEej6/Gx4fWzB
+gGdrJFGPqsvXuhYdzZAmy6xOaWcLIJeQ543bXv3YeST7EGRXJBc/ocTo2jIGTGjq
+hksFaid910VQlX3KGOLTDMUCk00TeEYBTTUx47PWoIsxVqbl2RzVXRSWL5hlPWlW
+29/BQtBGmsXxZyWtqqHudiUulGBSr4LcPyicZLI8nqCqD0ioS0TEmGh61nRBuwBa
+xmLCvPmpt0+sDuOU+1bme3w8juvTVToBIFxGB86rADd3ys+8NeZzXqi+bQIDAQAB
+o4GVMIGSMB0GA1UdDgQWBBT/m3vdkZpQyVQFdYrKHVoAHXDFODAfBgNVHSMEGDAW
+gBT/m3vdkZpQyVQFdYrKHVoAHXDFODAPBgNVHRMBAf8EBTADAQH/MD8GA1UdEQQ4
+MDaCGHd3dy5wdXBwZXRlZXItdGVzdHMudGVzdIIad3d3LnB1cHBldGVlci10ZXN0
+cy0xLnRlc3QwDQYJKoZIhvcNAQELBQADggEBAI1qp5ZppV1R3e8XxzwwkFDPFN8W
+Pe3AoqhAKyJnJl1NUn9q3sroEeSQRhODWUHCd7lENzhsT+3mzonNNkN9B/hq0rpK
+KHHczXILDqdyuxH3LxQ1VHGE8VN2NbdkfobtzAsA3woiJxOuGeusXJnKB4kJQeIP
+V+BMEZWeaSDC2PREkG7GOezmE1/WDUCYaorPw2whdCA5wJvTW3zXpJjYhfsld+5z
+KuErx4OCxRJij73/BD9SpLxDEY1cdl819F1IvxsRGhmTIaSly2hQLrhOgo1jgZtV
+FGCa6DSlXnQGLaV+N+ssR0lkCksNrNBVDfA1bP5bT/4VCcwUWwm9TUeF0Qo=
+-----END CERTIFICATE-----
diff --git a/remote/test/puppeteer/packages/testserver/key.pem b/remote/test/puppeteer/packages/testserver/key.pem
new file mode 100644
index 0000000000..cbc3acb229
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClZtuGBzoKdbLB
+3zEYRNPWZ8uLfCIohukBQeCEsXiyPSNfPy8uH/iiJylDXZ3puzP4gCLuqt1c2HXU
+/mcR6Pr8bHh9bMGAZ2skUY+qy9e6Fh3NkCbLrE5pZwsgl5Dnjdte/dh5JPsQZFck
+Fz+hxOjaMgZMaOqGSwVqJ33XRVCVfcoY4tMMxQKTTRN4RgFNNTHjs9agizFWpuXZ
+HNVdFJYvmGU9aVbb38FC0EaaxfFnJa2qoe52JS6UYFKvgtw/KJxksjyeoKoPSKhL
+RMSYaHrWdEG7AFrGYsK8+am3T6wO45T7VuZ7fDyO69NVOgEgXEYHzqsAN3fKz7w1
+5nNeqL5tAgMBAAECggEAKPveo0xBHnxhidZzBM9xKixX7D0a/a3IKI6ZQmfzPz8U
+97HhT+2OHyfS+qVEzribPRULEtZ1uV7Ne7R5958iKc/63yFGpTl6++nVzn1p++sl
+AV2Zr1gHqehlgnLr7eRhmh0OOZ5nM32ZdhDorH3tMLu6gc5xZktKkS4t6Vx8hj3a
+Docx+rbawp8GRd0p7I6vzIE3bsDab8hC+RTRO63q2G0BqgKwV9ZNtJxQgcDJ5L8N
+6gtM2z5nKXAIOCbCQYa1PsrDh3IRA/ZNxEeA9G3YQjwlZYCWmdRRplgDraYxcTBO
+oQGjaLwICNdcprMacPD6cCSgrI+PadzyMsAuk9SgpQKBgQDO9PT4gK40Pm+Damxv
++tWYBFmvn3vasmyolc1zVDltsxQbQTjKhVpTLXTTGmrIhDXEIIV9I4rg164WptQs
+6Brp2EwYR7ZJIrjvXs/9i2QTW1ZXvhdiWpB3s+RXD5VHGovHUadcI6wOgw2Cl+Jk
+zXjSIgyXKM99N1MAonuR7DyzTwKBgQDMmPX+9vWZMpS/gc6JLQiPPoGszE6tYjXg
+W3LpRUNqmO0/bDDjslbebDgrGAmhlkJlxzH6gz96VmGm1evEGPEet3euy8S9zuM3
+LCgEM9Ulqa3JbInwtKupmKv76Im+XWLLSxAXbfiel1zFRRwxI99A3ad0QRZ6Bov5
+3cHJBwvzgwKBgAU5HW2gIcVjxgC1EOOKmxVpFrJd/gw48JEYpsTAXWqtWFaPwNUr
+pGnw/b/OLN++pnS6tWPBH+Ioz1X3A+fWO8enE9SRCsKxw6UW6XzmpbHvXjB8ta5f
+xsGeoqan2AahXuG659RlehQrro2bM7WDkgcLoPG3r/TjDo83ipLWOXn1AoGAKWiL
+4R56dpcWI+xRsNG8ecFc3Ww8QDswTEg16aBrFJf+7GcpPexKSJn+hDpJOLsAlTjL
+lLgbkNcKzIlfPkEOC/l175quJvxIYFI/hxo2eXjuA2ZERMNMOvb7V/CocC7WX+7B
+Qvyu5OodjI+ANTHdbXNvAMhrlCbfDaMkJVuXv6ECgYBzvY4aYmVoFsr+72/EfLls
+Dz9pi55tUUWc61w6ovd+iliawvXeGi4wibtTH4iGj/C2sJIaMmOD99NQ7Oi/x89D
+oMgSUemkoFL8FGsZGyZ7szqxyON1jP42Bm2MQrW5kIf7Y4yaIGhoak5JNxn2JUyV
+gupVbY1mQ1GTPByxHeLh1w==
+-----END PRIVATE KEY-----
diff --git a/remote/test/puppeteer/packages/testserver/package.json b/remote/test/puppeteer/packages/testserver/package.json
new file mode 100644
index 0000000000..3a9ecf9c65
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@pptr/testserver",
+ "version": "0.6.0",
+ "description": "testing server",
+ "main": "lib/index.js",
+ "scripts": {
+ "build": "wireit",
+ "clean": "../../tools/clean.js"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b",
+ "clean": "if-file-deleted",
+ "files": [
+ "src/**"
+ ],
+ "output": [
+ "lib/**",
+ "tsconfig.tsbuildinfo"
+ ]
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/testserver"
+ },
+ "author": "The Chromium Authors",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mime": "3.0.0",
+ "ws": "8.16.0"
+ },
+ "devDependencies": {
+ "@types/mime": "3.0.4"
+ }
+}
diff --git a/remote/test/puppeteer/packages/testserver/src/index.ts b/remote/test/puppeteer/packages/testserver/src/index.ts
new file mode 100644
index 0000000000..2618fd4d0d
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/src/index.ts
@@ -0,0 +1,311 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import {readFile, readFileSync} from 'fs';
+import {
+ createServer as createHttpServer,
+ type IncomingMessage,
+ type RequestListener,
+ type Server as HttpServer,
+ type ServerResponse,
+} from 'http';
+import {
+ createServer as createHttpsServer,
+ type Server as HttpsServer,
+ type ServerOptions as HttpsServerOptions,
+} from 'https';
+import type {AddressInfo} from 'net';
+import {join} from 'path';
+import type {Duplex} from 'stream';
+import {gzip} from 'zlib';
+
+import {getType as getMimeType} from 'mime';
+import {Server as WebSocketServer, type WebSocket} from 'ws';
+
+interface Subscriber {
+ resolve: (msg: IncomingMessage) => void;
+ reject: (err?: Error) => void;
+ promise: Promise<IncomingMessage>;
+}
+
+type TestIncomingMessage = IncomingMessage & {postBody?: Promise<string>};
+
+export class TestServer {
+ PORT!: number;
+ PREFIX!: string;
+ CROSS_PROCESS_PREFIX!: string;
+ EMPTY_PAGE!: string;
+
+ #dirPath: string;
+ #server: HttpsServer | HttpServer;
+ #wsServer: WebSocketServer;
+
+ #startTime = new Date();
+ #cachedPathPrefix?: string;
+
+ #connections = new Set<Duplex>();
+ #routes = new Map<
+ string,
+ (msg: IncomingMessage, res: ServerResponse) => void
+ >();
+ #auths = new Map<string, {username: string; password: string}>();
+ #csp = new Map<string, string>();
+ #gzipRoutes = new Set<string>();
+ #requestSubscribers = new Map<string, Subscriber>();
+ #requests = new Set<ServerResponse>();
+
+ static async create(dirPath: string): Promise<TestServer> {
+ let res!: (value: unknown) => void;
+ const promise = new Promise(resolve => {
+ res = resolve;
+ });
+ const server = new TestServer(dirPath);
+ server.#server.once('listening', res);
+ server.#server.listen(0);
+ await promise;
+ return server;
+ }
+
+ static async createHTTPS(dirPath: string): Promise<TestServer> {
+ let res!: (value: unknown) => void;
+ const promise = new Promise(resolve => {
+ res = resolve;
+ });
+ const server = new TestServer(dirPath, {
+ key: readFileSync(join(__dirname, '..', 'key.pem')),
+ cert: readFileSync(join(__dirname, '..', 'cert.pem')),
+ passphrase: 'aaaa',
+ });
+ server.#server.once('listening', res);
+ server.#server.listen(0);
+ await promise;
+ return server;
+ }
+
+ constructor(dirPath: string, sslOptions?: HttpsServerOptions) {
+ this.#dirPath = dirPath;
+
+ if (sslOptions) {
+ this.#server = createHttpsServer(sslOptions, this.#onRequest);
+ } else {
+ this.#server = createHttpServer(this.#onRequest);
+ }
+ this.#server.on('connection', this.#onServerConnection);
+ // Disable this as sometimes the socket will timeout
+ // We rely on the fact that we will close the server at the end
+ this.#server.keepAliveTimeout = 0;
+ this.#wsServer = new WebSocketServer({server: this.#server});
+ this.#wsServer.on('connection', this.#onWebSocketConnection);
+ }
+
+ #onServerConnection = (connection: Duplex): void => {
+ this.#connections.add(connection);
+ // ECONNRESET is a legit error given
+ // that tab closing simply kills process.
+ connection.on('error', error => {
+ if ((error as NodeJS.ErrnoException).code !== 'ECONNRESET') {
+ throw error;
+ }
+ });
+ connection.once('close', () => {
+ return this.#connections.delete(connection);
+ });
+ };
+
+ get port(): number {
+ return (this.#server.address() as AddressInfo).port;
+ }
+
+ enableHTTPCache(pathPrefix: string): void {
+ this.#cachedPathPrefix = pathPrefix;
+ }
+
+ setAuth(path: string, username: string, password: string): void {
+ this.#auths.set(path, {username, password});
+ }
+
+ enableGzip(path: string): void {
+ this.#gzipRoutes.add(path);
+ }
+
+ setCSP(path: string, csp: string): void {
+ this.#csp.set(path, csp);
+ }
+
+ async stop(): Promise<void> {
+ this.reset();
+ for (const socket of this.#connections) {
+ socket.destroy();
+ }
+ this.#connections.clear();
+ await new Promise(x => {
+ return this.#server.close(x);
+ });
+ }
+
+ setRoute(
+ path: string,
+ handler: (req: IncomingMessage, res: ServerResponse) => void
+ ): void {
+ this.#routes.set(path, handler);
+ }
+
+ setRedirect(from: string, to: string): void {
+ this.setRoute(from, (_, res) => {
+ res.writeHead(302, {location: to});
+ res.end();
+ });
+ }
+
+ waitForRequest(path: string): Promise<TestIncomingMessage> {
+ const subscriber = this.#requestSubscribers.get(path);
+ if (subscriber) {
+ return subscriber.promise;
+ }
+ let resolve!: (value: IncomingMessage) => void;
+ let reject!: (reason?: Error) => void;
+ const promise = new Promise<IncomingMessage>((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ this.#requestSubscribers.set(path, {resolve, reject, promise});
+ return promise;
+ }
+
+ reset(): void {
+ this.#routes.clear();
+ this.#auths.clear();
+ this.#csp.clear();
+ this.#gzipRoutes.clear();
+ const error = new Error('Static Server has been reset');
+ for (const subscriber of this.#requestSubscribers.values()) {
+ subscriber.reject.call(undefined, error);
+ }
+ this.#requestSubscribers.clear();
+ for (const request of this.#requests.values()) {
+ if (!request.writableEnded) {
+ request.end();
+ }
+ }
+ this.#requests.clear();
+ }
+
+ #onRequest: RequestListener = (
+ request: TestIncomingMessage,
+ response
+ ): void => {
+ this.#requests.add(response);
+
+ request.on('error', (error: {code: string}) => {
+ if (error.code === 'ECONNRESET') {
+ response.end();
+ } else {
+ throw error;
+ }
+ });
+ request.postBody = new Promise(resolve => {
+ let body = '';
+ request.on('data', (chunk: string) => {
+ return (body += chunk);
+ });
+ request.on('end', () => {
+ return resolve(body);
+ });
+ });
+ assert(request.url);
+ const url = new URL(request.url, `https://${request.headers.host}`);
+ const path = url.pathname + url.search;
+ const auth = this.#auths.get(path);
+ if (auth) {
+ const credentials = Buffer.from(
+ (request.headers.authorization || '').split(' ')[1] || '',
+ 'base64'
+ ).toString();
+ if (credentials !== `${auth.username}:${auth.password}`) {
+ response.writeHead(401, {
+ 'WWW-Authenticate': 'Basic realm="Secure Area"',
+ });
+ response.end('HTTP Error 401 Unauthorized: Access is denied');
+ return;
+ }
+ }
+ const subscriber = this.#requestSubscribers.get(path);
+ if (subscriber) {
+ subscriber.resolve.call(undefined, request);
+ this.#requestSubscribers.delete(path);
+ }
+ const handler = this.#routes.get(path);
+ if (handler) {
+ handler.call(undefined, request, response);
+ } else {
+ this.serveFile(request, response, path);
+ }
+ };
+
+ serveFile(
+ request: IncomingMessage,
+ response: ServerResponse,
+ pathName: string
+ ): void {
+ if (pathName === '/') {
+ pathName = '/index.html';
+ }
+ const filePath = join(this.#dirPath, pathName.substring(1));
+
+ if (this.#cachedPathPrefix && filePath.startsWith(this.#cachedPathPrefix)) {
+ if (request.headers['if-modified-since']) {
+ response.statusCode = 304; // not modified
+ response.end();
+ return;
+ }
+ response.setHeader('Cache-Control', 'public, max-age=31536000');
+ response.setHeader('Last-Modified', this.#startTime.toISOString());
+ } else {
+ response.setHeader('Cache-Control', 'no-cache, no-store');
+ }
+ const csp = this.#csp.get(pathName);
+ if (csp) {
+ response.setHeader('Content-Security-Policy', csp);
+ }
+
+ readFile(filePath, (err, data) => {
+ // This can happen if the request is not awaited but started
+ // in the test and get clean via `reset()`
+ if (response.writableEnded) {
+ return;
+ }
+
+ if (err) {
+ response.statusCode = 404;
+ response.end(`File not found: ${filePath}`);
+ return;
+ }
+ const mimeType = getMimeType(filePath);
+ if (mimeType) {
+ const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(
+ mimeType
+ );
+ const contentType = isTextEncoding
+ ? `${mimeType}; charset=utf-8`
+ : mimeType;
+ response.setHeader('Content-Type', contentType);
+ }
+ if (this.#gzipRoutes.has(pathName)) {
+ response.setHeader('Content-Encoding', 'gzip');
+ gzip(data, (_, result) => {
+ response.end(result);
+ });
+ } else {
+ response.end(data);
+ }
+ });
+ }
+
+ #onWebSocketConnection = (socket: WebSocket): void => {
+ socket.send('opened');
+ };
+}
diff --git a/remote/test/puppeteer/packages/testserver/tsconfig.json b/remote/test/puppeteer/packages/testserver/tsconfig.json
new file mode 100644
index 0000000000..08e6681481
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "composite": true,
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "lib",
+ "rootDir": "src",
+ },
+ "include": ["src"],
+}
diff --git a/remote/test/puppeteer/packages/testserver/tsdoc.json b/remote/test/puppeteer/packages/testserver/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/packages/testserver/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/release-please-config.json b/remote/test/puppeteer/release-please-config.json
new file mode 100644
index 0000000000..9438312bbe
--- /dev/null
+++ b/remote/test/puppeteer/release-please-config.json
@@ -0,0 +1,29 @@
+{
+ "last-release-sha": "30e5b1a58edb8b1d94acdff00d64c76e76cf02a3",
+ "packages": {
+ "packages/puppeteer": {
+ "component": "puppeteer"
+ },
+ "packages/puppeteer-core": {
+ "component": "puppeteer-core"
+ },
+ "packages/testserver": {},
+ "packages/ng-schematics": {
+ "bump-minor-pre-major": true,
+ "separate-pull-requests": true
+ },
+ "packages/browsers": {}
+ },
+ "plugins": [
+ {
+ "type": "node-workspace",
+ "merge": false
+ },
+ {
+ "type": "linked-versions",
+ "group-name": "puppeteer",
+ "groupName": "puppeteer",
+ "components": ["puppeteer", "puppeteer-core"]
+ }
+ ]
+}
diff --git a/remote/test/puppeteer/test-d/CommonEventEmitter.test-d.ts b/remote/test/puppeteer/test-d/CommonEventEmitter.test-d.ts
new file mode 100644
index 0000000000..581b248b8a
--- /dev/null
+++ b/remote/test/puppeteer/test-d/CommonEventEmitter.test-d.ts
@@ -0,0 +1,19 @@
+// eslint-disable-next-line no-restricted-imports
+import {EventEmitter as NodeEventEmitter} from 'node:events';
+
+import {expectAssignable} from 'tsd';
+
+import type {CommonEventEmitter, EventEmitter, EventType} from 'puppeteer';
+
+declare const emitter: EventEmitter<Record<EventType, any>>;
+
+{
+ {
+ expectAssignable<CommonEventEmitter<Record<EventType, any>>>(
+ new NodeEventEmitter()
+ );
+ }
+ {
+ expectAssignable<CommonEventEmitter<Record<EventType, any>>>(emitter);
+ }
+}
diff --git a/remote/test/puppeteer/test-d/ElementHandle.test-d.ts b/remote/test/puppeteer/test-d/ElementHandle.test-d.ts
new file mode 100644
index 0000000000..469150933b
--- /dev/null
+++ b/remote/test/puppeteer/test-d/ElementHandle.test-d.ts
@@ -0,0 +1,1025 @@
+import {expectNotType, expectType} from 'tsd';
+
+import type {ElementHandle} from 'puppeteer';
+
+declare const handle: ElementHandle;
+
+{
+ {
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(await handle.$('a'));
+ expectNotType<ElementHandle<Element> | null>(await handle.$('a'));
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('a#id')
+ );
+ expectNotType<ElementHandle<Element> | null>(await handle.$('a#id'));
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('a.class')
+ );
+ expectNotType<ElementHandle<Element> | null>(await handle.$('a.class'));
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('a[attr=value]')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('a[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('a:psuedo-class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('a:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('a:func(arg)')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('a:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(await handle.$('div'));
+ expectNotType<ElementHandle<Element> | null>(await handle.$('div'));
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div#id')
+ );
+ expectNotType<ElementHandle<Element> | null>(await handle.$('div#id'));
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div.class')
+ );
+ expectNotType<ElementHandle<Element> | null>(await handle.$('div.class'));
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div[attr=value]')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div:psuedo-class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div:func(arg)')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$('some-custom'));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('some-custom#id')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('some-custom.class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('some-custom[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('some-custom:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('some-custom:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$(''));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$('#id'));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$('.class'));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$('[attr=value]'));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$(':pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$(':func(arg)'));
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a')
+ );
+ expectNotType<ElementHandle<Element> | null>(await handle.$('div > a'));
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a#id')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a#id')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a.class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a.class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a[attr=value]')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a:psuedo-class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a:func(arg)')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div')
+ );
+ expectNotType<ElementHandle<Element> | null>(await handle.$('div > div'));
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div#id')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div#id')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div.class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div.class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div[attr=value]')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div:psuedo-class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div:func(arg)')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom#id')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom.class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$('div > #id'));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$('div > .class'));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > [attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > :pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > :func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a')
+ );
+ expectNotType<ElementHandle<Element> | null>(await handle.$('div > a'));
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a#id')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a#id')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a.class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a.class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a[attr=value]')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a:psuedo-class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.$('div > a:func(arg)')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > a:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div')
+ );
+ expectNotType<ElementHandle<Element> | null>(await handle.$('div > div'));
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div#id')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div#id')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div.class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div.class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div[attr=value]')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div:psuedo-class')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.$('div > div:func(arg)')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.$('div > div:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom#id')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom.class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom[attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom:pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > some-custom:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$('div > #id'));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(await handle.$('div > .class'));
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > [attr=value]')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > :pseudo-class')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.$('div > :func(arg)')
+ );
+ }
+ }
+}
+
+{
+ {
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(await handle.$$('a'));
+ expectNotType<Array<ElementHandle<Element>>>(await handle.$$('a'));
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('a#id')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(await handle.$$('a#id'));
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('a.class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(await handle.$$('a.class'));
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('a[attr=value]')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('a[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('a:psuedo-class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('a:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('a:func(arg)')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('a:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(await handle.$$('div'));
+ expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div'));
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div#id')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div#id'));
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div.class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div.class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div[attr=value]')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div:psuedo-class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div:func(arg)')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<Element>>>(await handle.$$('some-custom'));
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('some-custom#id')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('some-custom.class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('some-custom[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('some-custom:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('some-custom:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<Element>>>(await handle.$$(''));
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(await handle.$$('#id'));
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(await handle.$$('.class'));
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$(':pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(await handle.$$(':func(arg)'));
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div > a'));
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a#id')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a#id')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a.class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a.class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a[attr=value]')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a:psuedo-class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a:func(arg)')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div#id')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div#id')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div.class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div.class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div[attr=value]')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div:psuedo-class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div:func(arg)')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom#id')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom.class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<Element>>>(await handle.$$('div > #id'));
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > .class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > [attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > :pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > :func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div > a'));
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a#id')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a#id')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a.class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a.class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a[attr=value]')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a:psuedo-class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLAnchorElement>>>(
+ await handle.$$('div > a:func(arg)')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > a:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div#id')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div#id')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div.class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div.class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div[attr=value]')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div:psuedo-class')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<HTMLDivElement>>>(
+ await handle.$$('div > div:func(arg)')
+ );
+ expectNotType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > div:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom#id')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom.class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom[attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom:pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > some-custom:func(arg)')
+ );
+ }
+ }
+ {
+ {
+ expectType<Array<ElementHandle<Element>>>(await handle.$$('div > #id'));
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > .class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > [attr=value]')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > :pseudo-class')
+ );
+ }
+ {
+ expectType<Array<ElementHandle<Element>>>(
+ await handle.$$('div > :func(arg)')
+ );
+ }
+ }
+}
+
+{
+ expectType<void>(
+ await handle.$eval(
+ 'a',
+ (element, int) => {
+ expectType<HTMLAnchorElement>(element);
+ expectType<number>(int);
+ },
+ 1
+ )
+ );
+ expectType<void>(
+ await handle.$eval(
+ 'div',
+ (element, int, str) => {
+ expectType<HTMLDivElement>(element);
+ expectType<number>(int);
+ expectType<string>(str);
+ },
+ 1,
+ ''
+ )
+ );
+ expectType<number>(
+ await handle.$eval(
+ 'a',
+ (element, value) => {
+ expectType<HTMLAnchorElement>(element);
+ return value;
+ },
+ 1
+ )
+ );
+ expectType<number>(
+ await handle.$eval(
+ 'some-element',
+ (element, value) => {
+ expectType<Element>(element);
+ return value;
+ },
+ 1
+ )
+ );
+ expectType<HTMLAnchorElement>(
+ await handle.$eval('a', element => {
+ return element;
+ })
+ );
+ expectType<unknown>(await handle.$eval('a', 'document'));
+}
+
+{
+ expectType<void>(
+ await handle.$$eval(
+ 'a',
+ (elements, int) => {
+ expectType<HTMLAnchorElement[]>(elements);
+ expectType<number>(int);
+ },
+ 1
+ )
+ );
+ expectType<void>(
+ await handle.$$eval(
+ 'div',
+ (elements, int, str) => {
+ expectType<HTMLDivElement[]>(elements);
+ expectType<number>(int);
+ expectType<string>(str);
+ },
+ 1,
+ ''
+ )
+ );
+ expectType<number>(
+ await handle.$$eval(
+ 'a',
+ (elements, value) => {
+ expectType<HTMLAnchorElement[]>(elements);
+ return value;
+ },
+ 1
+ )
+ );
+ expectType<number>(
+ await handle.$$eval(
+ 'some-element',
+ (elements, value) => {
+ expectType<Element[]>(elements);
+ return value;
+ },
+ 1
+ )
+ );
+ expectType<HTMLAnchorElement[]>(
+ await handle.$$eval('a', elements => {
+ return elements;
+ })
+ );
+ expectType<unknown>(await handle.$$eval('a', 'document'));
+}
+
+{
+ {
+ expectType<ElementHandle<HTMLAnchorElement> | null>(
+ await handle.waitForSelector('a')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.waitForSelector('a')
+ );
+ }
+ {
+ expectType<ElementHandle<HTMLDivElement> | null>(
+ await handle.waitForSelector('div')
+ );
+ expectNotType<ElementHandle<Element> | null>(
+ await handle.waitForSelector('div')
+ );
+ }
+ {
+ expectType<ElementHandle<Element> | null>(
+ await handle.waitForSelector('some-custom')
+ );
+ }
+}
diff --git a/remote/test/puppeteer/test-d/JSHandle.test-d.ts b/remote/test/puppeteer/test-d/JSHandle.test-d.ts
new file mode 100644
index 0000000000..fa7348d28e
--- /dev/null
+++ b/remote/test/puppeteer/test-d/JSHandle.test-d.ts
@@ -0,0 +1,84 @@
+import {expectNotAssignable, expectNotType, expectType} from 'tsd';
+
+import type {ElementHandle, JSHandle} from 'puppeteer';
+
+declare const handle: JSHandle;
+
+{
+ expectType<unknown>(await handle.evaluate('document'));
+ expectType<number>(
+ await handle.evaluate(() => {
+ return 1;
+ })
+ );
+ expectType<HTMLElement>(
+ await handle.evaluate(() => {
+ return document.body;
+ })
+ );
+ expectType<string>(
+ await handle.evaluate(() => {
+ return '';
+ })
+ );
+ expectType<string>(
+ await handle.evaluate((value, str) => {
+ expectNotAssignable<never>(value);
+ expectType<string>(str);
+ return '';
+ }, '')
+ );
+}
+
+{
+ expectType<JSHandle>(await handle.evaluateHandle('document'));
+ expectType<JSHandle<number>>(
+ await handle.evaluateHandle(() => {
+ return 1;
+ })
+ );
+ expectType<JSHandle<string>>(
+ await handle.evaluateHandle(() => {
+ return '';
+ })
+ );
+ expectType<JSHandle<string>>(
+ await handle.evaluateHandle((value, str) => {
+ expectNotAssignable<never>(value);
+ expectType<string>(str);
+ return '';
+ }, '')
+ );
+ expectType<ElementHandle<HTMLElement>>(
+ await handle.evaluateHandle(() => {
+ return document.body;
+ })
+ );
+}
+
+declare const handle2: JSHandle<{test: number}>;
+
+{
+ {
+ expectType<JSHandle<number>>(await handle2.getProperty('test'));
+ expectNotType<JSHandle<unknown>>(await handle2.getProperty('test'));
+ }
+ {
+ expectType<JSHandle<unknown>>(
+ await handle2.getProperty('key-doesnt-exist')
+ );
+ expectNotType<JSHandle<string>>(
+ await handle2.getProperty('key-doesnt-exist')
+ );
+ expectNotType<JSHandle<number>>(
+ await handle2.getProperty('key-doesnt-exist')
+ );
+ }
+}
+
+{
+ void handle.evaluate((value, other) => {
+ expectType<unknown>(value);
+ expectType<{test: number}>(other);
+ }, handle2);
+}
diff --git a/remote/test/puppeteer/test-d/NodeFor.test-d.ts b/remote/test/puppeteer/test-d/NodeFor.test-d.ts
new file mode 100644
index 0000000000..0a40b4e689
--- /dev/null
+++ b/remote/test/puppeteer/test-d/NodeFor.test-d.ts
@@ -0,0 +1,157 @@
+import {expectType, expectNotType} from 'tsd';
+
+import type {NodeFor} from 'puppeteer';
+
+declare const nodeFor: <Selector extends string>(
+ selector: Selector
+) => NodeFor<Selector>;
+
+{
+ {
+ expectType<HTMLTableRowElement>(
+ nodeFor(
+ '[data-testid="my-component"] div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div tbody tr'
+ )
+ );
+ expectNotType<Element>(
+ nodeFor(
+ '[data-testid="my-component"] div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div tbody tr'
+ )
+ );
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('a'));
+ expectNotType<Element>(nodeFor('a'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('a#ignored'));
+ expectNotType<Element>(nodeFor('a#ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('a.ignored'));
+ expectNotType<Element>(nodeFor('a.ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('a[ignored'));
+ expectNotType<Element>(nodeFor('a[ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('a:ignored'));
+ expectNotType<Element>(nodeFor('a:ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored a'));
+ expectNotType<Element>(nodeFor('ignored a'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored a#ignored'));
+ expectNotType<Element>(nodeFor('ignored a#ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored a.ignored'));
+ expectNotType<Element>(nodeFor('ignored a.ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored a[ignored'));
+ expectNotType<Element>(nodeFor('ignored a[ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored a:ignored'));
+ expectNotType<Element>(nodeFor('ignored a:ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored > a'));
+ expectNotType<Element>(nodeFor('ignored > a'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored > a#ignored'));
+ expectNotType<Element>(nodeFor('ignored > a#ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored > a.ignored'));
+ expectNotType<Element>(nodeFor('ignored > a.ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored > a[ignored'));
+ expectNotType<Element>(nodeFor('ignored > a[ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored > a:ignored'));
+ expectNotType<Element>(nodeFor('ignored > a:ignored'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored + a'));
+ expectNotType<Element>(nodeFor('ignored + a'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored ~ a'));
+ expectNotType<Element>(nodeFor('ignored ~ a'));
+ }
+ {
+ expectType<HTMLAnchorElement>(nodeFor('ignored | a'));
+ expectNotType<Element>(nodeFor('ignored | a'));
+ }
+ {
+ expectType<HTMLAnchorElement>(
+ nodeFor('ignored ignored > ignored + ignored | a#ignore')
+ );
+ expectNotType<Element>(
+ nodeFor('ignored ignored > ignored + ignored | a#ignore')
+ );
+ }
+}
+{
+ {
+ expectType<Element>(nodeFor(''));
+ }
+ {
+ expectType<Element>(nodeFor('#ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('.ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('[ignored'));
+ }
+ {
+ expectType<Element>(nodeFor(':ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored #ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored .ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored [ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored :ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored > #ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored > .ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored > [ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored > :ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored + #ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored ~ #ignored'));
+ }
+ {
+ expectType<Element>(nodeFor('ignored | #ignored'));
+ }
+ {
+ expectType<Element>(
+ nodeFor('ignored ignored > ignored ~ ignored + ignored | #ignored')
+ );
+ }
+}
diff --git a/remote/test/puppeteer/test-d/puppeteer.test-d.ts b/remote/test/puppeteer/test-d/puppeteer.test-d.ts
new file mode 100644
index 0000000000..f7a45c0db4
--- /dev/null
+++ b/remote/test/puppeteer/test-d/puppeteer.test-d.ts
@@ -0,0 +1,13 @@
+import {expectType} from 'tsd';
+
+import puppeteer, {
+ type connect,
+ type defaultArgs,
+ type executablePath,
+ type launch,
+} from 'puppeteer';
+
+expectType<typeof launch>(puppeteer.launch);
+expectType<typeof connect>(puppeteer.connect);
+expectType<typeof defaultArgs>(puppeteer.defaultArgs);
+expectType<typeof executablePath>(puppeteer.executablePath);
diff --git a/remote/test/puppeteer/test/.eslintrc.js b/remote/test/puppeteer/test/.eslintrc.js
new file mode 100644
index 0000000000..489868b6ed
--- /dev/null
+++ b/remote/test/puppeteer/test/.eslintrc.js
@@ -0,0 +1,38 @@
+module.exports = {
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ /** The mocha tests run on the compiled output in the /lib directory
+ * so we should avoid importing from src.
+ */
+ patterns: ['*src*'],
+ },
+ ],
+ },
+ overrides: [
+ {
+ files: ['*.spec.ts'],
+ rules: {
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {argsIgnorePattern: '^_', varsIgnorePattern: '^_'},
+ ],
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ message:
+ 'Use helper command `launch` to make sure the browsers get cleaned',
+ selector:
+ 'MemberExpression[object.name="puppeteer"][property.name="launch"]',
+ },
+ {
+ message: 'Unexpected debugging mocha test.',
+ selector:
+ 'CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflake"], CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflakeOnly"]',
+ },
+ ],
+ },
+ },
+ ],
+};
diff --git a/remote/test/puppeteer/test/README.md b/remote/test/puppeteer/test/README.md
new file mode 100644
index 0000000000..72085ecfb2
--- /dev/null
+++ b/remote/test/puppeteer/test/README.md
@@ -0,0 +1,95 @@
+# Puppeteer tests
+
+Unit tests in Puppeteer are written using [Mocha] as the test runner and [Expect] as the assertions library.
+
+## Test state
+
+We have some common setup that runs before each test and is defined in `mocha-utils.js`.
+
+You can use the `getTestState` function to read state. It exposes the following that you can use in your tests. These will be reset/tidied between tests automatically for you:
+
+- `puppeteer`: an instance of the Puppeteer library. This is exactly what you'd get if you ran `require('puppeteer')`.
+- `puppeteerPath`: the path to the root source file for Puppeteer.
+- `defaultBrowserOptions`: the default options the Puppeteer browser is launched from in test mode, so tests can use them and override if required.
+- `server`: a dummy test server instance (see `packages/testserver` for more).
+- `httpsServer`: a dummy test server HTTPS instance (see `packages/testserver` for more).
+- `isFirefox`: true if running in Firefox.
+- `isChrome`: true if running Chromium.
+- `isHeadless`: true if the test is in headless mode.
+
+If your test needs a browser instance, you can use the `setupTestBrowserHooks()` function which will automatically configure a browser that will be cleaned between each test suite run. You access this via `getTestState()`.
+
+If your test needs a Puppeteer page and context, you can use the `setupTestPageAndContextHooks()` function which will configure these. You can access `page` and `context` from `getTestState()` once you have done this.
+
+The best place to look is an existing test to see how they use the helpers.
+
+## Skipping tests in specific conditions
+
+To skip tests edit the [TestExpectations](https://github.com/puppeteer/puppeteer/blob/main/test/TestExpectations.json) file. See [test runner documentation](https://github.com/puppeteer/puppeteer/tree/main/tools/mocha-runner) for more details.
+
+## Running tests
+
+- To run all tests applicable for your platform:
+
+```bash
+npm test
+```
+
+- **Important**: don't forget to first build the code if you're testing local changes:
+
+```bash
+npm run build --workspace=@puppeteer-test/test && npm test
+```
+
+### CLI options
+
+| Description | Option | Type |
+| ----------------------------------------------------------------- | ---------------- | ------- |
+| Do not generate coverage report | --no-coverage | boolean |
+| Do not generate suggestion for updating TestExpectation.json file | --no-suggestions | boolean |
+| Specify a file to which to save run data | --save-stats-to | string |
+| Specify a file with a custom Mocha reporter | --reporter | string |
+| Number of times to retry failed tests. | --retries | number |
+| Timeout threshold value. | --timeout | number |
+| Tell Mocha to not run test files in parallel | --no-parallel | boolean |
+| Generate full stacktrace upon failure | --fullTrace | boolean |
+| Name of the Test suit defined in TestSuites.json | --test-suite | string |
+
+### Helpful information
+
+- To run a specific test, substitute the `it` with `it.only`:
+
+```ts
+ ...
+ it.only('should work', async function() {
+ const {server, page} = await getTestState();
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.ok).toBe(true);
+ });
+```
+
+- To disable a specific test, substitute the `it` with `it.skip`:
+
+```ts
+ ...
+ it.skip('should work', async function({server, page}) {
+ const {server, page} = await getTestState();
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.ok).toBe(true);
+ });
+```
+
+- To run Chrome headful tests:
+
+```bash
+npm run test:chrome:headful
+```
+
+- To run tests with custom browser executable:
+
+```bash
+BINARY=<path-to-executable> npm run test:chrome:headless # Or npm run test:firefox
+```
+
+[mocha]: https://mochajs.org/
+[expect]: https://www.npmjs.com/package/expect
diff --git a/remote/test/puppeteer/test/TestExpectations.json b/remote/test/puppeteer/test/TestExpectations.json
new file mode 100644
index 0000000000..f19c2f72e0
--- /dev/null
+++ b/remote/test/puppeteer/test/TestExpectations.json
@@ -0,0 +1,3714 @@
+[
+ {
+ "testIdPattern": "*",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[autofill.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[autofill.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[bfcache.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[click.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[debugInfo.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[debugInfo.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[device-request-prompt.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[dialog.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[drag-and-drop.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[drag-and-drop.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[drag-and-drop.spec] Legacy Drag n' Drop *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[fixtures.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[injected.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[jshandle.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch *",
+ "platforms": ["darwin", "linux"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch *",
+ "platforms": ["win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[locator.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[mouse.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goBack *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.authenticate *",
+ "platforms": ["darwin", "linux"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.authenticate *",
+ "platforms": ["win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.setBypassServiceWorker *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.setBypassServiceWorker *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.setExtraHTTPHeaders *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.isNavigationRequest *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.buffer *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.json *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.text *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Popup *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[prerender.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[queryselector.spec] querySelector *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[screencast.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots Cdp *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[stacktrace.spec] Stack trace *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[accessibility.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[accessibility.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne (Chromium web test) *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler waitForSelector (aria) should have an error message specifically for awaiting an element to be hidden",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler waitForSelector (aria) should have correct stack trace for timeout",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler waitForSelector (aria) should respect timeout",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler waitForSelector (aria) should throw when frame is detached",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[autofill.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "headless"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.process should return child_process instance",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.target should return browser target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should be prompt by default",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should close all belonging targets once closing context",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should create new incognito context",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should have default context",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should timeout waiting for a non-existent target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should wait for a target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext window.open should use parent tab context",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should not report created targets for custom CDP sessions",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[chromiumonly.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |pipe| option *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[Connection.spec] WebDriver BiDi Connection should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[coverage.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[coverage.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[coverage.spec] Coverage specs JSCoverage should ignore pptr internal scripts if reportAnonymousScripts is true",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[devtools.spec] DevTools should expose DevTools as a page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[devtools.spec] DevTools should open devtools when \"devtools: true\" option is given",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[devtools.spec] DevTools target.page() should return a DevTools page if asPage is used",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[devtools.spec] DevTools target.page() should return a DevTools page if custom isPageTarget is provided",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[drag-and-drop.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should not work if the click box is not visible due to the iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.viewport should get the proper viewport size",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.viewport should support mobile emulation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should replace symbols with undefined",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should return properly serialize objects with unknown type fields",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work for circular object",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluateOnNewDocument *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.removeScriptToEvaluateOnNewDocument *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.removeScriptToEvaluateOnNewDocument *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[fixtures.spec] Fixtures dumpio option should work with pipe option",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should handle nested frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.name()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[headful.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[idle_override.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[input.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not throw for circular objects",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should work with dates",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should return the RemoteObject",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should use the same JS wrappers",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect *",
+ "platforms": ["win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject navigation when browser closes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject waitForSelector when browser closes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath returns executablePath for channel",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath when executable path is configured its value is used",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch can launch and close the browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Chrome",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Firefox",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should have custom URL when launching browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should launch Chrome properly with --no-startup-window and waitForInitialPage=false",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments",
+ "platforms": ["linux"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch userDataDir argument with non-existent dir",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[mouse.spec] Mouse should not throw if buttons are pressed twice",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[mouse.spec] Mouse should reset properly",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.goto should return matching responses",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should send referer",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFinished",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events should fire events in proper order",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.setBypassServiceWorker *",
+ "platforms": ["win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network raw network headers Cross-origin set-cookie",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.initiator should return the initiator",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.isNavigationRequest should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.isNavigationRequest should work when navigating to image",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.postData should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.postData should work with blobs",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"],
+ "comment": "Not implemented for BiDi yet."
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.timing returns timing information",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should provide access to elements",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should support evaluating in oop iframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should support frames within OOP frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should support frames within OOP iframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should track navigations within OOP iframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should treat OOP iframes and normal iframes the same",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF waitForFrame should resolve immediately if the frame already exists",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.addStyleTag should throw when added with content to the CSP page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.bringToFront should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.close should *not* run beforeunload by default",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.close should *not* run beforeunload by default",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should have location when fetch fails",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should trigger correct Log",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with logging functions",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.error should throw when page crashes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.exposeFunction *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.exposeFunction should work with loading frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"],
+ "comment": "Missing request interception"
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.metrics metrics event fired on console.timeStamp",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.metrics should get metrics from a page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.pdf can print to PDF with accessible",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.pdf should respect timeout",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.removeExposedFunction should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setCacheEnabled should stay disabled when toggling request interception on/off",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setGeolocation should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setOfflineMode should emulate navigator.onLine",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setOfflineMode should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setUserAgent *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.waitForNetworkIdle should work with aborted requests",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[prerender.spec] Prerender can screencast",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[prerender.spec] Prerender can screencast",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["new-headless"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy in incognito browser context should respect proxy bypass list when configured at browser level",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy in incognito browser context should respect proxy bypass list when configured at context level",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors with name and role",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors with role",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work for ARIA selectors in multiple isolated worlds",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work with :hover",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests Text selectors in Page should clear caches",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[queryObjects.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[requestinterception-experimental.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "SKIP"]
+ },
+ {
+ "testIdPattern": "[requestinterception.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "SKIP"]
+ },
+ {
+ "testIdPattern": "[screencast.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[screencast.spec] Screencasts Page.screencast should validate options",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should get screenshot bigger than the viewport",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[stacktrace.spec] Stack trace *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target Browser.pages should return all of the pages",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target Browser.targets should return all of the targets",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target Browser.waitForTarget should timeout waiting for a non-existent target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should be able to use async waitForTarget",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should be able to use the default page in the browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should contain browser target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should report when a new page is created and closed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should report when a target url changes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[TargetManager.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "SKIP"]
+ },
+ {
+ "testIdPattern": "[touchscreen.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[tracing.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[tracing.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[worker.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[accessibility.spec] Accessibility filtering children of leaf nodes rich text editable fields should have children",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[accessibility.spec] Accessibility get snapshots while the tree is re-calculated",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler parseAriaSelector should find button",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryAll should find menu by name",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryAllArray $$eval should handle many elements",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne should find button by name and role",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne should find button by role",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne should find by name",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne should find first matching element",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[bfcache.spec] BFCache can navigate to a BFCached page containing an OOPIF and a worker",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.isConnected should set the browser connected state",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.isConnected should set the browser connected state",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.isConnected should set the browser connected state",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.process should not return child_process for remote browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.process should not return child_process for remote browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.target should return browser target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browser.spec] Browser specs Browser.version should return version",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should deny permission when not listed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should grant permission when listed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should grant persistent-storage",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should isolate permissions between browser contexts",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should reset permissions",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should trigger permission onchange",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should close all belonging targets once closing context",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should create new incognito context",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should fire target events",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should provide a context id",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should timeout waiting for a non-existent target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should wait for a target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should wait for a target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext should work across sessions",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[browsercontext.spec] BrowserContext window.open should use parent tab context",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should be able to detach session",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should be able to detach session",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should enable and disable domains independently",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should enable and disable domains independently",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should respect custom timeout",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should throw nice errors",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |browserURL| option should be able to connect using browserUrl, with and without trailing slash",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click on checkbox input and toggle",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click on checkbox label and toggle",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click the button if window.Node is removed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click the button inside an iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click the button with deviceScaleFactor set",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click with disabled javascript",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should click with disabled javascript",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should double click the button",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should scroll and click with disabled javascript",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should scroll and click with disabled javascript",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should select the text by triple clicking",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[click.spec] Page.click should select the text by triple clicking",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.cookies should get cookies from multiple urls",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.deleteCookie should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should default to setting secure cookie for HTTPS websites",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should isolate cookies in browser contexts",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set a cookie on a different domain",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set a cookie with a path",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set cookie with reasonable defaults",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set cookies from a frame",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set multiple cookies",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set secure same-site cookies from a frame",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[coverage.spec] Coverage specs CSSCoverage should work with complicated usecases",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[coverage.spec] Coverage specs JSCoverage should not ignore eval() scripts if reportAnonymousScripts is true",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[defaultbrowsercontext.spec] DefaultBrowserContext page.cookies() should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[defaultbrowsercontext.spec] DefaultBrowserContext page.deleteCookie() should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[defaultbrowsercontext.spec] DefaultBrowserContext page.setCookie() should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[dialog.spec] Page.Events.Dialog should allow accepting prompts",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[dialog.spec] Page.Events.Dialog should allow accepting prompts",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[drag-and-drop.spec] Drag n' Drop should drag and drop",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should handle nested frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should handle nested frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boxModel should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work for iframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work for iframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.contentFrame should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.isIntersectingViewport should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.isIntersectingViewport should work with svg elements",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulate should support clicking",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateCPUThrottling should change the CPU throttling rate successfully",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaFeatures should throw in case of bad argument",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaFeatures should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaType should throw in case of bad argument",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaType should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateNetworkConditions should change navigator.connection.effectiveType",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateNetworkConditions should change navigator.connection.effectiveType",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateTimezone should throw for invalid timezone IDs",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateTimezone should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateVisionDeficiency should throw for invalid vision deficiencies",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.emulateVisionDeficiency should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.viewport should detect touch when applying viewport with touches",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.viewport should get the proper viewport size",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.viewport should support landscape emulation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[emulation.spec] Emulation Page.viewport should support touch emulation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Frame.evaluate should have different execution contexts",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should await promise",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should simulate a user gesture",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw if elementHandles are from other frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw if elementHandles are from other frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw when evaluation triggers reload",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work from-inside an exposed function",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[extensions.spec] extensions background_page target type should be available",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[extensions.spec] extensions service_worker target type should be available",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[extensions.spec] extensions target.page() should return a background_page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[extensions.spec] extensions target.page() should return a background_page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[fixtures.spec] Fixtures should close the browser when the node process closes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should detach child frames on navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should report different frame instance when frame re-attaches",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should report different frame instance when frame re-attaches",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame from-inside shadow DOM",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.name()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.parent()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.parent()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should send events when frames are manipulated dynamically",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should send events when frames are manipulated dynamically",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should support framesets",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame.evaluate should throw for detached frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame.evaluate should throw for detached frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[frame.spec] Frame specs Frame.executionContext should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors Response.securityDetails Network redirects should report SecurityDetails",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors Response.securityDetails should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with mixed content",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with mixed content",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with mixed content",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with request interception",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard ElementHandle.press should not support |text| option",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should report shiftKey",
+ "platforms": ["darwin"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should specify location",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should specify repeat property",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should type all kinds of characters",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should type emoji",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should type emoji into an iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[keyboard.spec] Keyboard should type emoji into an iframe",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Browser target events should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Browser.Events.disconnected should be emitted when: browser gets closed, disconnected or underlying websocket gets closed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.close should terminate network waiters",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.close should terminate network waiters",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject navigation when browser closes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to close remote browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect multiple times to the same browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to a browser with no page targets",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to a browser with no page targets",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to the same page simultaneously",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to the same page simultaneously",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to reconnect",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to reconnect to a disconnected browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should support ignoreHTTPSErrors option",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should support targetFilter option",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should support targetFilter option",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath returns executablePath for channel",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch can launch and close the browser",
+ "platforms": ["win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Chrome",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Firefox",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Firefox",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "headless"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should close browser with beforeunload page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should filter out ignored default argument in Firefox",
+ "platforms": ["linux"],
+ "parameters": ["firefox", "headful"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should filter out ignored default arguments in Chrome",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should have custom URL when launching browser",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch userDataDir option should restore cookies",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[locator.spec] Locator Locator.click should work with a OOPIF",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[locator.spec] Locator Locator.click should work with a OOPIF",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[locator.spec] Locator Locator.race races multiple locators",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[mouse.spec] Mouse should send mouse wheel events",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[mouse.spec] Mouse should trigger hover state with removed window.Node",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.goto should return matching responses",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should fail when frame detaches",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should fail when frame detaches",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goBack should work with HistoryAPI",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should fail when main resources failed to load",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle2",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to page with iframe and networkidle0",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to URL with hash and fire requests without hash",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to URL with hash and fire requests without hash",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should not leak listeners during navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should not leak listeners during navigation of 11 pages",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should return response when page changes its URL after load",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should send referer",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should wait for network idle to succeed navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should work when navigating to data url",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should work when navigating to data url",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should work when page calls history API in beforeunload",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation",
+ "platforms": ["linux"],
+ "parameters": ["chrome", "headless"],
+ "expectations": ["PASS", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "headless"],
+ "expectations": ["PASS", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.goto should work with subframes return 204",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work when subframe issues window.stop()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work when subframe issues window.stop()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work when subframe issues window.stop()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with DOM history.back()/history.forward()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.replaceState()",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.Request",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.Request",
+ "platforms": ["win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFailed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFinished",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.RequestServedFromCache",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.RequestServedFromCache",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.Response",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events should fire events in proper order",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events should fire events in proper order",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events should support redirects",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events should support redirects",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.authenticate should allow disable authentication",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.authenticate should fail if wrong credentials",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.authenticate should not disable caching",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.authenticate should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.Events.Request should fire for iframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.Events.Request should fire for iframes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Page.setExtraHTTPHeaders should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource",
+ "platforms": ["win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.frame should work for subframe navigation request",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.frame should work for subframe navigation request",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.initiator should return the initiator",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.isNavigationRequest should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.isNavigationRequest should work with request interception",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.postData should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Request.postData should work with blobs",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"],
+ "comment": "Blobs have no POST data in Firefox's CDP implementation."
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.buffer should throw if the response does not have a body",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.buffer should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.buffer should work with compression",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.fromCache should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.fromCache should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.fromCache should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker",
+ "platforms": ["win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.headers should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.json should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.text should return uncompressed text",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.text should throw when requesting body of redirected response",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.text should wait until response completes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.text should wait until response completes",
+ "platforms": ["win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.text should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Response.timing returns timing information",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should keep track of a frames OOP state",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should provide access to elements",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[oopif.spec] OOPIF should support wait for navigation for transitions from local to OOPIF",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["PASS", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.bringToFront should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.client should return the client instance",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.close should reject all promises when page is closed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.close should run beforeunload if asked for",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.close should run beforeunload if asked for",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.close should run beforeunload if asked for",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.close should terminate network waiters",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should have location and stack trace for console API calls",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should have location and stack trace for console API calls",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should have location and stack trace for console API calls",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should have location when fetch fails",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should not fail for window object",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should trigger correct Log",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with logging functions",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with timing functions",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.error should throw when page crashes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Popup should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and rel=noopener",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and with rel=opener",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and without rel=opener",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Popup should work with fake-clicking target=_blank and rel=noopener",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.Events.Popup should work with noopener",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.exposeFunction should be callable from-inside evaluateOnNewDocument",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.metrics metrics event fired on console.timeStamp",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.metrics should get metrics from a page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.pdf can print to PDF with accessible",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.removeExposedFunction should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.select should work when re-defining top-level Event class",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass after cross-process navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass after cross-process navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP header",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP header",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP in iframes as well",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP in iframes as well",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP meta tag",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP meta tag",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setCacheEnabled should enable or disable the cache based on the state passed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setCacheEnabled should enable or disable the cache based on the state passed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setCacheEnabled should stay disabled when toggling request interception on/off",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setGeolocation should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setJavaScriptEnabled should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setJavaScriptEnabled should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setJavaScriptEnabled should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setOfflineMode should emulate navigator.onLine",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setOfflineMode should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.setUserAgent should work with additional userAgentMetdata",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[prerender.spec] Prerender can navigate to a prerendered page via Puppeteer",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[prerender.spec] Prerender can screencast",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[prerender.spec] Prerender via frame can navigate to a prerendered page via Puppeteer",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at browser level",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at context level",
+ "platforms": ["win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at context level",
+ "platforms": ["linux"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy in incognito browser context should respect proxy bypass list when configured at browser level",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy in incognito browser context should respect proxy bypass list when configured at context level",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy should proxy requests when configured",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors with name and role",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors with role",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work for ARIA selectors in multiple isolated worlds",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[queryObjects.spec] page.queryObjects should fail for disposed handles",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[queryObjects.spec] page.queryObjects should fail primitive values as prototypes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[queryObjects.spec] page.queryObjects should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[queryObjects.spec] page.queryObjects should work for non-trivial page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[requestinterception-experimental.spec] request interception \"after each\" hook in \"request interception\"",
+ "platforms": ["win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should load fonts if cache enabled",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["PASS", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should navigate to URL with hash and fire requests without hash",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should work with redirects",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should navigate to URL with hash and fire requests without hash",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots Cdp should use scale for clip",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should capture full element when larger than viewport",
+ "platforms": ["win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should clip clip bigger than the viewport without \"captureBeyondViewport\"",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[stacktrace.spec] Stack trace should work for none error objects",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target Browser.targets should return all of the targets",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should be able to use async waitForTarget",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should contain browser target",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should create a worker from a service worker",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should create a worker from a shared worker",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should have an opener",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should not report uninitialized pages",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should report when a new page is created and closed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should report when a service worker is created and destroyed",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should report when a target url changes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[target.spec] Target should report when a target url changes",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[touchscreen.spec] Touchscreen Touchscreen.prototype.touchMove should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should survive cross-process navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should survive cross-process navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should survive navigations",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should work when resolved right before execution context disposal",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should work with strict CSP policy",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should work with strict CSP policy",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector Page.waitForSelector is shortcut for main frame",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector Page.waitForSelector is shortcut for main frame",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should run in specified frame",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should run in specified frame",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should survive cross-process navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should survive cross-process navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["FAIL", "PASS", "TIMEOUT"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should survive cross-process navigation",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should throw when frame is detached",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should throw when frame is detached",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should work with removed MutationObserver",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should run in specified frame",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should run in specified frame",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should throw when frame is detached",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should throw when frame is detached",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[worker.spec] Workers should report console logs",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[worker.spec] Workers should report console logs",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[worker.spec] Workers should report errors",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["firefox", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[worker.spec] Workers should report errors",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[worker.spec] Workers should report errors",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["chrome", "webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
+ {
+ "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events",
+ "platforms": ["win32"],
+ "parameters": ["cdp", "chrome", "new-headless"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |pipe| option should fire \"disconnected\" when closing with pipe",
+ "platforms": ["darwin"],
+ "parameters": ["cdp", "chrome", "new-headless"],
+ "expectations": ["FAIL"],
+ "comment": "Remove with M121"
+ },
+ {
+ "testIdPattern": "[devtools.spec] DevTools should expose DevTools as a page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[devtools.spec] DevTools should open devtools when \"devtools: true\" option is given",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[devtools.spec] DevTools target.page() should return a DevTools page if asPage is used",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[devtools.spec] DevTools target.page() should return a DevTools page if custom isPageTarget is provided",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[extensions.spec] extensions background_page target type should be available",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[extensions.spec] extensions service_worker target type should be available",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[extensions.spec] extensions target.page() should return a background_page",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch userDataDir option restores preferences",
+ "platforms": ["win32"],
+ "parameters": ["firefox", "headless", "webDriverBiDi"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[network.spec] network Network Events Page.Events.Request",
+ "platforms": ["linux"],
+ "parameters": ["cdp", "chrome", "new-headless"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headful"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[page.spec] Page Page.bringToFront should work",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should be abortable",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headful"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should work with redirects",
+ "platforms": ["win32"],
+ "parameters": ["cdp", "chrome", "new-headless"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should work with redirects",
+ "platforms": ["win32"],
+ "parameters": ["cdp", "chrome", "headful"],
+ "expectations": ["FAIL", "PASS"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots Cdp should work in \"fromSurface: false\" mode",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "headless"],
+ "expectations": ["SKIP"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox", "headful"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox", "headless"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox", "headful"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox", "headless"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox", "headful"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "firefox", "headless"],
+ "expectations": ["FAIL"]
+ },
+ {
+ "testIdPattern": "[worker.spec] Workers Page.workers",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["cdp", "chrome", "headless"],
+ "expectations": ["FAIL", "PASS"]
+ }
+]
diff --git a/remote/test/puppeteer/test/TestSuites.json b/remote/test/puppeteer/test/TestSuites.json
new file mode 100644
index 0000000000..32adc45d3c
--- /dev/null
+++ b/remote/test/puppeteer/test/TestSuites.json
@@ -0,0 +1,74 @@
+{
+ "testSuites": [
+ {
+ "id": "chrome-headless",
+ "platforms": ["linux", "win32", "darwin"],
+ "parameters": ["chrome", "headless", "cdp"],
+ "expectedLineCoverage": 93
+ },
+ {
+ "id": "chrome-headful",
+ "platforms": ["linux"],
+ "parameters": ["chrome", "headful", "cdp"],
+ "expectedLineCoverage": 93
+ },
+ {
+ "id": "chrome-new-headless",
+ "platforms": ["linux"],
+ "parameters": ["chrome", "new-headless", "cdp"],
+ "expectedLineCoverage": 93
+ },
+ {
+ "id": "firefox-headless",
+ "platforms": ["linux", "darwin"],
+ "parameters": ["firefox", "headless", "cdp"],
+ "expectedLineCoverage": 80
+ },
+ {
+ "id": "firefox-headful",
+ "platforms": ["linux"],
+ "parameters": ["firefox", "headful", "cdp"],
+ "expectedLineCoverage": 80
+ },
+ {
+ "id": "firefox-bidi",
+ "platforms": ["linux"],
+ "parameters": ["firefox", "headless", "webDriverBiDi"],
+ "expectedLineCoverage": 56
+ },
+ {
+ "id": "firefox-bidi-headful",
+ "platforms": ["linux"],
+ "parameters": ["firefox", "headful", "webDriverBiDi"],
+ "expectedLineCoverage": 56
+ },
+ {
+ "id": "chrome-bidi",
+ "platforms": ["linux"],
+ "parameters": ["chrome", "headless", "webDriverBiDi"],
+ "expectedLineCoverage": 56
+ }
+ ],
+ "parameterDefinitions": {
+ "chrome": {
+ "PUPPETEER_PRODUCT": "chrome"
+ },
+ "firefox": {
+ "PUPPETEER_PRODUCT": "firefox"
+ },
+ "headless": {
+ "HEADLESS": "true",
+ "PUPPETEER_LOGLEVEL": "silent"
+ },
+ "headful": {
+ "HEADLESS": "false"
+ },
+ "new-headless": {
+ "HEADLESS": "new"
+ },
+ "webDriverBiDi": {
+ "PUPPETEER_PROTOCOL": "webDriverBiDi"
+ },
+ "cdp": {}
+ }
+}
diff --git a/remote/test/puppeteer/test/assets/abort-request.html b/remote/test/puppeteer/test/assets/abort-request.html
new file mode 100644
index 0000000000..77c056a422
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/abort-request.html
@@ -0,0 +1,13 @@
+<button id="abort"></button>
+
+<script>
+ const button = document.getElementById('abort');
+ button.addEventListener('click', getJson)
+ async function getJson() {
+ const abort = new AbortController();
+ const result = fetch("/simple.json", {
+ signal: abort.signal
+ });
+ abort.abort();
+ }
+</script> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/beforeunload.html b/remote/test/puppeteer/test/assets/beforeunload.html
new file mode 100644
index 0000000000..3cef6763f3
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/beforeunload.html
@@ -0,0 +1,10 @@
+<div>beforeunload demo.</div>
+<script>
+window.addEventListener('beforeunload', event => {
+ // Chrome way.
+ event.returnValue = 'Leave?';
+ // Firefox way.
+ event.preventDefault();
+});
+</script>
+
diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/index.html b/remote/test/puppeteer/test/assets/cached/bfcache/index.html
new file mode 100644
index 0000000000..3d79312828
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/bfcache/index.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>BFCached<a href="target.html">next</a></body>
diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/target.html b/remote/test/puppeteer/test/assets/cached/bfcache/target.html
new file mode 100644
index 0000000000..eafc537b64
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/bfcache/target.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>target</body>
diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe-container.html b/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe-container.html
new file mode 100644
index 0000000000..857914bb6d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe-container.html
@@ -0,0 +1,11 @@
+<body>BFCached<a href="target.html">next</a></body>
+<script>
+ window.addEventListener('DOMContentLoaded', () => {
+ const iframe = document.createElement('iframe');
+ const url = new URL(location.href);
+ url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost';
+ url.pathname = '/cached/bfcache/worker-iframe.html';
+ iframe.src = url.toString();
+ document.body.appendChild(iframe);
+ }, false);
+</script>
diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe.html b/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe.html
new file mode 100644
index 0000000000..9233f557c5
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe.html
@@ -0,0 +1,3 @@
+<script>
+ const worker = new Worker('worker.mjs', {type: 'module'})
+</script>
diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/worker.mjs b/remote/test/puppeteer/test/assets/cached/bfcache/worker.mjs
new file mode 100644
index 0000000000..72a8036e68
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/bfcache/worker.mjs
@@ -0,0 +1 @@
+console.log('HELLO');
diff --git a/remote/test/puppeteer/test/assets/cached/one-style-font.css b/remote/test/puppeteer/test/assets/cached/one-style-font.css
new file mode 100644
index 0000000000..6178de0350
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/one-style-font.css
@@ -0,0 +1,9 @@
+@font-face {
+ font-family: 'one-style';
+ src: url('./one-style.woff') format('woff');
+}
+
+body {
+ background-color: pink;
+ font-family: 'one-style', sans-serif;
+}
diff --git a/remote/test/puppeteer/test/assets/cached/one-style-font.html b/remote/test/puppeteer/test/assets/cached/one-style-font.html
new file mode 100644
index 0000000000..8e7236dfb3
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/one-style-font.html
@@ -0,0 +1,2 @@
+<link rel='stylesheet' href='./one-style-font.css'>
+<div>hello, world!</div>
diff --git a/remote/test/puppeteer/test/assets/cached/one-style.css b/remote/test/puppeteer/test/assets/cached/one-style.css
new file mode 100644
index 0000000000..04e7110b41
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/one-style.css
@@ -0,0 +1,3 @@
+body {
+ background-color: pink;
+}
diff --git a/remote/test/puppeteer/test/assets/cached/one-style.html b/remote/test/puppeteer/test/assets/cached/one-style.html
new file mode 100644
index 0000000000..4760f2b9f7
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/one-style.html
@@ -0,0 +1,2 @@
+<link rel='stylesheet' href='./one-style.css'>
+<div>hello, world!</div>
diff --git a/remote/test/puppeteer/test/assets/consolelog.html b/remote/test/puppeteer/test/assets/consolelog.html
new file mode 100644
index 0000000000..4a27803aa9
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/consolelog.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>console.log test</title>
+ </head>
+ <body>
+ <script>
+ function foo() {
+ console.log('yellow')
+ }
+ function bar() {
+ foo();
+ }
+ bar();
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/credit-card.html b/remote/test/puppeteer/test/assets/credit-card.html
new file mode 100644
index 0000000000..101013a0ca
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/credit-card.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+
+<body>
+ <form id="testform" method="post">
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <label for="name">Name on Card</label>
+ </td>
+ <td>
+ <input size="40" id="name" />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <label for="number">Card Number</label>
+ </td>
+ <td>
+ <input size="40" id="number" name="card_number" />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <label>Expiration Date</label>
+ </td>
+ <td>
+ <input size="2" id="expiration_month" name="ccmonth"> <input size="4" id="expiration_year"
+ name="ccyear" />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <input type="submit" value="Submit">
+ </form>
+</body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/csp.html b/remote/test/puppeteer/test/assets/csp.html
new file mode 100644
index 0000000000..34fc1fc1a5
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csp.html
@@ -0,0 +1 @@
+<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
diff --git a/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf
new file mode 100644
index 0000000000..4b208624e8
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/csscoverage/OFL.txt b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt
new file mode 100644
index 0000000000..a9b3c8b34e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt
@@ -0,0 +1,95 @@
+Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com),
+Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com),
+with Reserved Font Names "Dosis".
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/remote/test/puppeteer/test/assets/csscoverage/empty.html b/remote/test/puppeteer/test/assets/csscoverage/empty.html
new file mode 100644
index 0000000000..b3845c366d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/empty.html
@@ -0,0 +1,3 @@
+<style></style>
+<div>empty style tag</div>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/involved.html b/remote/test/puppeteer/test/assets/csscoverage/involved.html
new file mode 100644
index 0000000000..bcd9845b93
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/involved.html
@@ -0,0 +1,26 @@
+<style>
+@charset "utf-8";
+@namespace svg url(http://www.w3.org/2000/svg);
+@font-face {
+ font-family: "Example Font";
+ src: url("./Dosis-Regular.ttf");
+}
+
+#fluffy {
+ border: 1px solid black;
+ z-index: 1;
+ /* -webkit-disabled-property: rgb(1, 2, 3) */
+ -lol-cats: "dogs" /* non-existing property */
+}
+
+@media (min-width: 1px) {
+ span {
+ -webkit-border-radius: 10px;
+ font-family: "Example Font";
+ animation: 1s identifier;
+ }
+}
+</style>
+<div id="fluffy">woof!</div>
+<span>fancy text</span>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/media.html b/remote/test/puppeteer/test/assets/csscoverage/media.html
new file mode 100644
index 0000000000..bfb89f8f75
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/media.html
@@ -0,0 +1,4 @@
+<style>
+@media screen { div { color: green; } } </style>
+<div>hello, world</div>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/multiple.html b/remote/test/puppeteer/test/assets/csscoverage/multiple.html
new file mode 100644
index 0000000000..0fd97e962a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/multiple.html
@@ -0,0 +1,8 @@
+<link rel="stylesheet" href="stylesheet1.css">
+<link rel="stylesheet" href="stylesheet2.css">
+<script>
+window.addEventListener('DOMContentLoaded', () => {
+ // Force stylesheets to load.
+ console.log(window.getComputedStyle(document.body).color);
+}, false);
+</script>
diff --git a/remote/test/puppeteer/test/assets/csscoverage/simple.html b/remote/test/puppeteer/test/assets/csscoverage/simple.html
new file mode 100644
index 0000000000..3beae21829
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/simple.html
@@ -0,0 +1,6 @@
+<style>
+div { color: green; }
+a { color: blue; }
+</style>
+<div>hello, world</div>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html
new file mode 100644
index 0000000000..df4e9c276c
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html
@@ -0,0 +1,7 @@
+<style>
+body {
+ padding: 10px;
+}
+/*# sourceURL=nicename.css */
+</style>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css
new file mode 100644
index 0000000000..60f1eab971
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css
@@ -0,0 +1,3 @@
+body {
+ color: red;
+}
diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css
new file mode 100644
index 0000000000..a87defb098
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css
@@ -0,0 +1,4 @@
+html {
+ margin: 0;
+ padding: 0;
+}
diff --git a/remote/test/puppeteer/test/assets/csscoverage/unused.html b/remote/test/puppeteer/test/assets/csscoverage/unused.html
new file mode 100644
index 0000000000..5b8186a3bf
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/unused.html
@@ -0,0 +1,7 @@
+<style>
+@media screen {
+ a { color: green; }
+}
+/*# sourceURL=unused.css */
+</style>
+
diff --git a/remote/test/puppeteer/test/assets/detect-touch.html b/remote/test/puppeteer/test/assets/detect-touch.html
new file mode 100644
index 0000000000..80a4123fbd
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/detect-touch.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Detect Touch Test</title>
+ <script src='modernizr.js'></script>
+ </head>
+ <body style="font-size:30vmin">
+ <script>
+ document.body.textContent = Modernizr.touchevents ? 'YES' : 'NO';
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/digits/0.png b/remote/test/puppeteer/test/assets/digits/0.png
new file mode 100644
index 0000000000..ac3c4768ed
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/0.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/1.png b/remote/test/puppeteer/test/assets/digits/1.png
new file mode 100644
index 0000000000..6768222729
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/1.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/2.png b/remote/test/puppeteer/test/assets/digits/2.png
new file mode 100644
index 0000000000..b1daa4735d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/3.png b/remote/test/puppeteer/test/assets/digits/3.png
new file mode 100644
index 0000000000..6eca99b21b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/3.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/4.png b/remote/test/puppeteer/test/assets/digits/4.png
new file mode 100644
index 0000000000..a721071e2c
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/4.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/5.png b/remote/test/puppeteer/test/assets/digits/5.png
new file mode 100644
index 0000000000..15cb19932a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/5.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/6.png b/remote/test/puppeteer/test/assets/digits/6.png
new file mode 100644
index 0000000000..639f38439d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/6.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/7.png b/remote/test/puppeteer/test/assets/digits/7.png
new file mode 100644
index 0000000000..5c1150b005
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/7.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/8.png b/remote/test/puppeteer/test/assets/digits/8.png
new file mode 100644
index 0000000000..abb8b48b0b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/8.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/9.png b/remote/test/puppeteer/test/assets/digits/9.png
new file mode 100644
index 0000000000..6a40a21c6f
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/9.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/dynamic-oopif.html b/remote/test/puppeteer/test/assets/dynamic-oopif.html
new file mode 100644
index 0000000000..38614d0289
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/dynamic-oopif.html
@@ -0,0 +1,10 @@
+<script>
+window.addEventListener('DOMContentLoaded', () => {
+ const iframe = document.createElement('iframe');
+ const url = new URL(location.href);
+ url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost';
+ url.pathname = '/oopif.html';
+ iframe.src = url.toString();
+ document.body.appendChild(iframe);
+}, false);
+</script>
diff --git a/remote/test/puppeteer/test/assets/empty.html b/remote/test/puppeteer/test/assets/empty.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/empty.html
diff --git a/remote/test/puppeteer/test/assets/error.html b/remote/test/puppeteer/test/assets/error.html
new file mode 100644
index 0000000000..130400c006
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/error.html
@@ -0,0 +1,15 @@
+<script>
+a();
+
+function a() {
+ b();
+}
+
+function b() {
+ c();
+}
+
+function c() {
+ throw new Error('Fancy error!');
+}
+</script>
diff --git a/remote/test/puppeteer/test/assets/es6/.eslintrc b/remote/test/puppeteer/test/assets/es6/.eslintrc
new file mode 100644
index 0000000000..1903e176f5
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/es6/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "parserOptions": {
+ "sourceType": "module"
+ }
+} \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/es6/es6import.js b/remote/test/puppeteer/test/assets/es6/es6import.js
new file mode 100644
index 0000000000..9aac2d4d64
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/es6/es6import.js
@@ -0,0 +1,2 @@
+import num from './es6module.js';
+window.__es6injected = num;
diff --git a/remote/test/puppeteer/test/assets/es6/es6module.js b/remote/test/puppeteer/test/assets/es6/es6module.js
new file mode 100644
index 0000000000..7a4e8a723a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/es6/es6module.js
@@ -0,0 +1 @@
+export default 42;
diff --git a/remote/test/puppeteer/test/assets/es6/es6pathimport.js b/remote/test/puppeteer/test/assets/es6/es6pathimport.js
new file mode 100644
index 0000000000..eb17a9a3d1
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/es6/es6pathimport.js
@@ -0,0 +1,2 @@
+import num from './es6/es6module.js';
+window.__es6injected = num;
diff --git a/remote/test/puppeteer/test/assets/favicon.ico b/remote/test/puppeteer/test/assets/favicon.ico
new file mode 100644
index 0000000000..d4edd50799
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/favicon.ico
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/file-to-upload.txt b/remote/test/puppeteer/test/assets/file-to-upload.txt
new file mode 100644
index 0000000000..b4ad118489
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/file-to-upload.txt
@@ -0,0 +1 @@
+contents of the file \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/frames/frame.html b/remote/test/puppeteer/test/assets/frames/frame.html
new file mode 100644
index 0000000000..8f20d2da9f
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/frame.html
@@ -0,0 +1,8 @@
+<link rel='stylesheet' href='./style.css'>
+<script src='./script.js' type='text/javascript'></script>
+<style>
+div {
+ line-height: 18px;
+}
+</style>
+<div>Hi, I'm frame</div>
diff --git a/remote/test/puppeteer/test/assets/frames/frameset.html b/remote/test/puppeteer/test/assets/frames/frameset.html
new file mode 100644
index 0000000000..4d56f88839
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/frameset.html
@@ -0,0 +1,8 @@
+<frameset>
+ <frameset>
+ <frame src='./frame.html'></frame>
+ <frame src='about:blank'></frame>
+ </frameset>
+ <frame src='/empty.html'></frame>
+ <frame></frame>
+</frameset>
diff --git a/remote/test/puppeteer/test/assets/frames/lazy-frame.html b/remote/test/puppeteer/test/assets/frames/lazy-frame.html
new file mode 100644
index 0000000000..4821cd76cd
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/lazy-frame.html
@@ -0,0 +1,3 @@
+<iframe width="100%" height="300" src="about:blank"></iframe>
+<div style="height: 800vh"></div>
+<iframe width="100%" height="300" src='./frame.html' loading="lazy"></iframe> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/frames/nested-frames.html b/remote/test/puppeteer/test/assets/frames/nested-frames.html
new file mode 100644
index 0000000000..e9c5d83c03
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/nested-frames.html
@@ -0,0 +1,26 @@
+<style>
+:root {
+ scrollbar-width: none;
+}
+
+body {
+ display: flex;
+}
+
+body iframe {
+ flex-grow: 1;
+ flex-shrink: 1;
+}
+</style>
+<script>
+async function attachFrame(frameId, url) {
+ var frame = document.createElement('iframe');
+ frame.src = url;
+ frame.id = frameId;
+ document.body.appendChild(frame);
+ await new Promise(x => frame.onload = x);
+ return 'kazakh';
+}
+</script>
+<iframe src='./two-frames.html' name='2frames'></iframe>
+<iframe src='./frame.html' name='aframe'></iframe>
diff --git a/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html
new file mode 100644
index 0000000000..d1462641ff
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html
@@ -0,0 +1 @@
+<iframe src='./frame.html?param=value#fragment'></iframe>
diff --git a/remote/test/puppeteer/test/assets/frames/one-frame.html b/remote/test/puppeteer/test/assets/frames/one-frame.html
new file mode 100644
index 0000000000..e941d795a2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/one-frame.html
@@ -0,0 +1 @@
+<iframe src='./frame.html'></iframe>
diff --git a/remote/test/puppeteer/test/assets/frames/script.js b/remote/test/puppeteer/test/assets/frames/script.js
new file mode 100644
index 0000000000..be22256d16
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/script.js
@@ -0,0 +1 @@
+console.log('Cheers!');
diff --git a/remote/test/puppeteer/test/assets/frames/style.css b/remote/test/puppeteer/test/assets/frames/style.css
new file mode 100644
index 0000000000..5b5436e874
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/style.css
@@ -0,0 +1,3 @@
+div {
+ color: blue;
+}
diff --git a/remote/test/puppeteer/test/assets/frames/two-frames.html b/remote/test/puppeteer/test/assets/frames/two-frames.html
new file mode 100644
index 0000000000..b2ee853eda
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/two-frames.html
@@ -0,0 +1,13 @@
+<style>
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+body iframe {
+ flex-grow: 1;
+ flex-shrink: 1;
+}
+</style>
+<iframe src='./frame.html' name='uno'></iframe>
+<iframe src='./frame.html' name='dos'></iframe>
diff --git a/remote/test/puppeteer/test/assets/global-var.html b/remote/test/puppeteer/test/assets/global-var.html
new file mode 100644
index 0000000000..b6be975038
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/global-var.html
@@ -0,0 +1,3 @@
+<script>
+var globalVar = 123;
+</script> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/grid.html b/remote/test/puppeteer/test/assets/grid.html
new file mode 100644
index 0000000000..437193573d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/grid.html
@@ -0,0 +1,51 @@
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+ function generatePalette(amount) {
+ var result = [];
+ var hueStep = 360 / amount;
+ for (var i = 0; i < amount; ++i)
+ result.push('hsl(' + (hueStep * i) + ', 100%, 90%)');
+ return result;
+ }
+
+ var palette = generatePalette(100);
+ for (var i = 0; i < 200; ++i) {
+ var box = document.createElement('div');
+ box.classList.add('box');
+ box.style.setProperty('background-color', palette[i % palette.length]);
+ var x = i;
+ do {
+ var digit = x % 10;
+ x = (x / 10)|0;
+ var img = document.createElement('img');
+ img.src = `./digits/${digit}.png`;
+ box.insertBefore(img, box.firstChild);
+ } while (x);
+ document.body.appendChild(box);
+ }
+});
+</script>
+
+<style>
+
+:root {
+ scrollbar-width: none;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+}
+
+.box {
+ font-family: arial;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0;
+ padding: 0;
+ width: 50px;
+ height: 50px;
+ box-sizing: border-box;
+ border: 1px solid darkgray;
+}
diff --git a/remote/test/puppeteer/test/assets/historyapi.html b/remote/test/puppeteer/test/assets/historyapi.html
new file mode 100644
index 0000000000..bacaf9e9a0
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/historyapi.html
@@ -0,0 +1,5 @@
+<script>
+window.addEventListener('DOMContentLoaded', () => {
+ history.pushState({}, '', '#1');
+});
+</script>
diff --git a/remote/test/puppeteer/test/assets/idle-detector.html b/remote/test/puppeteer/test/assets/idle-detector.html
new file mode 100644
index 0000000000..83b496c03d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/idle-detector.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<div id="state"></div>
+<script>
+ const elState = document.querySelector('#state');
+ function setState(msg) {
+ elState.textContent = msg;
+ }
+ async function main() {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const idleDetector = new IdleDetector({
+ threshold: 60000,
+ signal,
+ });
+ idleDetector.addEventListener('change', () => {
+ const userState = idleDetector.userState;
+ const screenState = idleDetector.screenState;
+ setState(`Idle state: ${userState}, ${screenState}.`);
+ });
+ idleDetector.start();
+ }
+ main();
+</script>
diff --git a/remote/test/puppeteer/test/assets/initiator.html b/remote/test/puppeteer/test/assets/initiator.html
new file mode 100644
index 0000000000..12889d3242
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/initiator.html
@@ -0,0 +1,2 @@
+<iframe src="./frames/frame.html"></iframe>
+<script src="./initiator.js"></script>
diff --git a/remote/test/puppeteer/test/assets/initiator.js b/remote/test/puppeteer/test/assets/initiator.js
new file mode 100644
index 0000000000..642e775f31
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/initiator.js
@@ -0,0 +1,8 @@
+const script = document.createElement('script');
+script.src = './injectedfile.js';
+document.body.appendChild(script);
+
+const style = document.createElement('link');
+style.rel = 'stylesheet';
+style.href = './injectedstyle.css';
+document.head.appendChild(style);
diff --git a/remote/test/puppeteer/test/assets/injectedfile.js b/remote/test/puppeteer/test/assets/injectedfile.js
new file mode 100644
index 0000000000..c211b62c16
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/injectedfile.js
@@ -0,0 +1,2 @@
+window.__injected = 42;
+window.__injectedError = new Error('hi');
diff --git a/remote/test/puppeteer/test/assets/injectedstyle.css b/remote/test/puppeteer/test/assets/injectedstyle.css
new file mode 100644
index 0000000000..aa1634c255
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/injectedstyle.css
@@ -0,0 +1,3 @@
+body {
+ background-color: red;
+}
diff --git a/remote/test/puppeteer/test/assets/inline-svg.html b/remote/test/puppeteer/test/assets/inline-svg.html
new file mode 100644
index 0000000000..20023ecc79
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/inline-svg.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <body>
+ <svg>
+ <circle cx="10" cy="10" r="10" />
+ </svg>
+
+ <div style="margin-top: 5000px;">
+ <svg>
+ <circle cx="10" cy="10" r="10" />
+ </svg>
+ </div>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/inner-frame1.html b/remote/test/puppeteer/test/assets/inner-frame1.html
new file mode 100644
index 0000000000..00f19ec166
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/inner-frame1.html
@@ -0,0 +1,10 @@
+<script>
+ window.addEventListener('DOMContentLoaded', () => {
+ const iframe = document.createElement('iframe');
+ const url = new URL(location.href);
+ url.hostname = 'inner-frame2.test';
+ url.pathname = '/inner-frame2.html';
+ iframe.src = url.toString();
+ document.body.appendChild(iframe);
+ }, false);
+</script>
diff --git a/remote/test/puppeteer/test/assets/inner-frame2.html b/remote/test/puppeteer/test/assets/inner-frame2.html
new file mode 100644
index 0000000000..9a236cc48f
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/inner-frame2.html
@@ -0,0 +1 @@
+<button>click</button>
diff --git a/remote/test/puppeteer/test/assets/input/button.html b/remote/test/puppeteer/test/assets/input/button.html
new file mode 100644
index 0000000000..d4c6e13fd2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/button.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Button test</title>
+ </head>
+ <body>
+ <script src="mouse-helper.js"></script>
+ <button onclick="clicked();">Click target</button>
+ <script>
+ window.result = 'Was not clicked';
+ function clicked() {
+ result = 'Clicked';
+ }
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/input/checkbox.html b/remote/test/puppeteer/test/assets/input/checkbox.html
new file mode 100644
index 0000000000..ca56762e2b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/checkbox.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Selection Test</title>
+ </head>
+ <body>
+ <label for="agree">Remember Me</label>
+ <input id="agree" type="checkbox">
+ <script>
+ window.result = {
+ check: null,
+ events: [],
+ };
+
+ let checkbox = document.querySelector('input');
+
+ const events = [
+ 'change',
+ 'click',
+ 'dblclick',
+ 'input',
+ 'mousedown',
+ 'mouseenter',
+ 'mouseleave',
+ 'mousemove',
+ 'mouseout',
+ 'mouseover',
+ 'mouseup',
+ ];
+
+ for (let event of events) {
+ checkbox.addEventListener(event, () => {
+ if (['change', 'click', 'dblclick', 'input'].includes(event) === true) {
+ result.check = checkbox.checked;
+ }
+
+ result.events.push(event);
+ }, false);
+ }
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/drag-and-drop.html b/remote/test/puppeteer/test/assets/input/drag-and-drop.html
new file mode 100644
index 0000000000..b77870c4ad
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/drag-and-drop.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Drag-and-drop test</title>
+ <style>
+ #drop {
+ width: 5em;
+ height: 5em;
+ border: 1px solid black;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="drag" draggable="true">drag me</div>
+ <div id="drop"></div>
+ <div id="drag-state">0</div>
+ <script>
+ const drag = document.getElementById('drag');
+ const drop = document.getElementById('drop');
+ drag.addEventListener('dragstart', function(event) {
+ event.dataTransfer.setData('id', event.target.id);
+ document.getElementById('drag-state').textContent += '1';
+ });
+ drop.addEventListener('dragenter', function(event) {
+ event.preventDefault();
+ document.getElementById('drag-state').textContent += '2';
+ });
+ drop.addEventListener('dragover', function(event) {
+ event.preventDefault();
+ document.getElementById('drag-state').textContent += '3';
+ });
+ drop.addEventListener('drop', function(event) {
+ event.preventDefault();
+ const id = event.dataTransfer.getData('id');
+ const el = document.getElementById(id);
+ if (el) {
+ event.target.appendChild(el);
+ document.getElementById('drag-state').textContent += '4';
+ }
+ });
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/fileupload.html b/remote/test/puppeteer/test/assets/input/fileupload.html
new file mode 100644
index 0000000000..55fd7c5006
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/fileupload.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>File upload test</title>
+ </head>
+ <body>
+ <input type="file">
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/input/keyboard.html b/remote/test/puppeteer/test/assets/input/keyboard.html
new file mode 100644
index 0000000000..2f4b7d33c2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/keyboard.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Keyboard test</title>
+ </head>
+ <body>
+ <textarea></textarea>
+ <script>
+ window.result = "";
+ let textarea = document.querySelector('textarea');
+ textarea.focus();
+ textarea.addEventListener('keydown', event => {
+ log('Keydown:', event.key, event.code, modifiers(event));
+ });
+ textarea.addEventListener('input', event => {
+ log('input:', event.data, event.inputType, event.isComposing);
+ });
+ textarea.addEventListener('keyup', event => {
+ log('Keyup:', event.key, event.code, modifiers(event));
+ });
+ function modifiers(event) {
+ let m = [];
+ if (event.altKey)
+ m.push('Alt')
+ if (event.ctrlKey)
+ m.push('Control');
+ if (event.shiftKey)
+ m.push('Shift')
+ return '[' + m.join(' ') + ']';
+ }
+ function log(...args) {
+ console.log.apply(console, args);
+ result += args.join(' ') + '\n';
+ }
+ function getResult() {
+ let temp = result.trim();
+ result = "";
+ return temp;
+ }
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/input/mouse-helper.js b/remote/test/puppeteer/test/assets/input/mouse-helper.js
new file mode 100644
index 0000000000..97a764aa80
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/mouse-helper.js
@@ -0,0 +1,74 @@
+// This injects a box into the page that moves with the mouse;
+// Useful for debugging
+(function () {
+ const box = document.createElement('div');
+ box.classList.add('mouse-helper');
+ const styleElement = document.createElement('style');
+ styleElement.innerHTML = `
+ .mouse-helper {
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 20px;
+ height: 20px;
+ background: rgba(0,0,0,.4);
+ border: 1px solid white;
+ border-radius: 10px;
+ margin-left: -10px;
+ margin-top: -10px;
+ transition: background .2s, border-radius .2s, border-color .2s;
+ }
+ .mouse-helper.button-1 {
+ transition: none;
+ background: rgba(0,0,0,0.9);
+ }
+ .mouse-helper.button-2 {
+ transition: none;
+ border-color: rgba(0,0,255,0.9);
+ }
+ .mouse-helper.button-3 {
+ transition: none;
+ border-radius: 4px;
+ }
+ .mouse-helper.button-4 {
+ transition: none;
+ border-color: rgba(255,0,0,0.9);
+ }
+ .mouse-helper.button-5 {
+ transition: none;
+ border-color: rgba(0,255,0,0.9);
+ }
+ `;
+ document.head.appendChild(styleElement);
+ document.body.appendChild(box);
+ document.addEventListener(
+ 'mousemove',
+ (event) => {
+ box.style.left = event.pageX + 'px';
+ box.style.top = event.pageY + 'px';
+ updateButtons(event.buttons);
+ },
+ true
+ );
+ document.addEventListener(
+ 'mousedown',
+ (event) => {
+ updateButtons(event.buttons);
+ box.classList.add('button-' + event.which);
+ },
+ true
+ );
+ document.addEventListener(
+ 'mouseup',
+ (event) => {
+ updateButtons(event.buttons);
+ box.classList.remove('button-' + event.which);
+ },
+ true
+ );
+ function updateButtons(buttons) {
+ for (let i = 0; i < 5; i++)
+ {box.classList.toggle('button-' + i, buttons & (1 << i));}
+ }
+})();
diff --git a/remote/test/puppeteer/test/assets/input/rotatedButton.html b/remote/test/puppeteer/test/assets/input/rotatedButton.html
new file mode 100644
index 0000000000..1bce66cf5e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/rotatedButton.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Rotated button test</title>
+ </head>
+ <body>
+ <script src="mouse-helper.js"></script>
+ <button onclick="clicked();">Click target</button>
+ <style>
+ button {
+ transform: rotateY(180deg);
+ }
+ </style>
+ <script>
+ window.result = 'Was not clicked';
+ function clicked() {
+ result = 'Clicked';
+ }
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/scrollable.html b/remote/test/puppeteer/test/assets/input/scrollable.html
new file mode 100644
index 0000000000..75757824a4
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/scrollable.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Scrollable test</title>
+ </head>
+ <body>
+ <script src='mouse-helper.js'></script>
+ <script>
+ for (let i = 0; i < 100; i++) {
+ let button = document.createElement('button');
+ button.textContent = i + ': not clicked';
+ button.id = 'button-' + i;
+ button.onclick = () => button.textContent = 'clicked';
+ button.oncontextmenu = event => {
+ if (![2].includes(event.button)) {
+ return;
+ }
+ event.preventDefault();
+ button.textContent = 'context menu';
+ }
+ button.onmouseup = event => {
+ if (![1,3,4].includes(event.button)) {
+ return;
+ }
+ event.preventDefault();
+ button.textContent = {
+ 3: 'back click',
+ 4: 'forward click',
+ 1: 'aux click',
+ }[event.button];
+ }
+ document.body.appendChild(button);
+ document.body.appendChild(document.createElement('br'));
+ }
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/select.html b/remote/test/puppeteer/test/assets/input/select.html
new file mode 100644
index 0000000000..026d48e328
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/select.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Selection Test</title>
+ </head>
+ <body>
+ <select>
+ <option value="">Empty</option>
+ <option value="black">Black</option>
+ <option value="blue">Blue</option>
+ <option value="brown">Brown</option>
+ <option value="cyan">Cyan</option>
+ <option value="gray">Gray</option>
+ <option value="green">Green</option>
+ <option value="indigo">Indigo</option>
+ <option value="magenta">Magenta</option>
+ <option value="orange">Orange</option>
+ <option value="pink">Pink</option>
+ <option value="purple">Purple</option>
+ <option value="red">Red</option>
+ <option value="violet">Violet</option>
+ <option value="white">White</option>
+ <option value="yellow">Yellow</option>
+ </select>
+ <script>
+ window.result = {
+ onInput: null,
+ onChange: null,
+ onBubblingChange: null,
+ onBubblingInput: null,
+ };
+
+ let select = document.querySelector('select');
+
+ function makeEmpty() {
+ for (let i = select.options.length - 1; i >= 0; --i) {
+ select.remove(i);
+ }
+ }
+
+ function makeMultiple() {
+ select.setAttribute('multiple', true);
+ }
+
+ select.addEventListener('input', () => {
+ result.onInput = Array.from(select.querySelectorAll('option:checked')).map((option) => {
+ return option.value;
+ });
+ }, false);
+
+ select.addEventListener('change', () => {
+ result.onChange = Array.from(select.querySelectorAll('option:checked')).map((option) => {
+ return option.value;
+ });
+ }, false);
+
+ document.body.addEventListener('input', () => {
+ result.onBubblingInput = Array.from(select.querySelectorAll('option:checked')).map((option) => {
+ return option.value;
+ });
+ }, false);
+
+ document.body.addEventListener('change', () => {
+ result.onBubblingChange = Array.from(select.querySelectorAll('option:checked')).map((option) => {
+ return option.value;
+ });
+ }, false);
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/textarea.html b/remote/test/puppeteer/test/assets/input/textarea.html
new file mode 100644
index 0000000000..66fdc40304
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/textarea.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Textarea test</title>
+ </head>
+ <body>
+ <textarea rows="5" cols="20"></textarea>
+ <script src='mouse-helper.js'></script>
+ <script>
+ globalThis.result = '';
+ globalThis.textarea = document.querySelector('textarea');
+ textarea.addEventListener('input', () => result = textarea.value, false);
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/touchscreen.html b/remote/test/puppeteer/test/assets/input/touchscreen.html
new file mode 100644
index 0000000000..76e31c97f9
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/touchscreen.html
@@ -0,0 +1,122 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Touch test</title>
+ </head>
+
+ <body>
+ <style>
+ button {
+ box-sizing: border-box;
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 10px;
+ height: 10px;
+ padding: 0;
+ margin: 0;
+ }
+ </style>
+ <button>Click target</button>
+ <script>
+ var allEvents = [];
+ globalThis.addEventListener(
+ "touchstart",
+ (event) => {
+ allEvents.push({
+ type: "touchstart",
+ touches: [...event.changedTouches].map((touch) => [
+ touch.clientX,
+ touch.clientY,
+ touch.radiusX,
+ touch.radiusY,
+ ]),
+ });
+ },
+ true,
+ );
+ globalThis.addEventListener(
+ "touchmove",
+ (event) => {
+ allEvents.push({
+ type: "touchmove",
+ touches: [...event.changedTouches].map((touch) => [
+ touch.clientX,
+ touch.clientY,
+ touch.radiusX,
+ touch.radiusY,
+ ]),
+ });
+ },
+ true,
+ );
+ globalThis.addEventListener(
+ "touchend",
+ (event) => {
+ allEvents.push({
+ type: "touchend",
+ touches: [...event.changedTouches].map((touch) => [
+ touch.clientX,
+ touch.clientY,
+ touch.radiusX,
+ touch.radiusY,
+ ])
+ });
+ },
+ true,
+ );
+ globalThis.addEventListener(
+ "pointerdown",
+ (event) => {
+ allEvents.push({
+ type: "pointerdown",
+ x: event.x,
+ y: event.y,
+ width: event.width,
+ height: event.height,
+ });
+ },
+ true,
+ );
+ globalThis.addEventListener(
+ "pointermove",
+ (event) => {
+ allEvents.push({
+ type: "pointermove",
+ x: event.x,
+ y: event.y,
+ width: event.width,
+ height: event.height,
+ });
+ },
+ true,
+ );
+ globalThis.addEventListener(
+ "pointerup",
+ (event) => {
+ allEvents.push({
+ type: "pointerup",
+ x: event.x,
+ y: event.y,
+ width: event.width,
+ height: event.height,
+ });
+ },
+ true,
+ );
+ globalThis.addEventListener(
+ "click",
+ (event) => {
+ allEvents.push({
+ type: "click",
+ x: event.x,
+ y: event.y,
+ width: event.width,
+ height: event.height,
+ });
+ },
+ true,
+ );
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/wheel.html b/remote/test/puppeteer/test/assets/input/wheel.html
new file mode 100644
index 0000000000..3d093a993e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/wheel.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ body {
+ min-height: 100vh;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ div {
+ width: 105px;
+ height: 105px;
+ background: #cdf;
+ padding: 5px;
+ }
+ </style>
+ <title>Element: wheel event - Scaling_an_element_via_the_wheel - code sample</title>
+ </head>
+ <body>
+ <div>Scale me with your mouse wheel.</div>
+ <script>
+ function zoom(event) {
+ event.preventDefault();
+
+ scale += event.deltaY * -0.01;
+
+ // Restrict scale
+ scale = Math.min(Math.max(.125, scale), 4);
+
+ // Apply scale transform
+ el.style.transform = `scale(${scale})`;
+ }
+
+ let scale = 1;
+ const el = document.querySelector('div');
+ el.onwheel = zoom;
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/eval.html b/remote/test/puppeteer/test/assets/jscoverage/eval.html
new file mode 100644
index 0000000000..838ae28763
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/eval.html
@@ -0,0 +1 @@
+<script>eval('console.log("foo")')</script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/involved.html b/remote/test/puppeteer/test/assets/jscoverage/involved.html
new file mode 100644
index 0000000000..fcc32ba2ca
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/involved.html
@@ -0,0 +1,16 @@
+<script>
+function foo() {
+ if (1 > 2)
+ console.log(1);
+ if (1 < 2)
+ console.log(2);
+ let x = 1 > 2 ? 'foo' : 'bar';
+ let y = 1 < 2 ? 'foo' : 'bar';
+ let p = {a:1 > 2?function(){console.log('unused');}:function(){console.log('unused');}};
+ let z = () => {};
+ let q = () => {};
+ q();
+}
+
+foo();
+</script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/multiple.html b/remote/test/puppeteer/test/assets/jscoverage/multiple.html
new file mode 100644
index 0000000000..bdef59885b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/multiple.html
@@ -0,0 +1,2 @@
+<script src='script1.js'></script>
+<script src='script2.js'></script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/ranges.html b/remote/test/puppeteer/test/assets/jscoverage/ranges.html
new file mode 100644
index 0000000000..3d02670aea
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/ranges.html
@@ -0,0 +1,2 @@
+<script>
+function unused(){}console.log('used!');if(true===false)console.log('unused!');</script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/script1.js b/remote/test/puppeteer/test/assets/jscoverage/script1.js
new file mode 100644
index 0000000000..3bd241b50e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/script1.js
@@ -0,0 +1 @@
+console.log(3);
diff --git a/remote/test/puppeteer/test/assets/jscoverage/script2.js b/remote/test/puppeteer/test/assets/jscoverage/script2.js
new file mode 100644
index 0000000000..3bd241b50e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/script2.js
@@ -0,0 +1 @@
+console.log(3);
diff --git a/remote/test/puppeteer/test/assets/jscoverage/simple.html b/remote/test/puppeteer/test/assets/jscoverage/simple.html
new file mode 100644
index 0000000000..49eeeea6ae
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/simple.html
@@ -0,0 +1,2 @@
+<script>
+function foo() {function bar() { } console.log(1); } foo(); </script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html
new file mode 100644
index 0000000000..e477750320
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html
@@ -0,0 +1,4 @@
+<script>
+console.log(1);
+//# sourceURL=nicename.js
+</script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/unused.html b/remote/test/puppeteer/test/assets/jscoverage/unused.html
new file mode 100644
index 0000000000..59c4a5a70b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/unused.html
@@ -0,0 +1 @@
+<script>function foo() { }</script>
diff --git a/remote/test/puppeteer/test/assets/lazy-oopif-frame.html b/remote/test/puppeteer/test/assets/lazy-oopif-frame.html
new file mode 100644
index 0000000000..83a420d029
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/lazy-oopif-frame.html
@@ -0,0 +1,3 @@
+<iframe width="100%" height="300" src="about:blank"></iframe>
+<div style="height: 800vh"></div>
+<iframe width="100%" height="300" src="https://www.example.com" loading="lazy"></iframe>
diff --git a/remote/test/puppeteer/test/assets/main-frame.html b/remote/test/puppeteer/test/assets/main-frame.html
new file mode 100644
index 0000000000..0c50feff85
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/main-frame.html
@@ -0,0 +1,10 @@
+<script>
+ window.addEventListener('DOMContentLoaded', () => {
+ const iframe = document.createElement('iframe');
+ const url = new URL(location.href);
+ url.hostname = 'inner-frame1.test';
+ url.pathname = '/inner-frame1.html';
+ iframe.src = url.toString();
+ document.body.appendChild(iframe);
+ }, false);
+</script>
diff --git a/remote/test/puppeteer/test/assets/mobile.html b/remote/test/puppeteer/test/assets/mobile.html
new file mode 100644
index 0000000000..8e94b2fe29
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/mobile.html
@@ -0,0 +1 @@
+<meta name = "viewport" content = "initial-scale = 1, user-scalable = no">
diff --git a/remote/test/puppeteer/test/assets/modernizr.js b/remote/test/puppeteer/test/assets/modernizr.js
new file mode 100644
index 0000000000..7991a4ec40
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/modernizr.js
@@ -0,0 +1,3 @@
+/*! modernizr 3.5.0 (Custom Build) | MIT *
+* https://modernizr.com/download/?-touchevents-setclasses !*/
+!function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t<n.options.aliases.length;t++)e.push(n.options.aliases[t].toLowerCase());for(s=o(n.fn,"function")?n.fn():n.fn,a=0;a<e.length;a++)i=e[a],r=i.split("."),1===r.length?Modernizr[r[0]]=s:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=s),f.push((s?"":"no-")+r.join("-"))}}function a(e){var n=u.className,t=Modernizr._config.classPrefix||"";if(p&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(o,"$1"+t+"js$2")}Modernizr._config.enableClasses&&(n+=" "+t+e.join(" "+t),p?u.className.baseVal=n:u.className=n)}function i(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):p?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function r(){var e=n.body;return e||(e=i(p?"svg":"body"),e.fake=!0),e}function l(e,t,o,s){var a,l,f,c,d="modernizr",p=i("div"),h=r();if(parseInt(o,10))for(;o--;)f=i("div"),f.id=s?s[o]:d+(o+1),p.appendChild(f);return a=i("style"),a.type="text/css",a.id="s"+d,(h.fake?h:p).appendChild(a),h.appendChild(p),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(n.createTextNode(e)),p.id=d,h.fake&&(h.style.background="",h.style.overflow="hidden",c=u.style.overflow,u.style.overflow="hidden",u.appendChild(h)),l=t(p,e),h.fake?(h.parentNode.removeChild(h),u.style.overflow=c,u.offsetHeight):p.parentNode.removeChild(p),!!l}var f=[],c=[],d={_version:"3.5.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){c.push({name:e,fn:n,options:t})},addAsyncTest:function(e){c.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=d,Modernizr=new Modernizr;var u=n.documentElement,p="svg"===u.nodeName.toLowerCase(),h=d._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];d._prefixes=h;var m=d.testStyles=l;Modernizr.addTest("touchevents",function(){var t;if("ontouchstart"in e||e.DocumentTouch&&n instanceof DocumentTouch)t=!0;else{var o=["@media (",h.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");m(o,function(e){t=9===e.offsetTop})}return t}),s(),a(f),delete d.addTest,delete d.addAsyncTest;for(var v=0;v<Modernizr._q.length;v++)Modernizr._q[v]();e.Modernizr=Modernizr}(window,document);
diff --git a/remote/test/puppeteer/test/assets/networkidle.html b/remote/test/puppeteer/test/assets/networkidle.html
new file mode 100644
index 0000000000..910ae1736d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/networkidle.html
@@ -0,0 +1,19 @@
+<script>
+ async function sleep(delay) {
+ return new Promise(resolve => setTimeout(resolve, delay));
+ }
+
+ async function main() {
+ const roundOne = Promise.all([
+ fetch('fetch-request-a.js'),
+ fetch('fetch-request-b.js'),
+ fetch('fetch-request-c.js'),
+ ]);
+
+ await roundOne;
+ await sleep(50);
+ await fetch('fetch-request-d.js');
+ }
+
+ main();
+</script>
diff --git a/remote/test/puppeteer/test/assets/offscreenbuttons.html b/remote/test/puppeteer/test/assets/offscreenbuttons.html
new file mode 100644
index 0000000000..e487caf4d3
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/offscreenbuttons.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<style>
+ button {
+ position: absolute;
+ width: 100px;
+ height: 20px;
+ }
+
+ #btn0 { right: 0px; top: 0; }
+ #btn1 { right: -10px; top: 25px; }
+ #btn2 { right: -20px; top: 50px; }
+ #btn3 { right: -30px; top: 75px; }
+ #btn4 { right: -40px; top: 100px; }
+ #btn5 { right: -50px; top: 125px; }
+ #btn6 { right: -60px; top: 150px; }
+ #btn7 { right: -70px; top: 175px; }
+ #btn8 { right: -80px; top: 200px; }
+ #btn9 { right: -90px; top: 225px; }
+ #btn10 { right: -100px; top: 250px; }
+ #btn11 { right: -99.999px; top: 275px; }
+</style>
+<button id=btn0>0</button>
+<button id=btn1>1</button>
+<button id=btn2>2</button>
+<button id=btn3>3</button>
+<button id=btn4>4</button>
+<button id=btn5>5</button>
+<button id=btn6>6</button>
+<button id=btn7>7</button>
+<button id=btn8>8</button>
+<button id=btn9>9</button>
+<button id=btn10>10</button>
+<button id=btn11>11</button>
+<script>
+ for (const button of document.querySelectorAll('button')) {
+ button.addEventListener('click', () => {
+ console.log(`button #${button.textContent} clicked`);
+ });
+ }
+</script>
diff --git a/remote/test/puppeteer/test/assets/one-style.css b/remote/test/puppeteer/test/assets/one-style.css
new file mode 100644
index 0000000000..7b26410d8a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/one-style.css
@@ -0,0 +1,3 @@
+body {
+ background-color: pink;
+}
diff --git a/remote/test/puppeteer/test/assets/one-style.html b/remote/test/puppeteer/test/assets/one-style.html
new file mode 100644
index 0000000000..4760f2b9f7
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/one-style.html
@@ -0,0 +1,2 @@
+<link rel='stylesheet' href='./one-style.css'>
+<div>hello, world!</div>
diff --git a/remote/test/puppeteer/test/assets/oopif.html b/remote/test/puppeteer/test/assets/oopif.html
new file mode 100644
index 0000000000..f04b9127af
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/oopif.html
@@ -0,0 +1,5 @@
+<a id="navigate-within-document" href="#nav">Navigate within document</a>
+<a name="nav"></a>
+<script>
+ fetch('oopif.html?requestFromOOPIF')
+</script>
diff --git a/remote/test/puppeteer/test/assets/p-selectors.html b/remote/test/puppeteer/test/assets/p-selectors.html
new file mode 100644
index 0000000000..24900623d8
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/p-selectors.html
@@ -0,0 +1,15 @@
+<div id="a">hello <button id="b">world</button>
+ <span id="f"></span>
+ <div id="c"></div>
+</div>
+<a>My name is Jun (pronounced like "June")</a>
+
+<script>
+ const topShadow = document.querySelector('#c');
+ topShadow.attachShadow({ mode: "open" });
+ topShadow.shadowRoot.innerHTML = `shadow dom<div id="d"></div>`;
+
+ const innerShadow = topShadow.shadowRoot.querySelector('#d');
+ innerShadow.attachShadow({ mode: "open" });
+ innerShadow.shadowRoot.innerHTML = `<a id="e">deep text</a>`;
+</script> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/pdf.html b/remote/test/puppeteer/test/assets/pdf.html
new file mode 100644
index 0000000000..987df27ebe
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/pdf.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>PDF</title>
+ </head>
+ <body>
+ <div>PDF Content</div>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/picture.html b/remote/test/puppeteer/test/assets/picture.html
new file mode 100644
index 0000000000..18e3d70f5e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/picture.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<img
+ srcset="logo-1x.png, logo-2x.png 2x, logo-3x.png 3x"
+ src="logo-1x.png"
+ height="320"
+ width="320" /> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/playground.html b/remote/test/puppeteer/test/assets/playground.html
new file mode 100644
index 0000000000..828cfb1c70
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/playground.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Playground</title>
+ </head>
+ <body>
+ <button>A button</button>
+ <textarea>A text area</textarea>
+ <div id="first">First div</div>
+ <div id="second">
+ Second div
+ <span class="inner">Inner span</span>
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/popup/popup.html b/remote/test/puppeteer/test/assets/popup/popup.html
new file mode 100644
index 0000000000..b855162c25
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/popup/popup.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Popup</title>
+ </head>
+ <body>
+ I am a popup
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/popup/window-open.html b/remote/test/puppeteer/test/assets/popup/window-open.html
new file mode 100644
index 0000000000..d138be1d22
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/popup/window-open.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Popup test</title>
+ </head>
+ <body>
+ <script>
+ window.open('./popup.html');
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/pptr.png b/remote/test/puppeteer/test/assets/pptr.png
new file mode 100644
index 0000000000..65d87c68e6
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/pptr.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/prerender/index.html b/remote/test/puppeteer/test/assets/prerender/index.html
new file mode 100644
index 0000000000..e0eecb717d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/prerender/index.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<head>
+<script>
+ function addRules() {
+ const script = document.createElement('script');
+ script.type = 'speculationrules';
+ script.innerText = `
+ {
+ "prerender": [
+ {"source": "list", "urls": ["target.html"]}
+ ]
+ }
+ `;
+ document.head.append(script);
+ }
+</script>
+</head>
+<body>
+ <button onclick="addRules()">add rules</button>
+ <a href="target.html">test</a>
+</body>
diff --git a/remote/test/puppeteer/test/assets/prerender/target.html b/remote/test/puppeteer/test/assets/prerender/target.html
new file mode 100644
index 0000000000..f384b3cbb0
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/prerender/target.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<head>
+ <script>fetch('target.html?fromPrerendered')</script>
+</head>
+<body>target<input></input></body>
diff --git a/remote/test/puppeteer/test/assets/resetcss.html b/remote/test/puppeteer/test/assets/resetcss.html
new file mode 100644
index 0000000000..e4e04b1f8a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/resetcss.html
@@ -0,0 +1,50 @@
+<style>
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+</style>
diff --git a/remote/test/puppeteer/test/assets/resolution.html b/remote/test/puppeteer/test/assets/resolution.html
new file mode 100644
index 0000000000..6d9f59ef9f
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/resolution.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<style>
+ p {
+ color: transparent;
+ }
+ @media (resolution: 1dppx) {
+ p {
+ font-size: 1px;
+ }
+ }
+ @media (resolution: 2dppx) {
+ p {
+ font-size: 2px;
+ }
+ }
+ @media (resolution: 3dppx) {
+ p {
+ font-size: 3px;
+ }
+ }
+ </style>
+ <p>Test</p>
+ \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/self-request.html b/remote/test/puppeteer/test/assets/self-request.html
new file mode 100644
index 0000000000..88aff620ff
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/self-request.html
@@ -0,0 +1,5 @@
+<script>
+var req = new XMLHttpRequest();
+req.open('GET', '/self-request.html');
+req.send(null);
+</script>
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html
new file mode 100644
index 0000000000..bef85d985b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html
@@ -0,0 +1,3 @@
+<script>
+ window.registrationPromise = navigator.serviceWorker.register('sw.js');
+</script>
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js b/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js
new file mode 100644
index 0000000000..8b1a393741
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js
@@ -0,0 +1 @@
+// empty
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json b/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json
new file mode 100644
index 0000000000..25828b6d2b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Simple extension",
+ "version": "0.1",
+ "background": {
+ "service_worker": "background.js"
+ },
+ "permissions": ["background", "activeTab"],
+ "manifest_version": 3
+}
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css
new file mode 100644
index 0000000000..7b26410d8a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css
@@ -0,0 +1,3 @@
+body {
+ background-color: pink;
+}
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html
new file mode 100644
index 0000000000..a9d28acb09
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html
@@ -0,0 +1,5 @@
+<link rel="stylesheet" href="./style.css">
+<script>
+ window.registrationPromise = navigator.serviceWorker.register('sw.js');
+ window.activationPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+</script>
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js
new file mode 100644
index 0000000000..21381484b6
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js
@@ -0,0 +1,7 @@
+self.addEventListener('fetch', (event) => {
+ event.respondWith(fetch(event.request));
+});
+
+self.addEventListener('activate', (event) => {
+ event.waitUntil(clients.claim());
+});
diff --git a/remote/test/puppeteer/test/assets/shadow.html b/remote/test/puppeteer/test/assets/shadow.html
new file mode 100644
index 0000000000..3796ca768c
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/shadow.html
@@ -0,0 +1,17 @@
+<script>
+
+let h1 = null;
+window.button = null;
+window.clicked = false;
+
+window.addEventListener('DOMContentLoaded', () => {
+ const shadowRoot = document.body.attachShadow({mode: 'open'});
+ h1 = document.createElement('h1');
+ h1.textContent = 'Hellow Shadow DOM v1';
+ button = document.createElement('button');
+ button.textContent = 'Click';
+ button.addEventListener('click', () => clicked = true);
+ shadowRoot.appendChild(h1);
+ shadowRoot.appendChild(button);
+});
+</script>
diff --git a/remote/test/puppeteer/test/assets/simple-extension/content-script.js b/remote/test/puppeteer/test/assets/simple-extension/content-script.js
new file mode 100644
index 0000000000..0fd83b90f1
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/simple-extension/content-script.js
@@ -0,0 +1,2 @@
+console.log('hey from the content-script');
+self.thisIsTheContentScript = true;
diff --git a/remote/test/puppeteer/test/assets/simple-extension/index.js b/remote/test/puppeteer/test/assets/simple-extension/index.js
new file mode 100644
index 0000000000..a0bb3f4eae
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/simple-extension/index.js
@@ -0,0 +1,2 @@
+// Mock script for background extension
+window.MAGIC = 42;
diff --git a/remote/test/puppeteer/test/assets/simple-extension/manifest.json b/remote/test/puppeteer/test/assets/simple-extension/manifest.json
new file mode 100644
index 0000000000..da2cd082ed
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/simple-extension/manifest.json
@@ -0,0 +1,14 @@
+{
+ "name": "Simple extension",
+ "version": "0.1",
+ "background": {
+ "scripts": ["index.js"]
+ },
+ "content_scripts": [{
+ "matches": ["<all_urls>"],
+ "css": [],
+ "js": ["content-script.js"]
+ }],
+ "permissions": ["background", "activeTab"],
+ "manifest_version": 2
+}
diff --git a/remote/test/puppeteer/test/assets/simple.json b/remote/test/puppeteer/test/assets/simple.json
new file mode 100644
index 0000000000..6d95903051
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/simple.json
@@ -0,0 +1 @@
+{"foo": "bar"}
diff --git a/remote/test/puppeteer/test/assets/tamperable.html b/remote/test/puppeteer/test/assets/tamperable.html
new file mode 100644
index 0000000000..d027e97038
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/tamperable.html
@@ -0,0 +1,3 @@
+<script>
+ window.result = window.injected;
+</script> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/title.html b/remote/test/puppeteer/test/assets/title.html
new file mode 100644
index 0000000000..88a86ce412
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/title.html
@@ -0,0 +1 @@
+<title>Woof-Woof</title>
diff --git a/remote/test/puppeteer/test/assets/worker/worker.html b/remote/test/puppeteer/test/assets/worker/worker.html
new file mode 100644
index 0000000000..7de2d9fd9e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/worker/worker.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Worker test</title>
+ </head>
+ <body>
+ <script>
+ var worker = new Worker('worker.js');
+ worker.onmessage = function(message) {
+ console.log(message.data);
+ };
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/worker/worker.js b/remote/test/puppeteer/test/assets/worker/worker.js
new file mode 100644
index 0000000000..0626f13e58
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/worker/worker.js
@@ -0,0 +1,16 @@
+console.log('hello from the worker');
+
+function workerFunction() {
+ return 'worker function result';
+}
+
+self.addEventListener('message', (event) => {
+ console.log('got this data: ' + event.data);
+});
+
+(async function () {
+ while (true) {
+ self.postMessage(workerFunction.toString());
+ await new Promise((x) => setTimeout(x, 100));
+ }
+})();
diff --git a/remote/test/puppeteer/test/assets/wrappedlink.html b/remote/test/puppeteer/test/assets/wrappedlink.html
new file mode 100644
index 0000000000..429b6e9156
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/wrappedlink.html
@@ -0,0 +1,32 @@
+<style>
+:root {
+ font-family: monospace;
+}
+
+body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+div {
+ width: 10ch;
+ word-wrap: break-word;
+ border: 1px solid blue;
+ transform: rotate(33deg);
+ line-height: 8ch;
+ padding: 2ch;
+}
+
+a {
+ margin-left: 7ch;
+}
+</style>
+<div>
+ <a href='#clicked'>123321</a>
+</div>
+<script>
+ document.querySelector('a').addEventListener('click', () => {
+ window.__clicked = true;
+ });
+</script>
diff --git a/remote/test/puppeteer/test/fixtures/closeme.js b/remote/test/puppeteer/test/fixtures/closeme.js
new file mode 100644
index 0000000000..dbe798f70d
--- /dev/null
+++ b/remote/test/puppeteer/test/fixtures/closeme.js
@@ -0,0 +1,5 @@
+(async () => {
+ const [, , puppeteerRoot, options] = process.argv;
+ const browser = await require(puppeteerRoot).launch(JSON.parse(options));
+ console.log(browser.wsEndpoint());
+})();
diff --git a/remote/test/puppeteer/test/fixtures/dumpio.js b/remote/test/puppeteer/test/fixtures/dumpio.js
new file mode 100644
index 0000000000..a16cf4d633
--- /dev/null
+++ b/remote/test/puppeteer/test/fixtures/dumpio.js
@@ -0,0 +1,10 @@
+(async () => {
+ const [, , puppeteerRoot, options] = process.argv;
+ const browser = await require(puppeteerRoot).launch(JSON.parse(options));
+ const page = await browser.newPage();
+ await page.evaluate(() => {
+ return console.error('message from dumpio');
+ });
+ await page.close();
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt b/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt
new file mode 100644
index 0000000000..ecb1a6342f
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt
@@ -0,0 +1,20 @@
+[
+ {
+ "url": "http://localhost:<PORT>/csscoverage/involved.html",
+ "ranges": [
+ {
+ "start": 149,
+ "end": 297
+ },
+ {
+ "start": 306,
+ "end": 323
+ },
+ {
+ "start": 327,
+ "end": 433
+ }
+ ],
+ "text": "\n@charset \"utf-8\";\n@namespace svg url(http://www.w3.org/2000/svg);\n@font-face {\n font-family: \"Example Font\";\n src: url(\"./Dosis-Regular.ttf\");\n}\n\n#fluffy {\n border: 1px solid black;\n z-index: 1;\n /* -webkit-disabled-property: rgb(1, 2, 3) */\n -lol-cats: \"dogs\" /* non-existing property */\n}\n\n@media (min-width: 1px) {\n span {\n -webkit-border-radius: 10px;\n font-family: \"Example Font\";\n animation: 1s identifier;\n }\n}\n"
+ }
+] \ No newline at end of file
diff --git a/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio1.png b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio1.png
new file mode 100644
index 0000000000..c53502031f
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio1.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio2.png b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio2.png
new file mode 100644
index 0000000000..9d3e9fcc31
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio3.png b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio3.png
new file mode 100644
index 0000000000..3349dbd0ac
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio3.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png
new file mode 100644
index 0000000000..ff282e989b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png
new file mode 100644
index 0000000000..91a1cb8510
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png
new file mode 100644
index 0000000000..7b01753b6a
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png
new file mode 100644
index 0000000000..b9b8b2922b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt b/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt
new file mode 100644
index 0000000000..016b30bde8
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt
@@ -0,0 +1,36 @@
+[
+ {
+ "url": "http://localhost:<PORT>/jscoverage/involved.html",
+ "ranges": [
+ {
+ "start": 0,
+ "end": 35
+ },
+ {
+ "start": 50,
+ "end": 100
+ },
+ {
+ "start": 107,
+ "end": 141
+ },
+ {
+ "start": 148,
+ "end": 168
+ },
+ {
+ "start": 203,
+ "end": 204
+ },
+ {
+ "start": 238,
+ "end": 251
+ },
+ {
+ "start": 259,
+ "end": 298
+ }
+ ],
+ "text": "\nfunction foo() {\n if (1 > 2)\n console.log(1);\n if (1 < 2)\n console.log(2);\n let x = 1 > 2 ? 'foo' : 'bar';\n let y = 1 < 2 ? 'foo' : 'bar';\n let p = {a:1 > 2?function(){console.log('unused');}:function(){console.log('unused');}};\n let z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n"
+ }
+] \ No newline at end of file
diff --git a/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png b/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png
new file mode 100644
index 0000000000..8595e0598e
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png
new file mode 100644
index 0000000000..b010d1f87f
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png
new file mode 100644
index 0000000000..d713d27943
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png
new file mode 100644
index 0000000000..ac23b7de50
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png
new file mode 100644
index 0000000000..32e05bf05b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png
new file mode 100644
index 0000000000..cc8669d598
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png
new file mode 100644
index 0000000000..35c53377f9
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png
new file mode 100644
index 0000000000..5fcdb92355
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png
new file mode 100644
index 0000000000..917dd48188
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png
new file mode 100644
index 0000000000..d0c05ba795
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png
new file mode 100644
index 0000000000..917dd48188
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage-2.png b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage-2.png
new file mode 100644
index 0000000000..edc01c1041
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage-2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png
new file mode 100644
index 0000000000..d6d38217f7
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip-2.png b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip-2.png
new file mode 100644
index 0000000000..7ec69d3040
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip-2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png
new file mode 100644
index 0000000000..d7637631b7
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png b/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png
new file mode 100644
index 0000000000..ecab61fe17
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/transparent.png b/remote/test/puppeteer/test/golden-chrome/transparent.png
new file mode 100644
index 0000000000..1cf45d8688
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/transparent.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png
new file mode 100644
index 0000000000..4d74aac44c
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png
new file mode 100644
index 0000000000..78979425a9
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png
new file mode 100644
index 0000000000..79b4b0fa1b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png
new file mode 100644
index 0000000000..bede7c1ed0
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png
new file mode 100644
index 0000000000..d5f6bbec2e
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chrome/white.jpg b/remote/test/puppeteer/test/golden-chrome/white.jpg
new file mode 100644
index 0000000000..fb9070def3
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chrome/white.jpg
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio1.png b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio1.png
new file mode 100644
index 0000000000..8c814cf3f4
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio1.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio2.png b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio2.png
new file mode 100644
index 0000000000..a52579a1af
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio3.png b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio3.png
new file mode 100644
index 0000000000..d43e08f4ad
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio3.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png
new file mode 100644
index 0000000000..2e671db41c
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png
new file mode 100644
index 0000000000..a2a61af3d3
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png
new file mode 100644
index 0000000000..a6f69dd20a
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png
new file mode 100644
index 0000000000..5cce794edb
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png
new file mode 100644
index 0000000000..0a96e67f9a
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png
new file mode 100644
index 0000000000..63956b2a7c
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png
new file mode 100644
index 0000000000..f554b1d62c
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png
new file mode 100644
index 0000000000..5f58502b49
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png
new file mode 100644
index 0000000000..cc0eb7bfe4
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png
new file mode 100644
index 0000000000..fadcaa1207
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png
new file mode 100644
index 0000000000..0a78fb1ae7
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png
new file mode 100644
index 0000000000..fadcaa1207
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage-2.png b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage-2.png
new file mode 100644
index 0000000000..ac47ec83b1
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage-2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png
new file mode 100644
index 0000000000..ac47ec83b1
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip-2.png b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip-2.png
new file mode 100644
index 0000000000..f7c0830ba9
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip-2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png
new file mode 100644
index 0000000000..4c34e47fbd
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png
new file mode 100644
index 0000000000..f02ecae645
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/transparent.png b/remote/test/puppeteer/test/golden-firefox/transparent.png
new file mode 100644
index 0000000000..1cf45d8688
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/transparent.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/white.jpg b/remote/test/puppeteer/test/golden-firefox/white.jpg
new file mode 100644
index 0000000000..f04d7ec2ad
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/white.jpg
Binary files differ
diff --git a/remote/test/puppeteer/test/installation/.mocharc.cjs b/remote/test/puppeteer/test/installation/.mocharc.cjs
new file mode 100644
index 0000000000..5a797716e0
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/.mocharc.cjs
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @type {import('mocha').MochaOptions}
+ */
+module.exports = {
+ spec: ['build/**/*.spec.js'],
+ timeout: '240000ms',
+};
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js b/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js
new file mode 100644
index 0000000000..8f8fb329e7
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import 'puppeteer-core';
+import 'puppeteer-core/internal/revisions.js';
+import 'puppeteer-core/lib/esm/puppeteer/revisions.js';
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js b/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js
new file mode 100644
index 0000000000..4776d7e261
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import puppeteer from 'puppeteer-core';
+
+(async () => {
+ try {
+ await puppeteer.launch({
+ product: '${product}',
+ executablePath: 'node',
+ });
+ } catch (error) {
+ if (error.message.includes('Failed to launch the browser process')) {
+ process.exit(0);
+ }
+ console.error(error);
+ process.exit(1);
+ }
+})();
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs
new file mode 100644
index 0000000000..f4276f2589
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+require('puppeteer-core');
+require('puppeteer-core/internal/revisions.js');
+require('puppeteer-core/lib/cjs/puppeteer/revisions.js');
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js
new file mode 100644
index 0000000000..9e6ce241b2
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import puppeteer from 'puppeteer';
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+ await page.goto('http://example.com');
+ await page.$('aria/example');
+ await page.screenshot({path: 'example.png'});
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/basic.ts b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.ts
new file mode 100644
index 0000000000..28396d0096
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import puppeteer from 'puppeteer';
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+ await page.goto('http://example.com');
+ await page.$('aria/example');
+ await page.screenshot({path: 'example.png'});
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/bidi.js b/remote/test/puppeteer/test/installation/assets/puppeteer/bidi.js
new file mode 100644
index 0000000000..3e1df93654
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/bidi.js
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import puppeteer from 'puppeteer';
+(async () => {
+ const browser = await puppeteer.launch({
+ protocol: 'webDriverBiDi',
+ });
+ const page = await browser.newPage();
+ await page.goto('http://example.com');
+ await page.$('h1');
+ await page.screenshot({path: 'example.png'});
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs
new file mode 100644
index 0000000000..64a7b96681
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs
@@ -0,0 +1,8 @@
+const {join} = require('path');
+
+/**
+ * @type {import("puppeteer").Configuration}
+ */
+module.exports = {
+ cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
+};
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/puppeteer.config.ts b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/puppeteer.config.ts
new file mode 100644
index 0000000000..5bcb82ffc8
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/puppeteer.config.ts
@@ -0,0 +1,6 @@
+import {type Configuration} from 'puppeteer';
+import {join} from 'path';
+
+export default {
+ cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
+} satisfies Configuration;
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js b/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js
new file mode 100644
index 0000000000..cd742bafd5
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js
@@ -0,0 +1,10 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import 'puppeteer';
+
+// Should still be reachable.
+import 'puppeteer-core/internal/revisions.js';
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/installCanary.js b/remote/test/puppeteer/test/installation/assets/puppeteer/installCanary.js
new file mode 100644
index 0000000000..39a0113de9
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/installCanary.js
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ Browser,
+ detectBrowserPlatform,
+ install,
+ resolveBuildId,
+} from '@puppeteer/browsers';
+
+(async () => {
+ await install({
+ cacheDir: process.env['PUPPETEER_CACHE_DIR'],
+ browser: Browser.CHROME,
+ buildId: await resolveBuildId(
+ Browser.CHROME,
+ detectBrowserPlatform(),
+ 'canary'
+ ),
+ });
+})();
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs
new file mode 100644
index 0000000000..208eee9021
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs
@@ -0,0 +1,10 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+require('puppeteer');
+
+// Should still be reachable.
+require('puppeteer-core/internal/revisions.js');
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/trimCache.js b/remote/test/puppeteer/test/installation/assets/puppeteer/trimCache.js
new file mode 100644
index 0000000000..a810e2aac2
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/trimCache.js
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import puppeteer from 'puppeteer';
+
+(async () => {
+ await puppeteer.trimCache();
+})();
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/tsconfig.json b/remote/test/puppeteer/test/installation/assets/puppeteer/tsconfig.json
new file mode 100644
index 0000000000..ce77dbf8d9
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ },
+}
diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js b/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js
new file mode 100644
index 0000000000..30de2a4890
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export default {
+ mode: 'production',
+ entry: './index.js',
+ target: 'node',
+ externals: 'typescript',
+ output: {
+ path: process.cwd(),
+ filename: 'bundle.js',
+ },
+};
diff --git a/remote/test/puppeteer/test/installation/package.json b/remote/test/puppeteer/test/installation/package.json
new file mode 100644
index 0000000000..f5e804d99c
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@puppeteer-test/installation",
+ "version": "latest",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "build": "wireit",
+ "clean": "../../tools/clean.js",
+ "test": "mocha"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b",
+ "clean": "if-file-deleted",
+ "dependencies": [
+ "build:packages"
+ ],
+ "files": [
+ "tsconfig.json",
+ "src/**"
+ ],
+ "output": [
+ "build/**",
+ "tsconfig.tsbuildinfo"
+ ]
+ },
+ "build:packages": {
+ "command": "npm pack --quiet --workspace puppeteer --workspace puppeteer-core --workspace @puppeteer/browsers",
+ "dependencies": [
+ "../../packages/puppeteer:build",
+ "../../packages/puppeteer-core:build",
+ "../../packages/browsers:build"
+ ],
+ "files": [],
+ "output": [
+ "puppeteer-*.tgz"
+ ]
+ }
+ },
+ "files": [
+ ".mocharc.cjs",
+ "puppeteer-*.tgz",
+ "build",
+ "assets"
+ ],
+ "dependencies": {
+ "glob": "10.3.10",
+ "mocha": "10.2.0"
+ }
+}
diff --git a/remote/test/puppeteer/test/installation/src/browsers.spec.ts b/remote/test/puppeteer/test/installation/src/browsers.spec.ts
new file mode 100644
index 0000000000..0c91731455
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/browsers.spec.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import {spawnSync} from 'child_process';
+
+import {configureSandbox} from './sandbox.js';
+
+describe('`@puppeteer/browsers`', () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers'],
+ });
+
+ it('can launch CLI', async function () {
+ const result = spawnSync('npx', ['@puppeteer/browsers', '--help'], {
+ // npx is not found without the shell flag on Windows.
+ shell: process.platform === 'win32',
+ cwd: this.sandbox,
+ });
+ assert.strictEqual(result.status, 0);
+ assert.ok(
+ result.stdout
+ .toString('utf-8')
+ .startsWith('@puppeteer/browsers <command>')
+ );
+ });
+});
diff --git a/remote/test/puppeteer/test/installation/src/constants.ts b/remote/test/puppeteer/test/installation/src/constants.ts
new file mode 100644
index 0000000000..2b66b792d5
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/constants.ts
@@ -0,0 +1,25 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {dirname, join, resolve} from 'path';
+import {fileURLToPath} from 'url';
+
+import {globSync} from 'glob';
+
+export const PUPPETEER_CORE_PACKAGE_PATH = resolve(
+ globSync('puppeteer-core-*.tgz')[0]!
+);
+export const PUPPETEER_BROWSERS_PACKAGE_PATH = resolve(
+ globSync('puppeteer-browsers-[0-9]*.tgz')[0]!
+);
+export const PUPPETEER_PACKAGE_PATH = resolve(
+ globSync('puppeteer-[0-9]*.tgz')[0]!
+);
+export const ASSETS_DIR = join(
+ dirname(fileURLToPath(import.meta.url)),
+ '..',
+ 'assets'
+);
diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-cli.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-cli.spec.ts
new file mode 100644
index 0000000000..650cbc1832
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/puppeteer-cli.spec.ts
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import {spawnSync} from 'child_process';
+import {existsSync} from 'fs';
+import {readdir} from 'fs/promises';
+import {join} from 'path';
+
+import {configureSandbox} from './sandbox.js';
+
+describe('Puppeteer CLI', () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
+ env: cwd => {
+ return {
+ PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
+ PUPPETEER_SKIP_DOWNLOAD: 'true',
+ };
+ },
+ });
+
+ it('can launch', async function () {
+ const result = spawnSync('npx', ['puppeteer', '--help'], {
+ // npx is not found without the shell flag on Windows.
+ shell: process.platform === 'win32',
+ cwd: this.sandbox,
+ });
+ assert.strictEqual(result.status, 0);
+ assert.ok(
+ result.stdout.toString('utf-8').startsWith('puppeteer <command>')
+ );
+ });
+
+ it('can download a browser', async function () {
+ assert.ok(!existsSync(join(this.sandbox, '.cache', 'puppeteer')));
+ const result = spawnSync(
+ 'npx',
+ ['puppeteer', 'browsers', 'install', 'chrome'],
+ {
+ // npx is not found without the shell flag on Windows.
+ shell: process.platform === 'win32',
+ cwd: this.sandbox,
+ env: {
+ ...process.env,
+ PUPPETEER_CACHE_DIR: join(this.sandbox, '.cache', 'puppeteer'),
+ },
+ }
+ );
+ assert.strictEqual(result.status, 0);
+ const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
+ assert.equal(files.length, 1);
+ assert.equal(files[0], 'chrome');
+ });
+});
diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts
new file mode 100644
index 0000000000..1ed5511f6c
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts
@@ -0,0 +1,73 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import {readdir, writeFile} from 'fs/promises';
+import {join} from 'path';
+
+import {configureSandbox} from './sandbox.js';
+import {readAsset} from './util.js';
+
+describe('`puppeteer` with configuration', () => {
+ describe('cjs', () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
+ env: cwd => {
+ return {
+ PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
+ };
+ },
+ before: async cwd => {
+ await writeFile(
+ join(cwd, '.puppeteerrc.cjs'),
+ await readAsset('puppeteer', 'configuration', '.puppeteerrc.cjs')
+ );
+ },
+ });
+
+ it('evaluates', async function () {
+ const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
+ assert.equal(files.length, 2);
+ assert(files.includes('chrome'));
+ assert(files.includes('chrome-headless-shell'));
+
+ const script = await readAsset('puppeteer', 'basic.js');
+ await this.runScript(script, 'mjs');
+ });
+ });
+
+ describe('ts', () => {
+ configureSandbox({
+ dependencies: [
+ '@puppeteer/browsers',
+ 'puppeteer-core',
+ 'puppeteer',
+ 'typescript',
+ ],
+ env: cwd => {
+ return {
+ PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
+ };
+ },
+ before: async cwd => {
+ await writeFile(
+ join(cwd, 'puppeteer.config.ts'),
+ await readAsset('puppeteer', 'configuration', 'puppeteer.config.ts')
+ );
+ },
+ });
+
+ it('evaluates', async function () {
+ const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
+ assert.equal(files.length, 2);
+ assert(files.includes('chrome'));
+ assert(files.includes('chrome-headless-shell'));
+
+ const script = await readAsset('puppeteer', 'basic.js');
+ await this.runScript(script, 'mjs');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts
new file mode 100644
index 0000000000..9df19e1c85
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts
@@ -0,0 +1,34 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {configureSandbox} from './sandbox.js';
+import {readAsset} from './util.js';
+
+describe('`puppeteer-core`', () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core'],
+ });
+
+ it('evaluates CommonJS', async function () {
+ const script = await readAsset('puppeteer-core', 'requires.cjs');
+ await this.runScript(script, 'cjs');
+ });
+
+ it('evaluates ES modules', async function () {
+ const script = await readAsset('puppeteer-core', 'imports.js');
+ await this.runScript(script, 'mjs');
+ });
+
+ for (const product of ['firefox', 'chrome']) {
+ it(`\`launch\` for \`${product}\` with a bad \`executablePath\``, async function () {
+ const script = (await readAsset('puppeteer-core', 'launch.js')).replace(
+ '${product}',
+ product
+ );
+ await this.runScript(script, 'mjs');
+ });
+ }
+});
diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts
new file mode 100644
index 0000000000..b599af01dc
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import {readdir} from 'fs/promises';
+import {platform} from 'os';
+import {join} from 'path';
+
+import {configureSandbox} from './sandbox.js';
+import {readAsset} from './util.js';
+
+// Skipping this test on Windows as windows runners are much slower.
+(platform() === 'win32' ? describe.skip : describe)(
+ '`puppeteer` with Firefox',
+ () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
+ env: cwd => {
+ return {
+ PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
+ PUPPETEER_PRODUCT: 'firefox',
+ };
+ },
+ });
+
+ describe('with CDP', () => {
+ it('evaluates CommonJS', async function () {
+ const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
+ assert.equal(files.length, 1);
+ assert.equal(files[0], 'firefox');
+ const script = await readAsset('puppeteer-core', 'requires.cjs');
+ await this.runScript(script, 'cjs');
+ });
+
+ it('evaluates ES modules', async function () {
+ const script = await readAsset('puppeteer-core', 'imports.js');
+ await this.runScript(script, 'mjs');
+ });
+ });
+
+ describe('with WebDriverBiDi', () => {
+ it('evaluates ES modules', async function () {
+ const script = await readAsset('puppeteer', 'bidi.js');
+ await this.runScript(script, 'mjs');
+ });
+ });
+ }
+);
diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-typescript.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-typescript.spec.ts
new file mode 100644
index 0000000000..fc8ff133fb
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/puppeteer-typescript.spec.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {readFile, writeFile} from 'fs/promises';
+import {platform} from 'os';
+import {join} from 'path';
+
+import {configureSandbox} from './sandbox.js';
+import {execFile, readAsset} from './util.js';
+
+// Skipping this test on Windows as windows runners are much slower.
+(platform() === 'win32' ? describe.skip : describe)(
+ '`puppeteer` with TypeScript',
+ () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
+ devDependencies: ['typescript@4.7.4', '@types/node@16.3.3'],
+ env: cwd => {
+ return {
+ PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
+ };
+ },
+ });
+
+ it('should work', async function () {
+ // Write a Webpack configuration.
+ await writeFile(
+ join(this.sandbox, 'tsconfig.json'),
+ await readAsset('puppeteer', 'tsconfig.json')
+ );
+
+ // Write the source code.
+ await writeFile(
+ join(this.sandbox, 'index.ts'),
+ await readAsset('puppeteer', 'basic.ts')
+ );
+
+ // Compile.
+ await execFile('npx', ['tsc'], {cwd: this.sandbox, shell: true});
+
+ const script = await readFile(join(this.sandbox, 'index.js'), 'utf-8');
+
+ await this.runScript(script, 'cjs');
+ });
+ }
+);
diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts
new file mode 100644
index 0000000000..93902aec32
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {readFile, rm, writeFile} from 'fs/promises';
+import {join} from 'path';
+
+import {configureSandbox} from './sandbox.js';
+import {execFile, readAsset} from './util.js';
+
+describe('`puppeteer` with Webpack', () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
+ devDependencies: ['webpack', 'webpack-cli'],
+ env: cwd => {
+ return {
+ PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
+ };
+ },
+ });
+
+ it('evaluates WebPack Bundles', async function () {
+ // Write a Webpack configuration.
+ await writeFile(
+ join(this.sandbox, 'webpack.config.mjs'),
+ await readAsset('puppeteer', 'webpack', 'webpack.config.js')
+ );
+
+ // Write the source code.
+ await writeFile(
+ join(this.sandbox, 'index.js'),
+ await readAsset('puppeteer', 'basic.js')
+ );
+
+ // Bundle.
+ await execFile('npx', ['webpack'], {cwd: this.sandbox, shell: true});
+
+ // Remove `node_modules` to test independence.
+ await rm('node_modules', {recursive: true, force: true});
+
+ const script = await readFile(join(this.sandbox, 'bundle.js'), 'utf-8');
+
+ await this.runScript(script, 'cjs');
+ });
+});
diff --git a/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts
new file mode 100644
index 0000000000..d7b8757284
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts
@@ -0,0 +1,104 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+import {readdirSync} from 'fs';
+import {readdir} from 'fs/promises';
+import {platform} from 'os';
+import {join} from 'path';
+
+import {configureSandbox} from './sandbox.js';
+import {readAsset} from './util.js';
+
+describe('`puppeteer`', () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
+ env: cwd => {
+ return {
+ PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
+ };
+ },
+ });
+
+ it('evaluates CommonJS', async function () {
+ const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
+ assert.equal(files.length, 2);
+ assert(files.includes('chrome'));
+ assert(files.includes('chrome-headless-shell'));
+
+ const script = await readAsset('puppeteer-core', 'requires.cjs');
+ await this.runScript(script, 'cjs');
+ });
+
+ it('evaluates ES modules', async function () {
+ const script = await readAsset('puppeteer-core', 'imports.js');
+ await this.runScript(script, 'mjs');
+ });
+});
+
+// Skipping this test on Windows as windows runners are much slower.
+(platform() === 'win32' ? describe.skip : describe)(
+ '`puppeteer` with PUPPETEER_DOWNLOAD_PATH',
+ () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
+ env: cwd => {
+ return {
+ PUPPETEER_DOWNLOAD_PATH: join(cwd, '.cache', 'puppeteer'),
+ };
+ },
+ });
+
+ it('evaluates', async function () {
+ const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
+ assert.equal(files.length, 2);
+ assert(files.includes('chrome'));
+ assert(files.includes('chrome-headless-shell'));
+
+ const script = await readAsset('puppeteer', 'basic.js');
+ await this.runScript(script, 'mjs');
+ });
+ }
+);
+
+// Skipping this test on Windows as windows runners are much slower.
+(platform() === 'win32' ? describe.skip : describe)(
+ '`puppeteer` clears cache',
+ () => {
+ configureSandbox({
+ dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
+ env: cwd => {
+ return {
+ PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
+ };
+ },
+ });
+
+ it('evaluates', async function () {
+ assert.equal(
+ readdirSync(join(this.sandbox, '.cache', 'puppeteer', 'chrome')).length,
+ 1
+ );
+
+ await this.runScript(
+ await readAsset('puppeteer', 'installCanary.js'),
+ 'mjs'
+ );
+
+ assert.equal(
+ readdirSync(join(this.sandbox, '.cache', 'puppeteer', 'chrome')).length,
+ 2
+ );
+
+ await this.runScript(await readAsset('puppeteer', 'trimCache.js'), 'mjs');
+
+ assert.equal(
+ readdirSync(join(this.sandbox, '.cache', 'puppeteer', 'chrome')).length,
+ 1
+ );
+ });
+ }
+);
diff --git a/remote/test/puppeteer/test/installation/src/sandbox.ts b/remote/test/puppeteer/test/installation/src/sandbox.ts
new file mode 100644
index 0000000000..fde30dfcf9
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/sandbox.ts
@@ -0,0 +1,131 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import crypto from 'crypto';
+import {mkdtemp, rm, writeFile} from 'fs/promises';
+import {tmpdir} from 'os';
+import {join} from 'path';
+
+import {
+ PUPPETEER_CORE_PACKAGE_PATH,
+ PUPPETEER_PACKAGE_PATH,
+ PUPPETEER_BROWSERS_PACKAGE_PATH,
+} from './constants.js';
+import {execFile} from './util.js';
+
+const PKG_MANAGER = process.env['PKG_MANAGER'] || 'npm';
+
+let ADD_PKG_SUBCOMMAND = 'install';
+if (PKG_MANAGER !== 'npm') {
+ ADD_PKG_SUBCOMMAND = 'add';
+}
+
+export interface ItEvaluatesOptions {
+ commonjs?: boolean;
+}
+
+export interface ItEvaluatesFn {
+ (
+ title: string,
+ options: ItEvaluatesOptions,
+ getScriptContent: (cwd: string) => Promise<string>
+ ): void;
+ (title: string, getScriptContent: (cwd: string) => Promise<string>): void;
+}
+
+export interface SandboxOptions {
+ dependencies?: string[];
+ devDependencies?: string[];
+ /**
+ * This should be idempotent.
+ */
+ env?: ((cwd: string) => NodeJS.ProcessEnv) | NodeJS.ProcessEnv;
+ before?: (cwd: string) => Promise<void>;
+}
+
+declare module 'mocha' {
+ export interface Context {
+ /**
+ * The path to the root of the sandbox folder.
+ */
+ sandbox: string;
+ env: NodeJS.ProcessEnv | undefined;
+ runScript: (content: string, type: 'cjs' | 'mjs') => Promise<void>;
+ }
+}
+
+/**
+ * Configures mocha before/after hooks to create a temp folder and install
+ * specified dependencies.
+ */
+export const configureSandbox = (options: SandboxOptions): void => {
+ before(async function (): Promise<void> {
+ console.time('before');
+ const sandbox = await mkdtemp(join(tmpdir(), 'puppeteer-'));
+ const dependencies = (options.dependencies ?? []).map(module => {
+ switch (module) {
+ case 'puppeteer':
+ return PUPPETEER_PACKAGE_PATH;
+ case 'puppeteer-core':
+ return PUPPETEER_CORE_PACKAGE_PATH;
+ case '@puppeteer/browsers':
+ return PUPPETEER_BROWSERS_PACKAGE_PATH;
+ default:
+ return module;
+ }
+ });
+ const devDependencies = options.devDependencies ?? [];
+
+ let getEnv: (cwd: string) => NodeJS.ProcessEnv | undefined;
+ if (typeof options.env === 'function') {
+ getEnv = options.env;
+ } else {
+ const env = options.env;
+ getEnv = () => {
+ return env;
+ };
+ }
+ const env = {...process.env, ...getEnv(sandbox)};
+
+ await options.before?.(sandbox);
+ if (dependencies.length > 0) {
+ await execFile(PKG_MANAGER, [ADD_PKG_SUBCOMMAND, ...dependencies], {
+ cwd: sandbox,
+ env,
+ shell: true,
+ });
+ }
+ if (devDependencies.length > 0) {
+ await execFile(
+ PKG_MANAGER,
+ [ADD_PKG_SUBCOMMAND, '-D', ...devDependencies],
+ {
+ cwd: sandbox,
+ env,
+ shell: true,
+ }
+ );
+ }
+
+ this.sandbox = sandbox;
+ this.env = env;
+ this.runScript = async (content: string, type: 'cjs' | 'mjs') => {
+ const script = join(sandbox, `script-${crypto.randomUUID()}.${type}`);
+ await writeFile(script, content);
+ await execFile('node', [script], {cwd: sandbox, env});
+ };
+ console.timeEnd('before');
+ });
+
+ after(async function () {
+ console.time('after');
+ if (!process.env['KEEP_SANDBOX']) {
+ await rm(this.sandbox, {recursive: true, force: true, maxRetries: 5});
+ } else {
+ console.log('sandbox saved in', this.sandbox);
+ }
+ console.timeEnd('after');
+ });
+};
diff --git a/remote/test/puppeteer/test/installation/src/util.ts b/remote/test/puppeteer/test/installation/src/util.ts
new file mode 100644
index 0000000000..c975fd61e3
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/src/util.ts
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {execFile as execFileAsync} from 'child_process';
+import {readFile} from 'fs/promises';
+import {join} from 'path';
+import {promisify} from 'util';
+
+import {ASSETS_DIR} from './constants.js';
+
+export const execFile = promisify(execFileAsync);
+export const readAsset = (...components: string[]): Promise<string> => {
+ return readFile(join(ASSETS_DIR, ...components), 'utf8');
+};
diff --git a/remote/test/puppeteer/test/installation/tsconfig.json b/remote/test/puppeteer/test/installation/tsconfig.json
new file mode 100644
index 0000000000..146127b470
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "build",
+ "rootDir": "src",
+ },
+ "include": ["src"],
+}
diff --git a/remote/test/puppeteer/test/installation/tsdoc.json b/remote/test/puppeteer/test/installation/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/test/installation/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/test/package.json b/remote/test/puppeteer/test/package.json
new file mode 100644
index 0000000000..6470297572
--- /dev/null
+++ b/remote/test/puppeteer/test/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@puppeteer-test/test",
+ "version": "latest",
+ "private": true,
+ "scripts": {
+ "build": "wireit",
+ "clean": "../tools/clean.js"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b",
+ "clean": "if-file-deleted",
+ "dependencies": [
+ "../packages/puppeteer:build",
+ "../packages/testserver:build"
+ ],
+ "files": [
+ "src/**"
+ ],
+ "output": [
+ "build/**",
+ "tsconfig.tsbuildinfo"
+ ]
+ }
+ },
+ "dependencies": {
+ "diff": "5.1.0",
+ "jpeg-js": "0.4.4",
+ "pixelmatch": "5.3.0",
+ "pngjs": "7.0.0"
+ },
+ "devDependencies": {
+ "@types/diff": "5.0.9",
+ "@types/pixelmatch": "5.2.6",
+ "@types/pngjs": "6.0.4"
+ }
+}
diff --git a/remote/test/puppeteer/test/src/accessibility.spec.ts b/remote/test/puppeteer/test/src/accessibility.spec.ts
new file mode 100644
index 0000000000..09e9c90b96
--- /dev/null
+++ b/remote/test/puppeteer/test/src/accessibility.spec.ts
@@ -0,0 +1,567 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+import type {SerializedAXNode} from 'puppeteer-core/internal/cdp/Accessibility.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Accessibility', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <head>
+ <title>Accessibility Test</title>
+ </head>
+ <body>
+ <div>Hello World</div>
+ <h1>Inputs</h1>
+ <input placeholder="Empty input" autofocus />
+ <input placeholder="readonly input" readonly />
+ <input placeholder="disabled input" disabled />
+ <input aria-label="Input with whitespace" value=" " />
+ <input value="value only" />
+ <input aria-placeholder="placeholder" value="and a value" />
+ <div aria-hidden="true" id="desc">This is a description!</div>
+ <input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" />
+ <select>
+ <option>First Option</option>
+ <option>Second Option</option>
+ </select>
+ </body>`);
+
+ await page.focus('[placeholder="Empty input"]');
+ const golden = isFirefox
+ ? {
+ role: 'document',
+ name: 'Accessibility Test',
+ children: [
+ {role: 'text leaf', name: 'Hello World'},
+ {role: 'heading', name: 'Inputs', level: 1},
+ {role: 'entry', name: 'Empty input', focused: true},
+ {role: 'entry', name: 'readonly input', readonly: true},
+ {role: 'entry', name: 'disabled input', disabled: true},
+ {role: 'entry', name: 'Input with whitespace', value: ' '},
+ {role: 'entry', name: '', value: 'value only'},
+ {role: 'entry', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name
+ {
+ role: 'entry',
+ name: '',
+ value: 'and a value',
+ description: 'This is a description!',
+ }, // and here
+ {
+ role: 'combobox',
+ name: '',
+ value: 'First Option',
+ haspopup: true,
+ children: [
+ {
+ role: 'combobox option',
+ name: 'First Option',
+ selected: true,
+ },
+ {role: 'combobox option', name: 'Second Option'},
+ ],
+ },
+ ],
+ }
+ : {
+ role: 'RootWebArea',
+ name: 'Accessibility Test',
+ children: [
+ {role: 'StaticText', name: 'Hello World'},
+ {role: 'heading', name: 'Inputs', level: 1},
+ {role: 'textbox', name: 'Empty input', focused: true},
+ {role: 'textbox', name: 'readonly input', readonly: true},
+ {role: 'textbox', name: 'disabled input', disabled: true},
+ {role: 'textbox', name: 'Input with whitespace', value: ' '},
+ {role: 'textbox', name: '', value: 'value only'},
+ {role: 'textbox', name: 'placeholder', value: 'and a value'},
+ {
+ role: 'textbox',
+ name: 'placeholder',
+ value: 'and a value',
+ description: 'This is a description!',
+ },
+ {
+ role: 'combobox',
+ name: '',
+ value: 'First Option',
+ haspopup: 'menu',
+ children: [
+ {role: 'menuitem', name: 'First Option', selected: true},
+ {role: 'menuitem', name: 'Second Option'},
+ ],
+ },
+ ],
+ };
+ expect(await page.accessibility.snapshot()).toMatchObject(golden);
+ });
+ it('should report uninteresting nodes', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`<textarea>hi</textarea>`);
+ await page.focus('textarea');
+ const golden = isFirefox
+ ? {
+ role: 'entry',
+ name: '',
+ value: 'hi',
+ focused: true,
+ multiline: true,
+ children: [
+ {
+ role: 'text leaf',
+ name: 'hi',
+ },
+ ],
+ }
+ : {
+ role: 'textbox',
+ name: '',
+ value: 'hi',
+ focused: true,
+ multiline: true,
+ children: [
+ {
+ role: 'generic',
+ name: '',
+ children: [
+ {
+ role: 'StaticText',
+ name: 'hi',
+ },
+ ],
+ },
+ ],
+ };
+ expect(
+ findFocusedNode(
+ await page.accessibility.snapshot({interestingOnly: false})
+ )
+ ).toMatchObject(golden);
+ });
+ it('get snapshots while the tree is re-calculated', async () => {
+ // see https://github.com/puppeteer/puppeteer/issues/9404
+ const {page} = await getTestState();
+
+ await page.setContent(
+ `<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Accessible name + aria-expanded puppeteer bug</title>
+ <style>
+ [aria-expanded="false"] + * {
+ display: none;
+ }
+ </style>
+ </head>
+ <body>
+ <button hidden>Show</button>
+ <p>Some content</p>
+ <script>
+ const button = document.querySelector('button');
+ button.removeAttribute('hidden')
+ button.setAttribute('aria-expanded', 'false');
+ button.addEventListener('click', function() {
+ button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') !== 'true')
+ if (button.getAttribute('aria-expanded') == 'true') {
+ button.textContent = 'Hide'
+ } else {
+ button.textContent = 'Show'
+ }
+ })
+ </script>
+ </body>
+ </html>`
+ );
+ async function getAccessibleName(page: any, element: any) {
+ return (await page.accessibility.snapshot({root: element})).name;
+ }
+ using button = await page.$('button');
+ expect(await getAccessibleName(page, button)).toEqual('Show');
+ await button?.click();
+ await page.waitForSelector('aria/Hide');
+ });
+ it('roledescription', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div tabIndex=-1 aria-roledescription="foo">Hi</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ // See https://chromium-review.googlesource.com/c/chromium/src/+/3088862
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.roledescription).toBeUndefined();
+ });
+ it('orientation', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<a href="" role="slider" aria-orientation="vertical">11</a>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.orientation).toEqual('vertical');
+ });
+ it('autocomplete', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<input type="number" aria-autocomplete="list" />');
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.autocomplete).toEqual('list');
+ });
+ it('multiselectable', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.multiselectable).toEqual(true);
+ });
+ it('keyshortcuts', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.keyshortcuts).toEqual('foo');
+ });
+ describe('filtering children of leaf nodes', function () {
+ it('should not report text nodes inside controls', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div role="tablist">
+ <div role="tab" aria-selected="true"><b>Tab1</b></div>
+ <div role="tab">Tab2</div>
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'document',
+ name: '',
+ children: [
+ {
+ role: 'pagetab',
+ name: 'Tab1',
+ selected: true,
+ },
+ {
+ role: 'pagetab',
+ name: 'Tab2',
+ },
+ ],
+ }
+ : {
+ role: 'RootWebArea',
+ name: '',
+ children: [
+ {
+ role: 'tab',
+ name: 'Tab1',
+ selected: true,
+ },
+ {
+ role: 'tab',
+ name: 'Tab2',
+ },
+ ],
+ };
+ expect(await page.accessibility.snapshot()).toEqual(golden);
+ });
+ it('rich text editable fields should have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div contenteditable="true">
+ Edit this image: <img src="fakeimage.png" alt="my fake image">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'section',
+ name: '',
+ children: [
+ {
+ role: 'text leaf',
+ name: 'Edit this image:',
+ },
+ {
+ role: 'StaticText',
+ name: 'my fake image',
+ },
+ ],
+ }
+ : {
+ role: 'generic',
+ name: '',
+ value: 'Edit this image: ',
+ children: [
+ {
+ role: 'StaticText',
+ name: 'Edit this image: ',
+ },
+ {
+ role: 'image',
+ name: 'my fake image',
+ },
+ ],
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toMatchObject(golden);
+ });
+ it('rich text editable fields with role should have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div contenteditable="true" role='textbox'>
+ Edit this image: <img src="fakeimage.png" alt="my fake image">
+ </div>`);
+ // Image node should not be exposed in contenteditable elements. See https://crbug.com/1324392.
+ const golden = isFirefox
+ ? {
+ role: 'entry',
+ name: '',
+ value: 'Edit this image: my fake image',
+ children: [
+ {
+ role: 'StaticText',
+ name: 'my fake image',
+ },
+ ],
+ }
+ : {
+ role: 'textbox',
+ name: '',
+ value: 'Edit this image: ',
+ multiline: true,
+ children: [
+ {
+ role: 'StaticText',
+ name: 'Edit this image: ',
+ },
+ ],
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toMatchObject(golden);
+ });
+
+ // Firefox does not support contenteditable="plaintext-only".
+ describe('plaintext contenteditable', function () {
+ it('plain text field with role should not have children', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toEqual({
+ role: 'textbox',
+ name: '',
+ value: 'Edit this image:',
+ multiline: true,
+ });
+ });
+ });
+ it('non editable textbox with role and tabIndex and label should not have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox">
+ this is the inner content
+ <img alt="yo" src="fakeimg.png">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'entry',
+ name: 'my favorite textbox',
+ value: 'this is the inner content yo',
+ }
+ : {
+ role: 'textbox',
+ name: 'my favorite textbox',
+ value: 'this is the inner content ',
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+ it('checkbox with and tabIndex and label should not have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox">
+ this is the inner content
+ <img alt="yo" src="fakeimg.png">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'checkbutton',
+ name: 'my favorite checkbox',
+ checked: true,
+ }
+ : {
+ role: 'checkbox',
+ name: 'my favorite checkbox',
+ checked: true,
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+ it('checkbox without label should not have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div role="checkbox" aria-checked="true">
+ this is the inner content
+ <img alt="yo" src="fakeimg.png">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'checkbutton',
+ name: 'this is the inner content yo',
+ checked: true,
+ }
+ : {
+ role: 'checkbox',
+ name: 'this is the inner content yo',
+ checked: true,
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+
+ describe('root option', function () {
+ it('should work a button', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<button>My Button</button>`);
+
+ using button = (await page.$('button'))!;
+ expect(await page.accessibility.snapshot({root: button})).toEqual({
+ role: 'button',
+ name: 'My Button',
+ });
+ });
+ it('should work an input', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input title="My Input" value="My Value">`);
+
+ using input = (await page.$('input'))!;
+ expect(await page.accessibility.snapshot({root: input})).toEqual({
+ role: 'textbox',
+ name: 'My Input',
+ value: 'My Value',
+ });
+ });
+ it('should work a menu', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <div role="menu" title="My Menu">
+ <div role="menuitem">First Item</div>
+ <div role="menuitem">Second Item</div>
+ <div role="menuitem">Third Item</div>
+ </div>
+ `);
+
+ using menu = (await page.$('div[role="menu"]'))!;
+ expect(await page.accessibility.snapshot({root: menu})).toEqual({
+ role: 'menu',
+ name: 'My Menu',
+ children: [
+ {role: 'menuitem', name: 'First Item'},
+ {role: 'menuitem', name: 'Second Item'},
+ {role: 'menuitem', name: 'Third Item'},
+ ],
+ orientation: 'vertical',
+ });
+ });
+ it('should return null when the element is no longer in DOM', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<button>My Button</button>`);
+ using button = (await page.$('button'))!;
+ await page.$eval('button', button => {
+ return button.remove();
+ });
+ expect(await page.accessibility.snapshot({root: button})).toEqual(null);
+ });
+ it('should support the interestingOnly option', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div><button>My Button</button></div>`);
+ using div = (await page.$('div'))!;
+ expect(await page.accessibility.snapshot({root: div})).toEqual(null);
+ expect(
+ await page.accessibility.snapshot({
+ root: div,
+ interestingOnly: false,
+ })
+ ).toMatchObject({
+ role: 'generic',
+ name: '',
+ children: [
+ {
+ role: 'button',
+ name: 'My Button',
+ children: [{role: 'StaticText', name: 'My Button'}],
+ },
+ ],
+ });
+ });
+ });
+ });
+
+ function findFocusedNode(
+ node: SerializedAXNode | null
+ ): SerializedAXNode | null {
+ if (node?.focused) {
+ return node;
+ }
+ for (const child of node?.children || []) {
+ const focusedChild = findFocusedNode(child);
+ if (focusedChild) {
+ return focusedChild;
+ }
+ }
+ return null;
+ }
+});
diff --git a/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts
new file mode 100644
index 0000000000..434d01426a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts
@@ -0,0 +1,721 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame, detachFrame} from './utils.js';
+
+describe('AriaQueryHandler', () => {
+ setupTestBrowserHooks();
+
+ describe('parseAriaSelector', () => {
+ it('should find button', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<button id="btn" role="button"> Submit button and some spaces </button>'
+ );
+ const expectFound = async (button: ElementHandle | null) => {
+ assert(button);
+ const id = await button.evaluate((button: Element) => {
+ return button.id;
+ });
+ expect(id).toBe('btn');
+ };
+ {
+ using button = await page.$(
+ 'aria/Submit button and some spaces[role="button"]'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ "aria/Submit button and some spaces[role='button']"
+ );
+ await expectFound(button);
+ }
+ using button = await page.$(
+ 'aria/ Submit button and some spaces[role="button"]'
+ );
+ await expectFound(button);
+ {
+ using button = await page.$(
+ 'aria/Submit button and some spaces [role="button"]'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/Submit button and some spaces [ role = "button" ] '
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/[role="button"]Submit button and some spaces'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/Submit button [role="button"]and some spaces'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/[name=" Submit button and some spaces"][role="button"]'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ "aria/[name=' Submit button and some spaces'][role='button']"
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/ignored[name="Submit button and some spaces"][role="button"]'
+ );
+ await expectFound(button);
+ await expect(page.$('aria/smth[smth="true"]')).rejects.toThrow(
+ 'Unknown aria attribute "smth" in selector'
+ );
+ }
+ });
+ });
+
+ describe('queryOne', () => {
+ it('should find button by role', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="div"><button id="btn" role="button">Submit</button></div>'
+ );
+ using button = (await page.$(
+ 'aria/[role="button"]'
+ )) as ElementHandle<HTMLButtonElement>;
+ const id = await button!.evaluate(button => {
+ return button.id;
+ });
+ expect(id).toBe('btn');
+ });
+
+ it('should find button by name and role', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="div"><button id="btn" role="button">Submit</button></div>'
+ );
+ using button = (await page.$(
+ 'aria/Submit[role="button"]'
+ )) as ElementHandle<HTMLButtonElement>;
+ const id = await button!.evaluate(button => {
+ return button.id;
+ });
+ expect(id).toBe('btn');
+ });
+
+ it('should find first matching element', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ `
+ <div role="menu" id="mnu1" aria-label="menu div"></div>
+ <div role="menu" id="mnu2" aria-label="menu div"></div>
+ `
+ );
+ using div = (await page.$(
+ 'aria/menu div'
+ )) as ElementHandle<HTMLDivElement>;
+ const id = await div!.evaluate(div => {
+ return div.id;
+ });
+ expect(id).toBe('mnu1');
+ });
+
+ it('should find by name', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ `
+ <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div>
+ <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div>
+ `
+ );
+ using menu = (await page.$(
+ 'aria/menu-label1'
+ )) as ElementHandle<HTMLDivElement>;
+ const id = await menu!.evaluate(div => {
+ return div.id;
+ });
+ expect(id).toBe('mnu1');
+ });
+
+ it('should find 2nd element by name', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ `
+ <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div>
+ <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div>
+ `
+ );
+ using menu = (await page.$(
+ 'aria/menu-label2'
+ )) as ElementHandle<HTMLDivElement>;
+ const id = await menu!.evaluate(div => {
+ return div.id;
+ });
+ expect(id).toBe('mnu2');
+ });
+ });
+
+ describe('queryAll', () => {
+ it('should find menu by name', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ `
+ <div role="menu" id="mnu1" aria-label="menu div"></div>
+ <div role="menu" id="mnu2" aria-label="menu div"></div>
+ `
+ );
+ const divs = (await page.$$('aria/menu div')) as Array<
+ ElementHandle<HTMLDivElement>
+ >;
+ const ids = await Promise.all(
+ divs.map(n => {
+ return n.evaluate(div => {
+ return div.id;
+ });
+ })
+ );
+ expect(ids.join(', ')).toBe('mnu1, mnu2');
+ });
+ });
+ describe('queryAllArray', () => {
+ it('$$eval should handle many elements', async function () {
+ this.timeout(40_000);
+
+ const {page} = await getTestState();
+ await page.setContent('');
+ await page.evaluate(
+ `
+ for (var i = 0; i <= 10000; i++) {
+ const button = document.createElement('button');
+ button.textContent = i;
+ document.body.appendChild(button);
+ }
+ `
+ );
+ const sum = await page.$$eval('aria/[role="button"]', buttons => {
+ return buttons.reduce((acc, button) => {
+ return acc + Number(button.textContent);
+ }, 0);
+ });
+ expect(sum).toBe(50005000);
+ });
+ });
+
+ describe('waitForSelector (aria)', function () {
+ const addElement = (tag: string) => {
+ return document.body.appendChild(document.createElement(tag));
+ };
+
+ it('should immediately resolve promise if node exists', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ });
+
+ it('should work for ElementHandle.waitForSelector', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return (document.body.innerHTML = `<div><button>test</button></div>`);
+ });
+ using element = (await page.$('div'))!;
+ await element!.waitForSelector('aria/test');
+ });
+
+ it('should persist query handler bindings across reloads', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ await page.reload();
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ });
+
+ it('should persist query handler bindings across navigations', async () => {
+ const {page, server} = await getTestState();
+
+ // Reset page but make sure that execution context ids start with 1.
+ await page.goto('data:text/html,');
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+
+ // Reset page but again make sure that execution context ids start with 1.
+ await page.goto('data:text/html,');
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ });
+
+ it('should work independently of `exposeFunction`', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.exposeFunction('ariaQuerySelector', (a: number, b: number) => {
+ return a + b;
+ });
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)');
+ expect(result).toBe(10);
+ });
+
+ it('should work with removed MutationObserver', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ // @ts-expect-error This is the point of the test.
+ return delete window.MutationObserver;
+ });
+ const [handle] = await Promise.all([
+ page.waitForSelector('aria/anything'),
+ page.setContent(`<h1>anything</h1>`),
+ ]);
+ assert(handle);
+ expect(
+ await page.evaluate(x => {
+ return x.textContent;
+ }, handle)
+ ).toBe('anything');
+ });
+
+ it('should resolve promise when node is added', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const frame = page.mainFrame();
+ const watchdog = frame.waitForSelector('aria/[role="heading"]');
+ await frame.evaluate(addElement, 'br');
+ await frame.evaluate(addElement, 'h1');
+ using elementHandle = (await watchdog)!;
+ const tagName = await (
+ await elementHandle.getProperty('tagName')
+ ).jsonValue();
+ expect(tagName).toBe('H1');
+ });
+
+ it('should work when node is added through innerHTML', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const watchdog = page.waitForSelector('aria/name');
+ await page.evaluate(addElement, 'span');
+ await page.evaluate(() => {
+ return (document.querySelector('span')!.innerHTML =
+ '<h3><div aria-label="name"></div></h3>');
+ });
+ await watchdog;
+ });
+
+ it('Page.waitForSelector is shortcut for main frame', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const otherFrame = page.frames()[1];
+ const watchdog = page.waitForSelector('aria/[role="button"]');
+ await otherFrame!.evaluate(addElement, 'button');
+ await page.evaluate(addElement, 'button');
+ using elementHandle = await watchdog;
+ expect(elementHandle!.frame).toBe(page.mainFrame());
+ });
+
+ it('should run in specified frame', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(page, 'frame2', server.EMPTY_PAGE);
+ const frame1 = page.frames()[1];
+ const frame2 = page.frames()[2];
+ const waitForSelectorPromise = frame2!.waitForSelector(
+ 'aria/[role="button"]'
+ );
+ await frame1!.evaluate(addElement, 'button');
+ await frame2!.evaluate(addElement, 'button');
+ using elementHandle = await waitForSelectorPromise;
+ expect(elementHandle!.frame).toBe(frame2);
+ });
+
+ it('should throw when frame is detached', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1];
+ let waitError!: Error;
+ const waitPromise = frame!
+ .waitForSelector('aria/does-not-exist')
+ .catch(error => {
+ return (waitError = error);
+ });
+ await detachFrame(page, 'frame1');
+ await waitPromise;
+ expect(waitError).toBeTruthy();
+ expect(waitError.message).toContain(
+ 'waitForFunction failed: frame got detached.'
+ );
+ });
+
+ it('should survive cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let imgFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/[role="image"]')
+ .then(() => {
+ return (imgFound = true);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(imgFound).toBe(false);
+ await page.reload();
+ expect(imgFound).toBe(false);
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
+ await waitForSelector;
+ expect(imgFound).toBe(true);
+ });
+
+ it('should wait for visible', async () => {
+ const {page} = await getTestState();
+
+ let divFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/name', {visible: true})
+ .then(() => {
+ return (divFound = true);
+ });
+ await page.setContent(
+ `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>`
+ );
+ expect(divFound).toBe(false);
+ await page.evaluate(() => {
+ return document.querySelector('div')!.style.removeProperty('display');
+ });
+ expect(divFound).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .style.removeProperty('visibility');
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divFound).toBe(true);
+ });
+
+ it('should wait for visible recursively', async () => {
+ const {page} = await getTestState();
+
+ let divVisible = false;
+ const waitForSelector = page
+ .waitForSelector('aria/inner', {visible: true})
+ .then(() => {
+ return (divVisible = true);
+ })
+ .catch(() => {
+ return (divVisible = false);
+ });
+ await page.setContent(
+ `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>`
+ );
+ expect(divVisible).toBe(false);
+ await page.evaluate(() => {
+ return document.querySelector('div')!.style.removeProperty('display');
+ });
+ expect(divVisible).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .style.removeProperty('visibility');
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divVisible).toBe(true);
+ });
+
+ it('hidden should wait for visibility: hidden', async () => {
+ const {page} = await getTestState();
+
+ let divHidden = false;
+ await page.setContent(
+ `<div role='button' style='display: block;'>text</div>`
+ );
+ const waitForSelector = page
+ .waitForSelector('aria/[role="button"]', {hidden: true})
+ .then(() => {
+ return (divHidden = true);
+ })
+ .catch(() => {
+ return (divHidden = false);
+ });
+ await page.waitForSelector('aria/[role="button"]'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .style.setProperty('visibility', 'hidden');
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divHidden).toBe(true);
+ });
+
+ it('hidden should wait for display: none', async () => {
+ const {page} = await getTestState();
+
+ let divHidden = false;
+ await page.setContent(
+ `<div role='main' style='display: block;'>text</div>`
+ );
+ const waitForSelector = page
+ .waitForSelector('aria/[role="main"]', {hidden: true})
+ .then(() => {
+ return (divHidden = true);
+ })
+ .catch(() => {
+ return (divHidden = false);
+ });
+ await page.waitForSelector('aria/[role="main"]'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .style.setProperty('display', 'none');
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divHidden).toBe(true);
+ });
+
+ it('hidden should wait for removal', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div role='main'>text</div>`);
+ let divRemoved = false;
+ const waitForSelector = page
+ .waitForSelector('aria/[role="main"]', {hidden: true})
+ .then(() => {
+ return (divRemoved = true);
+ })
+ .catch(() => {
+ return (divRemoved = false);
+ });
+ await page.waitForSelector('aria/[role="main"]'); // do a round trip
+ expect(divRemoved).toBe(false);
+ await page.evaluate(() => {
+ return document.querySelector('div')!.remove();
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divRemoved).toBe(true);
+ });
+
+ it('should return null if waiting to hide non-existing element', async () => {
+ const {page} = await getTestState();
+
+ using handle = await page.waitForSelector('aria/non-existing', {
+ hidden: true,
+ });
+ expect(handle).toBe(null);
+ });
+
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ const error = await page
+ .waitForSelector('aria/[role="button"]', {
+ timeout: 10,
+ })
+ .catch(error => {
+ return error;
+ });
+ expect(error.message).toContain(
+ 'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded'
+ );
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+
+ it('should have an error message specifically for awaiting an element to be hidden', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div role='main'>text</div>`);
+ const promise = page.waitForSelector('aria/[role="main"]', {
+ hidden: true,
+ timeout: 10,
+ });
+ await expect(promise).rejects.toMatchObject({
+ message:
+ 'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded',
+ });
+ });
+
+ it('should respond to node attribute mutation', async () => {
+ const {page} = await getTestState();
+
+ let divFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/zombo')
+ .then(() => {
+ return (divFound = true);
+ })
+ .catch(() => {
+ return (divFound = false);
+ });
+ await page.setContent(`<div aria-label='notZombo'></div>`);
+ expect(divFound).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .setAttribute('aria-label', 'zombo');
+ });
+ expect(await waitForSelector).toBe(true);
+ });
+
+ it('should return the element handle', async () => {
+ const {page} = await getTestState();
+
+ const waitForSelector = page.waitForSelector('aria/zombo').catch(err => {
+ return err;
+ });
+ await page.setContent(`<div aria-label='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForSelector
+ )
+ ).toBe('anything');
+ });
+
+ it('should have correct stack trace for timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error!.stack).toContain(
+ 'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded'
+ );
+ });
+ });
+
+ describe('queryOne (Chromium web test)', () => {
+ async function setupPage(): ReturnType<typeof getTestState> {
+ const state = await getTestState();
+ await state.page.setContent(
+ `
+ <h2 id="shown">title</h2>
+ <h2 id="hidden" aria-hidden="true">title</h2>
+ <div id="node1" aria-labeledby="node2"></div>
+ <div id="node2" aria-label="bar"></div>
+ <div id="node3" aria-label="foo"></div>
+ <div id="node4" class="container">
+ <div id="node5" role="button" aria-label="foo"></div>
+ <div id="node6" role="button" aria-label="foo"></div>
+ <!-- Accessible name not available when element is hidden -->
+ <div id="node7" hidden role="button" aria-label="foo"></div>
+ <div id="node8" role="button" aria-label="bar"></div>
+ </div>
+ <button id="node10">text content</button>
+ <h1 id="node11">text content</h1>
+ <!-- Accessible name not available when role is "presentation" -->
+ <h1 id="node12" role="presentation">text content</h1>
+ <!-- Elements inside shadow dom should be found -->
+ <script>
+ const div = document.createElement('div');
+ const shadowRoot = div.attachShadow({mode: 'open'});
+ const h1 = document.createElement('h1');
+ h1.textContent = 'text content';
+ h1.id = 'node13';
+ shadowRoot.appendChild(h1);
+ document.documentElement.appendChild(div);
+ </script>
+ <img id="node20" src="" alt="Accessible Name">
+ <input id="node21" type="submit" value="Accessible Name">
+ <label id="node22" for="node23">Accessible Name</label>
+ <!-- Accessible name for the <input> is "Accessible Name" -->
+ <input id="node23">
+ <div id="node24" title="Accessible Name"></div>
+ <div role="treeitem" id="node30">
+ <div role="treeitem" id="node31">
+ <div role="treeitem" id="node32">item1</div>
+ <div role="treeitem" id="node33">item2</div>
+ </div>
+ <div role="treeitem" id="node34">item3</div>
+ </div>
+ <!-- Accessible name for the <div> is "item1 item2 item3" -->
+ <div aria-describedby="node30"></div>
+ `
+ );
+ return state;
+ }
+ const getIds = async (elements: ElementHandle[]) => {
+ return await Promise.all(
+ elements.map(element => {
+ return element.evaluate((element: Element) => {
+ return element.id;
+ });
+ })
+ );
+ };
+ it('should find by name "foo"', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/foo');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['node3', 'node5', 'node6']);
+ });
+ it('should find by name "bar"', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/bar');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['node1', 'node2', 'node8']);
+ });
+ it('should find treeitem by name', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/item1 item2 item3');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['node30']);
+ });
+ it('should find by role "button"', async () => {
+ const {page} = await setupPage();
+ const found = (await page.$$('aria/[role="button"]')) as Array<
+ ElementHandle<HTMLButtonElement>
+ >;
+ const ids = await getIds(found);
+ expect(ids).toEqual([
+ 'node5',
+ 'node6',
+ 'node7',
+ 'node8',
+ 'node10',
+ 'node21',
+ ]);
+ });
+ it('should find by role "heading"', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/[role="heading"]');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['shown', 'hidden', 'node11', 'node13']);
+ });
+ it('should find both ignored and unignored', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/title');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['shown']);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/autofill.spec.ts b/remote/test/puppeteer/test/src/autofill.spec.ts
new file mode 100644
index 0000000000..a04e4b8e8b
--- /dev/null
+++ b/remote/test/puppeteer/test/src/autofill.spec.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Autofill', function () {
+ setupTestBrowserHooks();
+ describe('ElementHandle.autofill', () => {
+ it('should fill out a credit card', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/credit-card.html');
+ using name = await page.waitForSelector('#name');
+ await name!.autofill({
+ creditCard: {
+ number: '4444444444444444',
+ name: 'John Smith',
+ expiryMonth: '01',
+ expiryYear: '2030',
+ cvc: '123',
+ },
+ });
+ expect(
+ await page.evaluate(() => {
+ const result = [];
+ for (const el of document.querySelectorAll('input')) {
+ result.push(el.value);
+ }
+ return result.join(',');
+ })
+ ).toBe('John Smith,4444444444444444,01,2030,Submit');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/browser.spec.ts b/remote/test/puppeteer/test/src/browser.spec.ts
new file mode 100644
index 0000000000..6f21af5d9a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/browser.spec.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Browser specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Browser.version', function () {
+ it('should return version', async () => {
+ const {browser} = await getTestState();
+
+ const version = await browser.version();
+ expect(version.length).toBeGreaterThan(0);
+ expect(version.toLowerCase()).atLeastOneToContain(['firefox', 'chrome']);
+ });
+ });
+
+ describe('Browser.userAgent', function () {
+ it('should include Browser engine', async () => {
+ const {browser, isChrome} = await getTestState();
+
+ const userAgent = await browser.userAgent();
+ expect(userAgent.length).toBeGreaterThan(0);
+ if (isChrome) {
+ expect(userAgent).toContain('WebKit');
+ } else {
+ expect(userAgent).toContain('Gecko');
+ }
+ });
+ });
+
+ describe('Browser.target', function () {
+ it('should return browser target', async () => {
+ const {browser} = await getTestState();
+
+ const target = browser.target();
+ expect(target.type()).toBe('browser');
+ });
+ });
+
+ describe('Browser.process', function () {
+ it('should return child_process instance', async () => {
+ const {browser} = await getTestState();
+
+ const process = await browser.process();
+ expect(process!.pid).toBeGreaterThan(0);
+ });
+ it('should not return child_process for remote browser', async () => {
+ const {browser, puppeteer} = await getTestState();
+
+ const browserWSEndpoint = browser.wsEndpoint();
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+ expect(remoteBrowser.process()).toBe(null);
+ await remoteBrowser.disconnect();
+ });
+ });
+
+ describe('Browser.isConnected', () => {
+ it('should set the browser connected state', async () => {
+ const {browser, puppeteer} = await getTestState();
+
+ const browserWSEndpoint = browser.wsEndpoint();
+ const newBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+ expect(newBrowser.isConnected()).toBe(true);
+ await newBrowser.disconnect();
+ expect(newBrowser.isConnected()).toBe(false);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/browsercontext.spec.ts b/remote/test/puppeteer/test/src/browsercontext.spec.ts
new file mode 100644
index 0000000000..9cbbda60a4
--- /dev/null
+++ b/remote/test/puppeteer/test/src/browsercontext.spec.ts
@@ -0,0 +1,368 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+describe('BrowserContext', function () {
+ setupTestBrowserHooks();
+
+ it('should have default context', async () => {
+ const {browser} = await getTestState({
+ skipContextCreation: true,
+ });
+ expect(browser.browserContexts()).toHaveLength(1);
+ const defaultContext = browser.browserContexts()[0]!;
+ expect(defaultContext!.isIncognito()).toBe(false);
+ let error!: Error;
+ await defaultContext!.close().catch(error_ => {
+ return (error = error_);
+ });
+ expect(browser.defaultBrowserContext()).toBe(defaultContext);
+ expect(error.message).toContain('cannot be closed');
+ });
+ it('should create new incognito context', async () => {
+ const {browser} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ expect(browser.browserContexts()).toHaveLength(1);
+ const context = await browser.createIncognitoBrowserContext();
+ expect(context.isIncognito()).toBe(true);
+ expect(browser.browserContexts()).toHaveLength(2);
+ expect(browser.browserContexts().indexOf(context) !== -1).toBe(true);
+ await context.close();
+ expect(browser.browserContexts()).toHaveLength(1);
+ });
+ it('should close all belonging targets once closing context', async () => {
+ const {browser} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ expect(await browser.pages()).toHaveLength(1);
+
+ const context = await browser.createIncognitoBrowserContext();
+ await context.newPage();
+ expect(await browser.pages()).toHaveLength(2);
+ expect(await context.pages()).toHaveLength(1);
+
+ await context.close();
+ expect(await browser.pages()).toHaveLength(1);
+ });
+ it('window.open should use parent tab context', async () => {
+ const {browser, server, page, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [popupTarget] = await Promise.all([
+ waitEvent(browser, 'targetcreated'),
+ page.evaluate(url => {
+ return window.open(url);
+ }, server.EMPTY_PAGE),
+ ]);
+ expect(popupTarget.browserContext()).toBe(context);
+ });
+ it('should fire target events', async () => {
+ const {server, context} = await getTestState();
+
+ const events: string[] = [];
+ context.on('targetcreated', target => {
+ events.push('CREATED: ' + target.url());
+ });
+ context.on('targetchanged', target => {
+ events.push('CHANGED: ' + target.url());
+ });
+ context.on('targetdestroyed', target => {
+ events.push('DESTROYED: ' + target.url());
+ });
+ const page = await context.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.close();
+ expect(events).toEqual([
+ 'CREATED: about:blank',
+ `CHANGED: ${server.EMPTY_PAGE}`,
+ `DESTROYED: ${server.EMPTY_PAGE}`,
+ ]);
+ });
+ it('should wait for a target', async () => {
+ const {server, context} = await getTestState();
+
+ let resolved = false;
+
+ const targetPromise = context.waitForTarget(target => {
+ return target.url() === server.EMPTY_PAGE;
+ });
+ targetPromise
+ .then(() => {
+ return (resolved = true);
+ })
+ .catch(error => {
+ resolved = true;
+ if (error instanceof TimeoutError) {
+ console.error(error);
+ } else {
+ throw error;
+ }
+ });
+ const page = await context.newPage();
+ expect(resolved).toBe(false);
+ await page.goto(server.EMPTY_PAGE);
+ try {
+ const target = await targetPromise;
+ expect(await target.page()).toBe(page);
+ } catch (error) {
+ if (error instanceof TimeoutError) {
+ console.error(error);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should timeout waiting for a non-existent target', async () => {
+ const {browser, server} = await getTestState();
+
+ const context = await browser.createIncognitoBrowserContext();
+ const error = await context
+ .waitForTarget(
+ target => {
+ return target.url() === server.EMPTY_PAGE;
+ },
+ {
+ timeout: 1,
+ }
+ )
+ .catch(error_ => {
+ return error_;
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ await context.close();
+ });
+
+ it('should isolate localStorage and cookies', async () => {
+ const {browser, server} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ // Create two incognito contexts.
+ const context1 = await browser.createIncognitoBrowserContext();
+ const context2 = await browser.createIncognitoBrowserContext();
+ expect(context1.targets()).toHaveLength(0);
+ expect(context2.targets()).toHaveLength(0);
+
+ // Create a page in first incognito context.
+ const page1 = await context1.newPage();
+ await page1.goto(server.EMPTY_PAGE);
+ await page1.evaluate(() => {
+ localStorage.setItem('name', 'page1');
+ document.cookie = 'name=page1';
+ });
+
+ expect(context1.targets()).toHaveLength(1);
+ expect(context2.targets()).toHaveLength(0);
+
+ // Create a page in second incognito context.
+ const page2 = await context2.newPage();
+ await page2.goto(server.EMPTY_PAGE);
+ await page2.evaluate(() => {
+ localStorage.setItem('name', 'page2');
+ document.cookie = 'name=page2';
+ });
+
+ expect(context1.targets()).toHaveLength(1);
+ expect(context1.targets()[0]).toBe(page1.target());
+ expect(context2.targets()).toHaveLength(1);
+ expect(context2.targets()[0]).toBe(page2.target());
+
+ // Make sure pages don't share localstorage or cookies.
+ expect(
+ await page1.evaluate(() => {
+ return localStorage.getItem('name');
+ })
+ ).toBe('page1');
+ expect(
+ await page1.evaluate(() => {
+ return document.cookie;
+ })
+ ).toBe('name=page1');
+ expect(
+ await page2.evaluate(() => {
+ return localStorage.getItem('name');
+ })
+ ).toBe('page2');
+ expect(
+ await page2.evaluate(() => {
+ return document.cookie;
+ })
+ ).toBe('name=page2');
+
+ // Cleanup contexts.
+ await Promise.all([context1.close(), context2.close()]);
+ expect(browser.browserContexts()).toHaveLength(1);
+ });
+
+ it('should work across sessions', async () => {
+ const {browser, puppeteer} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ expect(browser.browserContexts()).toHaveLength(1);
+ const context = await browser.createIncognitoBrowserContext();
+ expect(browser.browserContexts()).toHaveLength(2);
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const contexts = remoteBrowser.browserContexts();
+ expect(contexts).toHaveLength(2);
+ await remoteBrowser.disconnect();
+ await context.close();
+ });
+
+ it('should provide a context id', async () => {
+ const {browser} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ expect(browser.browserContexts()).toHaveLength(1);
+ expect(browser.browserContexts()[0]!.id).toBeUndefined();
+
+ const context = await browser.createIncognitoBrowserContext();
+ expect(browser.browserContexts()).toHaveLength(2);
+ expect(browser.browserContexts()[1]!.id).toBeDefined();
+ await context.close();
+ });
+
+ describe('BrowserContext.overridePermissions', function () {
+ function getPermission(page: Page, name: PermissionName) {
+ return page.evaluate(name => {
+ return navigator.permissions.query({name}).then(result => {
+ return result.state;
+ });
+ }, name);
+ }
+
+ it('should be prompt by default', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(await getPermission(page, 'geolocation')).toBe('prompt');
+ });
+ it('should deny permission when not listed', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await context.overridePermissions(server.EMPTY_PAGE, []);
+ expect(await getPermission(page, 'geolocation')).toBe('denied');
+ });
+ it('should fail when bad permission is given', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error!: Error;
+ await context
+ // @ts-expect-error purposeful bad input for test
+ .overridePermissions(server.EMPTY_PAGE, ['foo'])
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Unknown permission: foo');
+ });
+ it('should grant permission when listed', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
+ expect(await getPermission(page, 'geolocation')).toBe('granted');
+ });
+ it('should reset permissions', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
+ expect(await getPermission(page, 'geolocation')).toBe('granted');
+ await context.clearPermissionOverrides();
+ expect(await getPermission(page, 'geolocation')).toBe('prompt');
+ });
+ it('should trigger permission onchange', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ (globalThis as any).events = [];
+ return navigator.permissions
+ .query({name: 'geolocation'})
+ .then(function (result) {
+ (globalThis as any).events.push(result.state);
+ result.onchange = function () {
+ (globalThis as any).events.push(result.state);
+ };
+ });
+ });
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).events;
+ })
+ ).toEqual(['prompt']);
+ await context.overridePermissions(server.EMPTY_PAGE, []);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).events;
+ })
+ ).toEqual(['prompt', 'denied']);
+ await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).events;
+ })
+ ).toEqual(['prompt', 'denied', 'granted']);
+ await context.clearPermissionOverrides();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).events;
+ })
+ ).toEqual(['prompt', 'denied', 'granted', 'prompt']);
+ });
+ it('should isolate permissions between browser contexts', async () => {
+ const {page, server, context, browser} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const otherContext = await browser.createIncognitoBrowserContext();
+ const otherPage = await otherContext.newPage();
+ await otherPage.goto(server.EMPTY_PAGE);
+ expect(await getPermission(page, 'geolocation')).toBe('prompt');
+ expect(await getPermission(otherPage, 'geolocation')).toBe('prompt');
+
+ await context.overridePermissions(server.EMPTY_PAGE, []);
+ await otherContext.overridePermissions(server.EMPTY_PAGE, [
+ 'geolocation',
+ ]);
+ expect(await getPermission(page, 'geolocation')).toBe('denied');
+ expect(await getPermission(otherPage, 'geolocation')).toBe('granted');
+
+ await context.clearPermissionOverrides();
+ expect(await getPermission(page, 'geolocation')).toBe('prompt');
+ expect(await getPermission(otherPage, 'geolocation')).toBe('granted');
+
+ await otherContext.close();
+ });
+ it('should grant persistent-storage', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(await getPermission(page, 'persistent-storage')).not.toBe(
+ 'granted'
+ );
+ await context.overridePermissions(server.EMPTY_PAGE, [
+ 'persistent-storage',
+ ]);
+ expect(await getPermission(page, 'persistent-storage')).toBe('granted');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts b/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts
new file mode 100644
index 0000000000..2000c0e435
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts
@@ -0,0 +1,147 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {Target} from 'puppeteer-core/internal/api/Target.js';
+import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js';
+
+import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js';
+import {waitEvent} from '../utils.js';
+
+describe('Target.createCDPSession', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+
+ await Promise.all([
+ client.send('Runtime.enable'),
+ client.send('Runtime.evaluate', {expression: 'window.foo = "bar"'}),
+ ]);
+ const foo = await page.evaluate(() => {
+ return (globalThis as any).foo;
+ });
+ expect(foo).toBe('bar');
+ });
+
+ it('should not report created targets for custom CDP sessions', async () => {
+ const {browser} = await getTestState();
+ let called = 0;
+ const handler = async (target: Target) => {
+ called++;
+ if (called > 1) {
+ throw new Error('Too many targets created');
+ }
+ await target.createCDPSession();
+ };
+ browser.browserContexts()[0]!.on('targetcreated', handler);
+ await browser.newPage();
+ browser.browserContexts()[0]!.off('targetcreated', handler);
+ });
+
+ it('should send events', async () => {
+ const {page, server} = await getTestState();
+
+ const client = await page.createCDPSession();
+ await client.send('Network.enable');
+ const events: unknown[] = [];
+ client.on('Network.requestWillBeSent', event => {
+ events.push(event);
+ });
+ await Promise.all([
+ waitEvent(client, 'Network.requestWillBeSent'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(events).toHaveLength(1);
+ });
+ it('should enable and disable domains independently', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ await client.send('Runtime.enable');
+ await client.send('Debugger.enable');
+ // JS coverage enables and then disables Debugger domain.
+ await page.coverage.startJSCoverage();
+ await page.coverage.stopJSCoverage();
+ // generate a script in page and wait for the event.
+ const [event] = await Promise.all([
+ waitEvent(client, 'Debugger.scriptParsed'),
+ page.evaluate('//# sourceURL=foo.js'),
+ ]);
+ // expect events to be dispatched.
+ expect(event.url).toBe('foo.js');
+ });
+ it('should be able to detach session', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ await client.send('Runtime.enable');
+ const evalResponse = await client.send('Runtime.evaluate', {
+ expression: '1 + 2',
+ returnByValue: true,
+ });
+ expect(evalResponse.result.value).toBe(3);
+ await client.detach();
+ let error!: Error;
+ try {
+ await client.send('Runtime.evaluate', {
+ expression: '3 + 1',
+ returnByValue: true,
+ });
+ } catch (error_) {
+ if (isErrorLike(error_)) {
+ error = error_ as Error;
+ }
+ }
+ expect(error.message).toContain('Session closed.');
+ });
+ it('should throw nice errors', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ const error = await theSourceOfTheProblems().catch(error => {
+ return error;
+ });
+ expect(error.stack).toContain('theSourceOfTheProblems');
+ expect(error.message).toContain('ThisCommand.DoesNotExist');
+
+ async function theSourceOfTheProblems() {
+ // @ts-expect-error This fails in TS as it knows that command does not
+ // exist but we want to have this tests for our users who consume in JS
+ // not TS.
+ await client.send('ThisCommand.DoesNotExist');
+ }
+ });
+
+ it('should respect custom timeout', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ await expect(
+ client.send(
+ 'Runtime.evaluate',
+ {
+ expression: 'new Promise(resolve => {})',
+ awaitPromise: true,
+ },
+ {
+ timeout: 50,
+ }
+ )
+ ).rejects.toThrowError(
+ `Runtime.evaluate timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.`
+ );
+ });
+
+ it('should expose the underlying connection', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ expect(client.connection()).toBeTruthy();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts b/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts
new file mode 100644
index 0000000000..d1f8992530
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts
@@ -0,0 +1,96 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {CdpBrowser} from 'puppeteer-core/internal/cdp/Browser.js';
+
+import {getTestState, launch} from '../mocha-utils.js';
+import {attachFrame} from '../utils.js';
+
+describe('TargetManager', () => {
+ /* We use a special browser for this test as we need the --site-per-process flag */
+ let state: Awaited<ReturnType<typeof launch>> & {
+ browser: CdpBrowser;
+ };
+
+ beforeEach(async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+ state = (await launch(
+ Object.assign({}, defaultBrowserOptions, {
+ args: (defaultBrowserOptions.args || []).concat([
+ '--site-per-process',
+ '--remote-debugging-port=21222',
+ '--host-rules=MAP * 127.0.0.1',
+ ]),
+ }),
+ {createPage: false}
+ )) as Awaited<ReturnType<typeof launch>> & {
+ browser: CdpBrowser;
+ };
+ });
+
+ afterEach(async () => {
+ await state.close();
+ });
+
+ // CDP-specific test.
+ it('should handle targets', async () => {
+ const {server, context, browser} = state;
+
+ const targetManager = browser._targetManager();
+ expect(targetManager.getAvailableTargets().size).toBe(3);
+
+ expect(await context.pages()).toHaveLength(0);
+ expect(targetManager.getAvailableTargets().size).toBe(3);
+
+ const page = await context.newPage();
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(5);
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(5);
+
+ // attach a local iframe.
+ let framePromise = page.waitForFrame(frame => {
+ return frame.url().endsWith('/empty.html');
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await framePromise;
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(5);
+ expect(page.frames()).toHaveLength(2);
+
+ // // attach a remote frame iframe.
+ framePromise = page.waitForFrame(frame => {
+ return frame.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
+ });
+ await attachFrame(
+ page,
+ 'frame2',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ await framePromise;
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(6);
+ expect(page.frames()).toHaveLength(3);
+
+ framePromise = page.waitForFrame(frame => {
+ return frame.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
+ });
+ await attachFrame(
+ page,
+ 'frame3',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ await framePromise;
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(7);
+ expect(page.frames()).toHaveLength(4);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts b/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts
new file mode 100644
index 0000000000..211f93cd6b
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {PageEvent} from 'puppeteer-core';
+
+import {launch} from '../mocha-utils.js';
+import {waitEvent} from '../utils.js';
+
+describe('BFCache', function () {
+ it('can navigate to a BFCached page', async () => {
+ const {httpsServer, page, close} = await launch({
+ ignoreHTTPSErrors: true,
+ });
+
+ try {
+ page.setDefaultTimeout(3000);
+
+ await page.goto(httpsServer.PREFIX + '/cached/bfcache/index.html');
+
+ await Promise.all([page.waitForNavigation(), page.locator('a').click()]);
+
+ expect(page.url()).toContain('target.html');
+
+ await Promise.all([page.waitForNavigation(), page.goBack()]);
+
+ expect(
+ await page.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('BFCachednext');
+ } finally {
+ await close();
+ }
+ });
+
+ it('can navigate to a BFCached page containing an OOPIF and a worker', async () => {
+ const {httpsServer, page, close} = await launch({
+ ignoreHTTPSErrors: true,
+ });
+ try {
+ page.setDefaultTimeout(3000);
+ const [worker1] = await Promise.all([
+ waitEvent(page, PageEvent.WorkerCreated),
+ page.goto(
+ httpsServer.PREFIX + '/cached/bfcache/worker-iframe-container.html'
+ ),
+ ]);
+ expect(await worker1.evaluate('1 + 1')).toBe(2);
+ await Promise.all([page.waitForNavigation(), page.locator('a').click()]);
+
+ const [worker2] = await Promise.all([
+ waitEvent(page, PageEvent.WorkerCreated),
+ page.waitForNavigation(),
+ page.goBack(),
+ ]);
+ expect(await worker2.evaluate('1 + 1')).toBe(2);
+ } finally {
+ await close();
+ }
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/devtools.spec.ts b/remote/test/puppeteer/test/src/cdp/devtools.spec.ts
new file mode 100644
index 0000000000..c158481af2
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/devtools.spec.ts
@@ -0,0 +1,123 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+
+import {getTestState, launch} from '../mocha-utils.js';
+
+describe('DevTools', function () {
+ /* These tests fire up an actual browser so let's
+ * allow a higher timeout
+ */
+ this.timeout(20_000);
+
+ let launchOptions: PuppeteerLaunchOptions & {
+ devtools: boolean;
+ };
+ const browsers: Array<() => Promise<void>> = [];
+
+ beforeEach(async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+ launchOptions = Object.assign({}, defaultBrowserOptions, {
+ devtools: true,
+ });
+ });
+
+ async function launchBrowser(options: typeof launchOptions) {
+ const {browser, close} = await launch(options, {createContext: false});
+ browsers.push(close);
+ return browser;
+ }
+
+ afterEach(async () => {
+ await Promise.all(
+ browsers.map((close, index) => {
+ delete browsers[index];
+ return close();
+ })
+ );
+ });
+
+ it('target.page() should return a DevTools page if custom isPageTarget is provided', async function () {
+ const {puppeteer} = await getTestState({skipLaunch: true});
+ const originalBrowser = await launchBrowser(launchOptions);
+
+ const browserWSEndpoint = originalBrowser.wsEndpoint();
+
+ const browser = await puppeteer.connect({
+ browserWSEndpoint,
+ _isPageTarget(target) {
+ return (
+ target.type() === 'other' && target.url().startsWith('devtools://')
+ );
+ },
+ });
+ const devtoolsPageTarget = await browser.waitForTarget(target => {
+ return target.type() === 'other';
+ });
+ const page = (await devtoolsPageTarget.page())!;
+ expect(
+ await page.evaluate(() => {
+ return 2 * 3;
+ })
+ ).toBe(6);
+ expect(await browser.pages()).toContainEqual(page);
+ });
+ it('target.page() should return a DevTools page if asPage is used', async function () {
+ const {puppeteer} = await getTestState({skipLaunch: true});
+ const originalBrowser = await launchBrowser(launchOptions);
+
+ const browserWSEndpoint = originalBrowser.wsEndpoint();
+
+ const browser = await puppeteer.connect({
+ browserWSEndpoint,
+ });
+ const devtoolsPageTarget = await browser.waitForTarget(target => {
+ return target.type() === 'other';
+ });
+ const page = (await devtoolsPageTarget.asPage())!;
+ expect(
+ await page.evaluate(() => {
+ return 2 * 3;
+ })
+ ).toBe(6);
+ expect(await browser.pages()).toContainEqual(page);
+ });
+ it('should open devtools when "devtools: true" option is given', async () => {
+ const browser = await launchBrowser(
+ Object.assign({devtools: true}, launchOptions)
+ );
+ const context = await browser.createIncognitoBrowserContext();
+ await Promise.all([
+ context.newPage(),
+ browser.waitForTarget((target: {url: () => string | string[]}) => {
+ return target.url().includes('devtools://');
+ }),
+ ]);
+ await browser.close();
+ });
+ it('should expose DevTools as a page', async () => {
+ const browser = await launchBrowser(
+ Object.assign({devtools: true}, launchOptions)
+ );
+ const context = await browser.createIncognitoBrowserContext();
+ const [target] = await Promise.all([
+ browser.waitForTarget((target: {url: () => string | string[]}) => {
+ return target.url().includes('devtools://');
+ }),
+ context.newPage(),
+ ]);
+ const page = await target.page();
+ await page!.waitForFunction(() => {
+ // @ts-expect-error wrong context.
+ return Boolean(DevToolsAPI);
+ });
+ await browser.close();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/extensions.spec.ts b/remote/test/puppeteer/test/src/cdp/extensions.spec.ts
new file mode 100644
index 0000000000..6db9f931ad
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/extensions.spec.ts
@@ -0,0 +1,120 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+
+import expect from 'expect';
+import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+
+import {getTestState, launch} from '../mocha-utils.js';
+
+const extensionPath = path.join(
+ __dirname,
+ '..',
+ '..',
+ 'assets',
+ 'simple-extension'
+);
+const serviceWorkerExtensionPath = path.join(
+ __dirname,
+ '..',
+ '..',
+ 'assets',
+ 'serviceworkers',
+ 'extension'
+);
+
+describe('extensions', function () {
+ /* These tests fire up an actual browser so let's
+ * allow a higher timeout
+ */
+ this.timeout(20_000);
+
+ let extensionOptions: PuppeteerLaunchOptions & {
+ args: string[];
+ };
+ const browsers: Array<() => Promise<void>> = [];
+
+ beforeEach(async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+
+ extensionOptions = Object.assign({}, defaultBrowserOptions, {
+ args: [
+ `--disable-extensions-except=${extensionPath}`,
+ `--load-extension=${extensionPath}`,
+ ],
+ });
+ });
+
+ async function launchBrowser(options: typeof extensionOptions) {
+ const {browser, close} = await launch(options, {createContext: false});
+ browsers.push(close);
+ return browser;
+ }
+
+ afterEach(async () => {
+ await Promise.all(
+ browsers.map((close, index) => {
+ delete browsers[index];
+ return close();
+ })
+ );
+ });
+
+ it('background_page target type should be available', async () => {
+ const browserWithExtension = await launchBrowser(extensionOptions);
+ const page = await browserWithExtension.newPage();
+ const backgroundPageTarget = await browserWithExtension.waitForTarget(
+ target => {
+ return target.type() === 'background_page';
+ }
+ );
+ await page.close();
+ await browserWithExtension.close();
+ expect(backgroundPageTarget).toBeTruthy();
+ });
+
+ it('service_worker target type should be available', async () => {
+ const browserWithExtension = await launchBrowser({
+ args: [
+ `--disable-extensions-except=${serviceWorkerExtensionPath}`,
+ `--load-extension=${serviceWorkerExtensionPath}`,
+ ],
+ });
+ const page = await browserWithExtension.newPage();
+ const serviceWorkerTarget = await browserWithExtension.waitForTarget(
+ target => {
+ return target.type() === 'service_worker';
+ }
+ );
+ await page.close();
+ await browserWithExtension.close();
+ expect(serviceWorkerTarget).toBeTruthy();
+ });
+
+ it('target.page() should return a background_page', async function () {
+ const browserWithExtension = await launchBrowser(extensionOptions);
+ const backgroundPageTarget = await browserWithExtension.waitForTarget(
+ target => {
+ return target.type() === 'background_page';
+ }
+ );
+ const page = (await backgroundPageTarget.page())!;
+ expect(
+ await page.evaluate(() => {
+ return 2 * 3;
+ })
+ ).toBe(6);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).MAGIC;
+ })
+ ).toBe(42);
+ await browserWithExtension.close();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/prerender.spec.ts b/remote/test/puppeteer/test/src/cdp/prerender.spec.ts
new file mode 100644
index 0000000000..4e0fb30da9
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/prerender.spec.ts
@@ -0,0 +1,181 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {statSync} from 'fs';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js';
+import {getUniqueVideoFilePlaceholder} from '../utils.js';
+
+describe('Prerender', function () {
+ setupTestBrowserHooks();
+
+ it('can navigate to a prerendered page via input', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ using link = await page.waitForSelector('a');
+ await Promise.all([page.waitForNavigation(), link?.click()]);
+ expect(
+ await page.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ });
+
+ it('can navigate to a prerendered page via Puppeteer', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ await page.goto(server.PREFIX + '/prerender/target.html');
+ expect(
+ await page.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ });
+
+ describe('via frame', () => {
+ it('can navigate to a prerendered page via input', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ const mainFrame = page.mainFrame();
+ using link = await mainFrame.waitForSelector('a');
+ await Promise.all([mainFrame.waitForNavigation(), link?.click()]);
+ expect(mainFrame).toBe(page.mainFrame());
+ expect(
+ await mainFrame.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ expect(mainFrame).toBe(page.mainFrame());
+ });
+
+ it('can navigate to a prerendered page via Puppeteer', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ const mainFrame = page.mainFrame();
+ await mainFrame.goto(server.PREFIX + '/prerender/target.html');
+ expect(
+ await mainFrame.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ expect(mainFrame).toBe(page.mainFrame());
+ });
+ });
+
+ it('can screencast', async () => {
+ using file = getUniqueVideoFilePlaceholder();
+
+ const {page, server} = await getTestState();
+
+ const recorder = await page.screencast({
+ path: file.filename,
+ scale: 0.5,
+ crop: {width: 100, height: 100, x: 0, y: 0},
+ speed: 0.5,
+ });
+
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ using link = await page.locator('a').waitHandle();
+ await Promise.all([page.waitForNavigation(), link.click()]);
+ using input = await page.locator('input').waitHandle();
+ await input.type('ab', {delay: 100});
+
+ await recorder.stop();
+
+ expect(statSync(file.filename).size).toBeGreaterThan(0);
+ });
+
+ describe('with network requests', () => {
+ it('can receive requests from the prerendered page', async () => {
+ const {page, server} = await getTestState();
+
+ const urls: string[] = [];
+ page.on('request', request => {
+ urls.push(request.url());
+ });
+
+ await page.goto(server.PREFIX + '/prerender/index.html');
+ using button = await page.waitForSelector('button');
+ await button?.click();
+ const mainFrame = page.mainFrame();
+ using link = await mainFrame.waitForSelector('a');
+ await Promise.all([mainFrame.waitForNavigation(), link?.click()]);
+ expect(mainFrame).toBe(page.mainFrame());
+ expect(
+ await mainFrame.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ expect(mainFrame).toBe(page.mainFrame());
+ expect(
+ urls.find(url => {
+ return url.endsWith('prerender/target.html');
+ })
+ ).toBeTruthy();
+ expect(
+ urls.find(url => {
+ return url.includes('prerender/index.html');
+ })
+ ).toBeTruthy();
+ expect(
+ urls.find(url => {
+ return url.includes('prerender/target.html?fromPrerendered');
+ })
+ ).toBeTruthy();
+ });
+ });
+
+ describe('with emulation', () => {
+ it('can configure viewport for prerendered pages', async () => {
+ const {page, server} = await getTestState();
+ await page.setViewport({
+ width: 300,
+ height: 400,
+ });
+ await page.goto(server.PREFIX + '/prerender/index.html');
+ using button = await page.waitForSelector('button');
+ await button?.click();
+ using link = await page.waitForSelector('a');
+ await Promise.all([page.waitForNavigation(), link?.click()]);
+ const result = await page.evaluate(() => {
+ return {
+ width: document.documentElement.clientWidth,
+ height: document.documentElement.clientHeight,
+ dpr: window.devicePixelRatio,
+ };
+ });
+ expect({
+ width: result.width,
+ height: result.height,
+ }).toStrictEqual({
+ width: 300 * result.dpr,
+ height: 400 * result.dpr,
+ });
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts b/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts
new file mode 100644
index 0000000000..405303fb6b
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts
@@ -0,0 +1,108 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js';
+
+describe('page.queryObjects', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ // Create a custom class
+ using classHandle = await page.evaluateHandle(() => {
+ return class CustomClass {};
+ });
+
+ // Create an instance.
+ await page.evaluate(CustomClass => {
+ // @ts-expect-error: Different context.
+ self.customClass = new CustomClass();
+ }, classHandle);
+
+ // Validate only one has been added.
+ using prototypeHandle = await page.evaluateHandle(CustomClass => {
+ return CustomClass.prototype;
+ }, classHandle);
+ using objectsHandle = await page.queryObjects(prototypeHandle);
+ await expect(
+ page.evaluate(objects => {
+ return objects.length;
+ }, objectsHandle)
+ ).resolves.toBe(1);
+
+ // Check that instances.
+ await expect(
+ page.evaluate(objects => {
+ // @ts-expect-error: Different context.
+ return objects[0] === self.customClass;
+ }, objectsHandle)
+ ).resolves.toBeTruthy();
+ });
+ it('should work for non-trivial page', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+
+ // Create a custom class
+ using classHandle = await page.evaluateHandle(() => {
+ return class CustomClass {};
+ });
+
+ // Create an instance.
+ await page.evaluate(CustomClass => {
+ // @ts-expect-error: Different context.
+ self.customClass = new CustomClass();
+ }, classHandle);
+
+ // Validate only one has been added.
+ using prototypeHandle = await page.evaluateHandle(CustomClass => {
+ return CustomClass.prototype;
+ }, classHandle);
+ using objectsHandle = await page.queryObjects(prototypeHandle);
+ await expect(
+ page.evaluate(objects => {
+ return objects.length;
+ }, objectsHandle)
+ ).resolves.toBe(1);
+
+ // Check that instances.
+ await expect(
+ page.evaluate(objects => {
+ // @ts-expect-error: Different context.
+ return objects[0] === self.customClass;
+ }, objectsHandle)
+ ).resolves.toBeTruthy();
+ });
+ it('should fail for disposed handles', async () => {
+ const {page} = await getTestState();
+
+ using prototypeHandle = await page.evaluateHandle(() => {
+ return HTMLBodyElement.prototype;
+ });
+ // We want to dispose early.
+ await prototypeHandle.dispose();
+ let error!: Error;
+ await page.queryObjects(prototypeHandle).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Prototype JSHandle is disposed!');
+ });
+ it('should fail primitive values as prototypes', async () => {
+ const {page} = await getTestState();
+
+ using prototypeHandle = await page.evaluateHandle(() => {
+ return 42;
+ });
+ let error!: Error;
+ await page.queryObjects(prototypeHandle).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe(
+ 'Prototype JSHandle must not be referencing primitive value'
+ );
+ });
+});
diff --git a/remote/test/puppeteer/test/src/chromiumonly.spec.ts b/remote/test/puppeteer/test/src/chromiumonly.spec.ts
new file mode 100644
index 0000000000..e0c41317aa
--- /dev/null
+++ b/remote/test/puppeteer/test/src/chromiumonly.spec.ts
@@ -0,0 +1,168 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {IncomingMessage} from 'http';
+
+import expect from 'expect';
+import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
+
+import {getTestState, setupTestBrowserHooks, launch} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+// TODO: rename this test suite to launch/connect test suite as it actually
+// works across browsers.
+describe('Chromium-Specific Launcher tests', function () {
+ describe('Puppeteer.launch |browserURL| option', function () {
+ it('should be able to connect using browserUrl, with and without trailing slash', async () => {
+ const {close, puppeteer} = await launch({
+ args: ['--remote-debugging-port=21222'],
+ });
+ try {
+ const browserURL = 'http://127.0.0.1:21222';
+
+ const browser1 = await puppeteer.connect({browserURL});
+ const page1 = await browser1.newPage();
+ expect(
+ await page1.evaluate(() => {
+ return 7 * 8;
+ })
+ ).toBe(56);
+ await browser1.disconnect();
+
+ const browser2 = await puppeteer.connect({
+ browserURL: browserURL + '/',
+ });
+ const page2 = await browser2.newPage();
+ expect(
+ await page2.evaluate(() => {
+ return 8 * 7;
+ })
+ ).toBe(56);
+ await browser2.disconnect();
+ } finally {
+ await close();
+ }
+ });
+ it('should throw when using both browserWSEndpoint and browserURL', async () => {
+ const {browser, close, puppeteer} = await launch({
+ args: ['--remote-debugging-port=21222'],
+ });
+ try {
+ const browserURL = 'http://127.0.0.1:21222';
+
+ let error!: Error;
+ await puppeteer
+ .connect({
+ browserURL,
+ browserWSEndpoint: browser.wsEndpoint(),
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain(
+ 'Exactly one of browserWSEndpoint, browserURL or transport'
+ );
+ } finally {
+ await close();
+ }
+ });
+ it('should throw when trying to connect to non-existing browser', async () => {
+ const {close, puppeteer} = await launch({
+ args: ['--remote-debugging-port=21222'],
+ });
+ try {
+ const browserURL = 'http://127.0.0.1:32333';
+
+ let error!: Error;
+ await puppeteer.connect({browserURL}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain(
+ 'Failed to fetch browser webSocket URL from'
+ );
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Puppeteer.launch |pipe| option', function () {
+ it('should support the pipe option', async () => {
+ const {browser, close} = await launch({pipe: true}, {createPage: false});
+ try {
+ expect(await browser.pages()).toHaveLength(1);
+ expect(browser.wsEndpoint()).toBe('');
+ const page = await browser.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ } finally {
+ await close();
+ }
+ });
+ it('should support the pipe argument', async () => {
+ const {defaultBrowserOptions} = await getTestState({skipLaunch: true});
+ const options = Object.assign({}, defaultBrowserOptions);
+ options.args = ['--remote-debugging-pipe'].concat(options.args || []);
+ const {browser, close} = await launch(options);
+ try {
+ expect(browser.wsEndpoint()).toBe('');
+ const page = await browser.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ } finally {
+ await close();
+ }
+ });
+ it('should fire "disconnected" when closing with pipe', async function () {
+ const {browser, close} = await launch({pipe: true});
+ try {
+ const disconnectedEventPromise = waitEvent(browser, 'disconnected');
+ // Emulate user exiting browser.
+ browser.process()!.kill();
+ await Deferred.race([
+ disconnectedEventPromise,
+ Deferred.create({
+ message: `Failed in after Hook`,
+ timeout: this.timeout() - 1000,
+ }),
+ ]);
+ } finally {
+ await close();
+ }
+ });
+ });
+});
+
+describe('Chromium-Specific Page Tests', function () {
+ setupTestBrowserHooks();
+
+ it('Page.setRequestInterception should work with intervention headers', async () => {
+ const {server, page} = await getTestState();
+
+ server.setRoute('/intervention', (_req, res) => {
+ return res.end(`
+ <script>
+ document.write('<script src="${server.CROSS_PROCESS_PREFIX}/intervention.js">' + '</scr' + 'ipt>');
+ </script>
+ `);
+ });
+ server.setRedirect('/intervention.js', '/redirect.js');
+ let serverRequest: IncomingMessage | undefined;
+ server.setRoute('/redirect.js', (req, res) => {
+ serverRequest = req;
+ res.end('console.log(1);');
+ });
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ await page.goto(server.PREFIX + '/intervention');
+ // Check for feature URL substring rather than https://www.chromestatus.com to
+ // make it work with Edgium.
+ expect(serverRequest!.headers['intervention']).toContain(
+ 'feature/5718547946799104'
+ );
+ });
+});
diff --git a/remote/test/puppeteer/test/src/click.spec.ts b/remote/test/puppeteer/test/src/click.spec.ts
new file mode 100644
index 0000000000..cdc0e6c133
--- /dev/null
+++ b/remote/test/puppeteer/test/src/click.spec.ts
@@ -0,0 +1,478 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {KnownDevices} from 'puppeteer';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame} from './utils.js';
+
+describe('Page.click', function () {
+ setupTestBrowserHooks();
+
+ it('should click the button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should click svg', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <svg height="100" width="100">
+ <circle onclick="javascript:window.__CLICKED=42" cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
+ </svg>
+ `);
+ await page.click('circle');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__CLICKED;
+ })
+ ).toBe(42);
+ });
+ it('should click the button if window.Node is removed', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.evaluate(() => {
+ // @ts-expect-error Expected.
+ return delete window.Node;
+ });
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4281
+ it('should click on a span with an inline element inside', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <style>
+ span::before {
+ content: 'q';
+ }
+ </style>
+ <span onclick='javascript:window.CLICKED=42'></span>
+ `);
+ await page.click('span');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).CLICKED;
+ })
+ ).toBe(42);
+ });
+ it('should not throw UnhandledPromiseRejection when page closes', async () => {
+ const {page} = await getTestState();
+
+ const newPage = await page.browser().newPage();
+ await Promise.all([newPage.close(), newPage.mouse.click(1, 2)]).catch(
+ () => {}
+ );
+ });
+ it('should click the button after navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.click('button');
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should click with disabled javascript', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setJavaScriptEnabled(false);
+ await page.goto(server.PREFIX + '/wrappedlink.html');
+ await Promise.all([page.click('a'), page.waitForNavigation()]);
+ expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
+ });
+ it('should scroll and click with disabled javascript', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setJavaScriptEnabled(false);
+ await page.goto(server.PREFIX + '/wrappedlink.html');
+ using body = await page.waitForSelector('body');
+ await body!.evaluate(el => {
+ el.style.paddingTop = '3000px';
+ });
+ await Promise.all([page.click('a'), page.waitForNavigation()]);
+ expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
+ });
+ it('should click when one of inline box children is outside of viewport', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <style>
+ i {
+ position: absolute;
+ top: -1000px;
+ }
+ </style>
+ <span onclick='javascript:window.CLICKED = 42;'><i>woof</i><b>doggo</b></span>
+ `);
+ await page.click('span');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).CLICKED;
+ })
+ ).toBe(42);
+ });
+ it('should select the text by triple clicking', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ const text =
+ "This is the text that we are going to try to select. Let's see how it goes.";
+ await page.keyboard.type(text);
+ await page.evaluate(() => {
+ (window as any).clicks = [];
+ window.addEventListener('click', event => {
+ return (window as any).clicks.push(event.detail);
+ });
+ });
+ await page.click('textarea', {count: 3});
+ expect(
+ await page.evaluate(() => {
+ return (window as any).clicks;
+ })
+ ).toMatchObject({0: 1, 1: 2, 2: 3});
+ expect(
+ await page.evaluate(() => {
+ const textarea = document.querySelector('textarea');
+ return textarea!.value.substring(
+ textarea!.selectionStart,
+ textarea!.selectionEnd
+ );
+ })
+ ).toBe(text);
+ });
+ it('should click offscreen buttons', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ const messages: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() === 'log') {
+ return messages.push(msg.text());
+ }
+ return;
+ });
+ for (let i = 0; i < 11; ++i) {
+ // We might've scrolled to click a button - reset to (0, 0).
+ await page.evaluate(() => {
+ return window.scrollTo(0, 0);
+ });
+ await page.click(`#btn${i}`);
+ }
+ expect(messages).toEqual([
+ 'button #0 clicked',
+ 'button #1 clicked',
+ 'button #2 clicked',
+ 'button #3 clicked',
+ 'button #4 clicked',
+ 'button #5 clicked',
+ 'button #6 clicked',
+ 'button #7 clicked',
+ 'button #8 clicked',
+ 'button #9 clicked',
+ 'button #10 clicked',
+ ]);
+ });
+
+ it('should click wrapped links', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/wrappedlink.html');
+ await page.click('a');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__clicked;
+ })
+ ).toBe(true);
+ });
+
+ it('should click on checkbox input and toggle', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/checkbox.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(null);
+ await page.click('input#agree');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.events;
+ })
+ ).toEqual([
+ 'mouseover',
+ 'mouseenter',
+ 'mousemove',
+ 'mousedown',
+ 'mouseup',
+ 'click',
+ 'input',
+ 'change',
+ ]);
+ await page.click('input#agree');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(false);
+ });
+
+ it('should click on checkbox label and toggle', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/checkbox.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(null);
+ await page.click('label[for="agree"]');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.events;
+ })
+ ).toEqual(['click', 'input', 'change']);
+ await page.click('label[for="agree"]');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(false);
+ });
+
+ it('should fail to click a missing button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ let error!: Error;
+ await page.click('button.does-not-exist').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe(
+ 'No element found for selector: button.does-not-exist'
+ );
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/161
+ it('should not hang with touch-enabled viewports', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport(KnownDevices['iPhone 6'].viewport);
+ await page.mouse.down();
+ await page.mouse.move(100, 10);
+ await page.mouse.up();
+ });
+ it('should scroll and click the button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-5');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-5')!.textContent;
+ })
+ ).toBe('clicked');
+ await page.click('#button-80');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-80')!.textContent;
+ })
+ ).toBe('clicked');
+ });
+ it('should double click the button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.evaluate(() => {
+ (globalThis as any).double = false;
+ const button = document.querySelector('button');
+ button!.addEventListener('dblclick', () => {
+ (globalThis as any).double = true;
+ });
+ });
+ using button = (await page.$('button'))!;
+ await button!.click({count: 2});
+ expect(await page.evaluate('double')).toBe(true);
+ expect(await page.evaluate('result')).toBe('Clicked');
+ });
+ it('should click a partially obscured button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.evaluate(() => {
+ const button = document.querySelector('button');
+ button!.textContent = 'Some really long text that will go offscreen';
+ button!.style.position = 'absolute';
+ button!.style.left = '368px';
+ });
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should click a rotated button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/rotatedButton.html');
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should fire contextmenu event on right click', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', {button: 'right'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-8')!.textContent;
+ })
+ ).toBe('context menu');
+ });
+ it('should fire aux event on middle click', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', {button: 'middle'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-8')!.textContent;
+ })
+ ).toBe('aux click');
+ });
+ it('should fire back click', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', {button: 'back'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-8')!.textContent;
+ })
+ ).toBe('back click');
+ });
+ it('should fire forward click', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', {button: 'forward'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-8')!.textContent;
+ })
+ ).toBe('forward click');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/206
+ it('should click links which cause navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setContent(`<a href="${server.EMPTY_PAGE}">empty.html</a>`);
+ // This await should not hang.
+ await page.click('a');
+ });
+ it('should click the button inside an iframe', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent('<div style="width:100px;height:100px">spacer</div>');
+ await attachFrame(
+ page,
+ 'button-test',
+ server.PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ using button = await frame!.$('button');
+ await button!.click();
+ expect(
+ await frame!.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4110
+ it('should click the button with fixed position inside an iframe', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(
+ '<div style="width:100px;height:2000px">spacer</div>'
+ );
+ await attachFrame(
+ page,
+ 'button-test',
+ server.CROSS_PROCESS_PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ await frame!.$eval('button', (button: Element) => {
+ return (button as HTMLElement).style.setProperty('position', 'fixed');
+ });
+ await frame!.click('button');
+ expect(
+ await frame!.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should click the button with deviceScaleFactor set', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 400, height: 400, deviceScaleFactor: 5});
+ expect(
+ await page.evaluate(() => {
+ return window.devicePixelRatio;
+ })
+ ).toBe(5);
+ await page.setContent('<div style="width:100px;height:100px">spacer</div>');
+ await attachFrame(
+ page,
+ 'button-test',
+ server.PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ using button = await frame!.$('button');
+ await button!.click();
+ expect(
+ await frame!.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cookies.spec.ts b/remote/test/puppeteer/test/src/cookies.spec.ts
new file mode 100644
index 0000000000..f232831b72
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cookies.spec.ts
@@ -0,0 +1,557 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+
+import {
+ expectCookieEquals,
+ getTestState,
+ launch,
+ setupTestBrowserHooks,
+} from './mocha-utils.js';
+
+describe('Cookie specs', () => {
+ setupTestBrowserHooks();
+
+ describe('Page.cookies', function () {
+ it('should return no cookies in pristine browser context', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await expectCookieEquals(await page.cookies(), []);
+ });
+ it('should get a cookie', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ document.cookie = 'username=John Doe';
+ });
+
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+ it('should properly report httpOnly cookie', async () => {
+ const {page, server} = await getTestState();
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('Set-Cookie', 'a=b; HttpOnly; Path=/');
+ res.end();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const cookies = await page.cookies();
+ expect(cookies).toHaveLength(1);
+ expect(cookies[0]!.httpOnly).toBe(true);
+ });
+ it('should properly report "Strict" sameSite cookie', async () => {
+ const {page, server} = await getTestState();
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('Set-Cookie', 'a=b; SameSite=Strict');
+ res.end();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const cookies = await page.cookies();
+ expect(cookies).toHaveLength(1);
+ expect(cookies[0]!.sameSite).toBe('Strict');
+ });
+ it('should properly report "Lax" sameSite cookie', async () => {
+ const {page, server} = await getTestState();
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('Set-Cookie', 'a=b; SameSite=Lax');
+ res.end();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const cookies = await page.cookies();
+ expect(cookies).toHaveLength(1);
+ expect(cookies[0]!.sameSite).toBe('Lax');
+ });
+ it('should get multiple cookies', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ document.cookie = 'username=John Doe';
+ document.cookie = 'password=1234';
+ });
+ const cookies = await page.cookies();
+ cookies.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ });
+ await expectCookieEquals(cookies, [
+ {
+ name: 'password',
+ value: '1234',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 12,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourceScheme: 'NonSecure',
+ },
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+ it('should get cookies from multiple urls', async () => {
+ const {page} = await getTestState();
+ await page.setCookie(
+ {
+ url: 'https://foo.com',
+ name: 'doggo',
+ value: 'woofs',
+ },
+ {
+ url: 'https://bar.com',
+ name: 'catto',
+ value: 'purrs',
+ },
+ {
+ url: 'https://baz.com',
+ name: 'birdo',
+ value: 'tweets',
+ }
+ );
+ const cookies = await page.cookies('https://foo.com', 'https://baz.com');
+ cookies.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ });
+ await expectCookieEquals(cookies, [
+ {
+ name: 'birdo',
+ value: 'tweets',
+ domain: 'baz.com',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 11,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ sourcePort: 443,
+ sourceScheme: 'Secure',
+ },
+ {
+ name: 'doggo',
+ value: 'woofs',
+ domain: 'foo.com',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 10,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ sourcePort: 443,
+ sourceScheme: 'Secure',
+ },
+ ]);
+ });
+ });
+ describe('Page.setCookie', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'password',
+ value: '123456',
+ });
+ expect(
+ await page.evaluate(() => {
+ return document.cookie;
+ })
+ ).toEqual('password=123456');
+ });
+ it('should isolate cookies in browser contexts', async () => {
+ const {page, server, browser} = await getTestState();
+
+ const anotherContext = await browser.createIncognitoBrowserContext();
+ const anotherPage = await anotherContext.newPage();
+
+ await page.goto(server.EMPTY_PAGE);
+ await anotherPage.goto(server.EMPTY_PAGE);
+
+ await page.setCookie({name: 'page1cookie', value: 'page1value'});
+ await anotherPage.setCookie({name: 'page2cookie', value: 'page2value'});
+
+ const cookies1 = await page.cookies();
+ const cookies2 = await anotherPage.cookies();
+ expect(cookies1).toHaveLength(1);
+ expect(cookies2).toHaveLength(1);
+ expect(cookies1[0]!.name).toBe('page1cookie');
+ expect(cookies1[0]!.value).toBe('page1value');
+ expect(cookies2[0]!.name).toBe('page2cookie');
+ expect(cookies2[0]!.value).toBe('page2value');
+ await anotherContext.close();
+ });
+ it('should set multiple cookies', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie(
+ {
+ name: 'password',
+ value: '123456',
+ },
+ {
+ name: 'foo',
+ value: 'bar',
+ }
+ );
+ const cookieStrings = await page.evaluate(() => {
+ const cookies = document.cookie.split(';');
+ return cookies
+ .map(cookie => {
+ return cookie.trim();
+ })
+ .sort();
+ });
+
+ expect(cookieStrings).toEqual(['foo=bar', 'password=123456']);
+ });
+ it('should have |expires| set to |-1| for session cookies', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'password',
+ value: '123456',
+ });
+ const cookies = await page.cookies();
+ expect(cookies[0]!.session).toBe(true);
+ expect(cookies[0]!.expires).toBe(-1);
+ });
+ it('should set cookie with reasonable defaults', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'password',
+ value: '123456',
+ });
+ const cookies = await page.cookies();
+ await expectCookieEquals(
+ cookies.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ }),
+ [
+ {
+ name: 'password',
+ value: '123456',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 14,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]
+ );
+ });
+ it('should set a cookie with a path', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.setCookie({
+ name: 'gridcookie',
+ value: 'GRID',
+ path: '/grid.html',
+ });
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'gridcookie',
+ value: 'GRID',
+ domain: 'localhost',
+ path: '/grid.html',
+ sameParty: false,
+ expires: -1,
+ size: 14,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID');
+ await page.goto(server.EMPTY_PAGE);
+ await expectCookieEquals(await page.cookies(), []);
+ expect(await page.evaluate('document.cookie')).toBe('');
+ await page.goto(server.PREFIX + '/grid.html');
+ expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID');
+ });
+ it('should not set a cookie on a blank page', async () => {
+ const {page} = await getTestState();
+
+ await page.goto('about:blank');
+ let error!: Error;
+ try {
+ await page.setCookie({name: 'example-cookie', value: 'best'});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain(
+ 'At least one of the url and domain needs to be specified'
+ );
+ });
+ it('should not set a cookie with blank page URL', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE);
+ try {
+ await page.setCookie(
+ {name: 'example-cookie', value: 'best'},
+ {url: 'about:blank', name: 'example-cookie-blank', value: 'best'}
+ );
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toEqual(
+ `Blank page can not have cookie "example-cookie-blank"`
+ );
+ });
+ it('should not set a cookie on a data URL page', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.goto('data:,Hello%2C%20World!');
+ try {
+ await page.setCookie({name: 'example-cookie', value: 'best'});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain(
+ 'At least one of the url and domain needs to be specified'
+ );
+ });
+ it('should default to setting secure cookie for HTTPS websites', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const SECURE_URL = 'https://example.com';
+ await page.setCookie({
+ url: SECURE_URL,
+ name: 'foo',
+ value: 'bar',
+ });
+ const [cookie] = await page.cookies(SECURE_URL);
+ expect(cookie!.secure).toBe(true);
+ });
+ it('should be able to set insecure cookie for HTTP website', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const HTTP_URL = 'http://example.com';
+ await page.setCookie({
+ url: HTTP_URL,
+ name: 'foo',
+ value: 'bar',
+ });
+ const [cookie] = await page.cookies(HTTP_URL);
+ expect(cookie!.secure).toBe(false);
+ });
+ it('should set a cookie on a different domain', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ url: 'https://www.example.com',
+ name: 'example-cookie',
+ value: 'best',
+ });
+ expect(await page.evaluate('document.cookie')).toBe('');
+ await expectCookieEquals(await page.cookies(), []);
+ await expectCookieEquals(await page.cookies('https://www.example.com'), [
+ {
+ name: 'example-cookie',
+ value: 'best',
+ domain: 'www.example.com',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 18,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ sourcePort: 443,
+ sourceScheme: 'Secure',
+ },
+ ]);
+ });
+ it('should set cookies from a frame', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.setCookie({name: 'localhost-cookie', value: 'best'});
+ await page.evaluate(src => {
+ let fulfill!: () => void;
+ const promise = new Promise<void>(x => {
+ return (fulfill = x);
+ });
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.onload = fulfill;
+ iframe.src = src;
+ return promise;
+ }, server.CROSS_PROCESS_PREFIX);
+ await page.setCookie({
+ name: '127-cookie',
+ value: 'worst',
+ url: server.CROSS_PROCESS_PREFIX,
+ });
+ expect(await page.evaluate('document.cookie')).toBe(
+ 'localhost-cookie=best'
+ );
+ expect(await page.frames()[1]!.evaluate('document.cookie')).toBe('');
+
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'localhost-cookie',
+ value: 'best',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 20,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+
+ await expectCookieEquals(
+ await page.cookies(server.CROSS_PROCESS_PREFIX),
+ [
+ {
+ name: '127-cookie',
+ value: 'worst',
+ domain: '127.0.0.1',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 15,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]
+ );
+ });
+ it('should set secure same-site cookies from a frame', async () => {
+ const {httpsServer, browser, close} = await launch({
+ ignoreHTTPSErrors: true,
+ });
+
+ try {
+ const page = await browser.newPage();
+ await page.goto(httpsServer.PREFIX + '/grid.html');
+ await page.evaluate(src => {
+ let fulfill!: () => void;
+ const promise = new Promise<void>(x => {
+ return (fulfill = x);
+ });
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.onload = fulfill;
+ iframe.src = src;
+ return promise;
+ }, httpsServer.CROSS_PROCESS_PREFIX);
+ await page.setCookie({
+ name: '127-same-site-cookie',
+ value: 'best',
+ url: httpsServer.CROSS_PROCESS_PREFIX,
+ sameSite: 'None',
+ });
+
+ expect(await page.frames()[1]!.evaluate('document.cookie')).toBe(
+ '127-same-site-cookie=best'
+ );
+ await expectCookieEquals(
+ await page.cookies(httpsServer.CROSS_PROCESS_PREFIX),
+ [
+ {
+ name: '127-same-site-cookie',
+ value: 'best',
+ domain: '127.0.0.1',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 24,
+ httpOnly: false,
+ sameSite: 'None',
+ secure: true,
+ session: true,
+ sourcePort: 443,
+ sourceScheme: 'Secure',
+ },
+ ]
+ );
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Page.deleteCookie', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie(
+ {
+ name: 'cookie1',
+ value: '1',
+ },
+ {
+ name: 'cookie2',
+ value: '2',
+ },
+ {
+ name: 'cookie3',
+ value: '3',
+ }
+ );
+ expect(await page.evaluate('document.cookie')).toBe(
+ 'cookie1=1; cookie2=2; cookie3=3'
+ );
+ await page.deleteCookie({name: 'cookie2'});
+ expect(await page.evaluate('document.cookie')).toBe(
+ 'cookie1=1; cookie3=3'
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/coverage.spec.ts b/remote/test/puppeteer/test/src/coverage.spec.ts
new file mode 100644
index 0000000000..6a95db541c
--- /dev/null
+++ b/remote/test/puppeteer/test/src/coverage.spec.ts
@@ -0,0 +1,343 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Coverage specs', function () {
+ setupTestBrowserHooks();
+
+ describe('JSCoverage', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/simple.html', {
+ waitUntil: 'load',
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toContain('/jscoverage/simple.html');
+ expect(coverage[0]!.ranges).toEqual([
+ {start: 0, end: 17},
+ {start: 35, end: 61},
+ ]);
+ });
+ it('should report sourceURLs', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/sourceurl.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toBe('nicename.js');
+ });
+ it('should ignore eval() scripts by default', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/eval.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ });
+ it('should not ignore eval() scripts if reportAnonymousScripts is true', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage({reportAnonymousScripts: true});
+ await page.goto(server.PREFIX + '/jscoverage/eval.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(
+ coverage.find(entry => {
+ return entry.url.startsWith('debugger://');
+ })
+ ).not.toBe(null);
+ expect(coverage).toHaveLength(2);
+ });
+ it('should ignore pptr internal scripts if reportAnonymousScripts is true', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage({reportAnonymousScripts: true});
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate('console.log("foo")');
+ await page.evaluate(() => {
+ return console.log('bar');
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(0);
+ });
+ it('should report multiple scripts', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/multiple.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(2);
+ coverage.sort((a, b) => {
+ return a.url.localeCompare(b.url);
+ });
+ expect(coverage[0]!.url).toContain('/jscoverage/script1.js');
+ expect(coverage[1]!.url).toContain('/jscoverage/script2.js');
+ });
+ it('should report right ranges', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/ranges.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ const entry = coverage[0]!;
+ expect(entry.ranges).toHaveLength(2);
+ const range1 = entry.ranges[0]!;
+ expect(entry.text.substring(range1.start, range1.end)).toBe('\n');
+ const range2 = entry.ranges[1]!;
+ expect(entry.text.substring(range2.start, range2.end)).toBe(
+ `console.log('used!');if(true===false)`
+ );
+ });
+ it('should report right ranges for "per function" scope', async () => {
+ const {page, server} = await getTestState();
+
+ const coverageOptions = {
+ useBlockCoverage: false,
+ };
+
+ await page.coverage.startJSCoverage(coverageOptions);
+ await page.goto(server.PREFIX + '/jscoverage/ranges.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ const entry = coverage[0]!;
+ expect(entry.ranges).toHaveLength(2);
+ const range1 = entry.ranges[0]!;
+ expect(entry.text.substring(range1.start, range1.end)).toBe('\n');
+ const range2 = entry.ranges[1]!;
+ expect(entry.text.substring(range2.start, range2.end)).toBe(
+ `console.log('used!');if(true===false)console.log('unused!');`
+ );
+ });
+ it('should report scripts that have no coverage', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/unused.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ const entry = coverage[0]!;
+ expect(entry.url).toContain('unused.html');
+ expect(entry.ranges).toHaveLength(0);
+ });
+ it('should work with conditionals', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/involved.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(
+ JSON.stringify(coverage, null, 2).replace(/:\d{4,5}\//g, ':<PORT>/')
+ ).toBeGolden('jscoverage-involved.txt');
+ });
+ // @see https://crbug.com/990945
+ it.skip('should not hang when there is a debugger statement', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ debugger; // eslint-disable-line no-debugger
+ });
+ await page.coverage.stopJSCoverage();
+ });
+ describe('resetOnNavigation', function () {
+ it('should report scripts across navigations when disabled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage({resetOnNavigation: false});
+ await page.goto(server.PREFIX + '/jscoverage/multiple.html');
+ // TODO: navigating too fast might loose JS coverage data in the browser.
+ await page.waitForNetworkIdle();
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(2);
+ });
+
+ it('should NOT report scripts across navigations when enabled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage(); // Enabled by default.
+ await page.goto(server.PREFIX + '/jscoverage/multiple.html');
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(0);
+ });
+ });
+ describe('includeRawScriptCoverage', function () {
+ it('should not include rawScriptCoverage field when disabled', async () => {
+ const {page, server} = await getTestState();
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/simple.html', {
+ waitUntil: 'load',
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.rawScriptCoverage).toBeUndefined();
+ });
+ it('should include rawScriptCoverage field when enabled', async () => {
+ const {page, server} = await getTestState();
+ await page.coverage.startJSCoverage({
+ includeRawScriptCoverage: true,
+ });
+ await page.goto(server.PREFIX + '/jscoverage/simple.html', {
+ waitUntil: 'load',
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.rawScriptCoverage).toBeTruthy();
+ });
+ });
+ // @see https://crbug.com/990945
+ it.skip('should not hang when there is a debugger statement', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ debugger; // eslint-disable-line no-debugger
+ });
+ await page.coverage.stopJSCoverage();
+ });
+ });
+
+ describe('CSSCoverage', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/simple.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toContain('/csscoverage/simple.html');
+ expect(coverage[0]!.ranges).toEqual([{start: 1, end: 22}]);
+ const range = coverage[0]!.ranges[0]!;
+ expect(coverage[0]!.text.substring(range.start, range.end)).toBe(
+ 'div { color: green; }'
+ );
+ });
+ it('should report sourceURLs', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/sourceurl.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toBe('nicename.css');
+ });
+ it('should report multiple stylesheets', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/multiple.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(2);
+ coverage.sort((a, b) => {
+ return a.url.localeCompare(b.url);
+ });
+ expect(coverage[0]!.url).toContain('/csscoverage/stylesheet1.css');
+ expect(coverage[1]!.url).toContain('/csscoverage/stylesheet2.css');
+ });
+ it('should report stylesheets that have no coverage', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/unused.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toBe('unused.css');
+ expect(coverage[0]!.ranges).toHaveLength(0);
+ });
+ it('should work with media queries', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/media.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toContain('/csscoverage/media.html');
+ expect(coverage[0]!.ranges).toEqual([
+ {start: 8, end: 15},
+ {start: 17, end: 38},
+ ]);
+ });
+ it('should work with complicated usecases', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/involved.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(
+ JSON.stringify(coverage, null, 2).replace(/:\d{4,5}\//g, ':<PORT>/')
+ ).toBeGolden('csscoverage-involved.txt');
+ });
+ it('should work with empty stylesheets', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/empty.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.text).toEqual('');
+ });
+ it('should ignore injected stylesheets', async () => {
+ const {page} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.addStyleTag({content: 'body { margin: 10px;}'});
+ // trigger style recalc
+ const margin = await page.evaluate(() => {
+ return window.getComputedStyle(document.body).margin;
+ });
+ expect(margin).toBe('10px');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(0);
+ });
+ it('should work with a recently loaded stylesheet', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.evaluate(async url => {
+ document.body.textContent = 'hello, world';
+
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = url;
+ document.head.appendChild(link);
+ await new Promise(x => {
+ return (link.onload = x);
+ });
+ }, server.PREFIX + '/csscoverage/stylesheet1.css');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ });
+ describe('resetOnNavigation', function () {
+ it('should report stylesheets across navigations', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage({resetOnNavigation: false});
+ await page.goto(server.PREFIX + '/csscoverage/multiple.html');
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(2);
+ });
+ it('should NOT report scripts across navigations', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage(); // Enabled by default.
+ await page.goto(server.PREFIX + '/csscoverage/multiple.html');
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(0);
+ });
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/debugInfo.spec.ts b/remote/test/puppeteer/test/src/debugInfo.spec.ts
new file mode 100644
index 0000000000..079107cab7
--- /dev/null
+++ b/remote/test/puppeteer/test/src/debugInfo.spec.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('DebugInfo', function () {
+ setupTestBrowserHooks();
+
+ describe('Browser.debugInfo', function () {
+ it('should work', async () => {
+ const {page, browser} = await getTestState();
+
+ const promise = page.evaluate(() => {
+ return new Promise(resolve => {
+ // @ts-expect-error another context
+ window.resolve = resolve;
+ });
+ });
+ try {
+ expect(browser.debugInfo.pendingProtocolErrors).toHaveLength(1);
+ } finally {
+ await page.evaluate(() => {
+ // @ts-expect-error another context
+ window.resolve();
+ });
+ }
+ await promise;
+ expect(browser.debugInfo.pendingProtocolErrors).toHaveLength(0);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts
new file mode 100644
index 0000000000..69a5a069af
--- /dev/null
+++ b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts
@@ -0,0 +1,104 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+
+import {
+ expectCookieEquals,
+ getTestState,
+ setupTestBrowserHooks,
+} from './mocha-utils.js';
+
+describe('DefaultBrowserContext', function () {
+ setupTestBrowserHooks();
+
+ it('page.cookies() should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ document.cookie = 'username=John Doe';
+ });
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+ it('page.setCookie() should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'username',
+ value: 'John Doe',
+ });
+ expect(
+ await page.evaluate(() => {
+ return document.cookie;
+ })
+ ).toBe('username=John Doe');
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+ it('page.deleteCookie() should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie(
+ {
+ name: 'cookie1',
+ value: '1',
+ },
+ {
+ name: 'cookie2',
+ value: '2',
+ }
+ );
+ expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2');
+ await page.deleteCookie({name: 'cookie2'});
+ expect(await page.evaluate('document.cookie')).toBe('cookie1=1');
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'cookie1',
+ value: '1',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 8,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/device-request-prompt.spec.ts b/remote/test/puppeteer/test/src/device-request-prompt.spec.ts
new file mode 100644
index 0000000000..e6e2cdd65e
--- /dev/null
+++ b/remote/test/puppeteer/test/src/device-request-prompt.spec.ts
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+
+import {launch} from './mocha-utils.js';
+
+describe('device request prompt', function () {
+ let state: Awaited<ReturnType<typeof launch>>;
+
+ before(async () => {
+ state = await launch(
+ {
+ args: ['--enable-features=WebBluetoothNewPermissionsBackend'],
+ ignoreHTTPSErrors: true,
+ },
+ {
+ after: 'all',
+ }
+ );
+ });
+
+ after(async () => {
+ await state.close();
+ });
+
+ beforeEach(async () => {
+ state.context = await state.browser.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ });
+
+ afterEach(async () => {
+ await state.context.close();
+ });
+
+ // Bug: #11072
+ it('does not crash', async function () {
+ this.timeout(1_000);
+
+ const {page, httpsServer} = state;
+
+ await page.goto(httpsServer.EMPTY_PAGE);
+
+ await expect(
+ page.waitForDevicePrompt({
+ timeout: 10,
+ })
+ ).rejects.toThrow(TimeoutError);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/dialog.spec.ts b/remote/test/puppeteer/test/src/dialog.spec.ts
new file mode 100644
index 0000000000..e137ccf517
--- /dev/null
+++ b/remote/test/puppeteer/test/src/dialog.spec.ts
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+import sinon from 'sinon';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Page.Events.Dialog', function () {
+ setupTestBrowserHooks();
+
+ it('should fire', async () => {
+ const {page} = await getTestState();
+
+ const onDialog = sinon.stub().callsFake(dialog => {
+ dialog.accept();
+ });
+ page.on('dialog', onDialog);
+
+ await page.evaluate(() => {
+ return alert('yo');
+ });
+
+ expect(onDialog.callCount).toEqual(1);
+ const dialog = onDialog.firstCall.args[0]!;
+ expect(dialog.type()).toBe('alert');
+ expect(dialog.defaultValue()).toBe('');
+ expect(dialog.message()).toBe('yo');
+ });
+
+ it('should allow accepting prompts', async () => {
+ const {page} = await getTestState();
+
+ const onDialog = sinon.stub().callsFake(dialog => {
+ dialog.accept('answer!');
+ });
+ page.on('dialog', onDialog);
+
+ const result = await page.evaluate(() => {
+ return prompt('question?', 'yes.');
+ });
+
+ expect(onDialog.callCount).toEqual(1);
+ const dialog = onDialog.firstCall.args[0]!;
+ expect(dialog.type()).toBe('prompt');
+ expect(dialog.defaultValue()).toBe('yes.');
+ expect(dialog.message()).toBe('question?');
+
+ expect(result).toBe('answer!');
+ });
+ it('should dismiss the prompt', async () => {
+ const {page} = await getTestState();
+
+ page.on('dialog', dialog => {
+ void dialog.dismiss();
+ });
+ const result = await page.evaluate(() => {
+ return prompt('question?');
+ });
+ expect(result).toBe(null);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/diffstyle.css b/remote/test/puppeteer/test/src/diffstyle.css
new file mode 100644
index 0000000000..202e85f41a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/diffstyle.css
@@ -0,0 +1,13 @@
+body {
+ font-family: monospace;
+ white-space: pre;
+}
+
+ins {
+ background-color: #9cffa0;
+ text-decoration: none;
+}
+
+del {
+ background-color: #ff9e9e;
+}
diff --git a/remote/test/puppeteer/test/src/drag-and-drop.spec.ts b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts
new file mode 100644
index 0000000000..cfe18b55a4
--- /dev/null
+++ b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts
@@ -0,0 +1,154 @@
+/**
+ * @license
+ * Copyright 2021 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+async function getDragState() {
+ const {page} = await getTestState({skipLaunch: true});
+ return parseInt(
+ await page.$eval('#drag-state', element => {
+ return element.innerHTML;
+ }),
+ 10
+ );
+}
+
+describe("Legacy Drag n' Drop", function () {
+ setupTestBrowserHooks();
+
+ it('should emit a dragIntercepted event when dragged', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ const data = await draggable.drag({x: 1, y: 1});
+
+ assert(data instanceof Object);
+ expect(data.items).toHaveLength(1);
+ expect(await getDragState()).toBe(1);
+ });
+ it('should emit a dragEnter', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ const data = await draggable.drag({x: 1, y: 1});
+ assert(data instanceof Object);
+ using dropzone = (await page.$('#drop'))!;
+ await dropzone.dragEnter(data);
+
+ expect(await getDragState()).toBe(12);
+ });
+ it('should emit a dragOver event', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ const data = await draggable.drag({x: 1, y: 1});
+ assert(data instanceof Object);
+ using dropzone = (await page.$('#drop'))!;
+ await dropzone.dragEnter(data);
+ await dropzone.dragOver(data);
+
+ expect(await getDragState()).toBe(123);
+ });
+ it('can be dropped', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ using dropzone = (await page.$('#drop'))!;
+ const data = await draggable.drag({x: 1, y: 1});
+ assert(data instanceof Object);
+ await dropzone.dragEnter(data);
+ await dropzone.dragOver(data);
+ await dropzone.drop(data);
+
+ expect(await getDragState()).toBe(12334);
+ });
+ it('can be dragged and dropped with a single function', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ using dropzone = (await page.$('#drop'))!;
+ await draggable.dragAndDrop(dropzone);
+
+ expect(await getDragState()).toBe(12334);
+ });
+});
+
+describe("Drag n' Drop", () => {
+ setupTestBrowserHooks();
+
+ it('should drop', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+
+ using draggable = await page.$('#drag');
+ assert(draggable);
+ using dropzone = await page.$('#drop');
+ assert(dropzone);
+
+ await dropzone.drop(draggable);
+
+ expect(await getDragState()).toBe(1234);
+ });
+ it('should drop using mouse', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+
+ using draggable = await page.$('#drag');
+ assert(draggable);
+ using dropzone = await page.$('#drop');
+ assert(dropzone);
+
+ await draggable.hover();
+ await page.mouse.down();
+ await dropzone.hover();
+
+ expect(await getDragState()).toBe(123);
+
+ await page.mouse.up();
+ expect(await getDragState()).toBe(1234);
+ });
+ it('should drag and drop', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+
+ using draggable = await page.$('#drag');
+ assert(draggable);
+ using dropzone = await page.$('#drop');
+ assert(dropzone);
+
+ await draggable.drag(dropzone);
+ await dropzone.drop(draggable);
+
+ expect(await getDragState()).toBe(1234);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/elementhandle.spec.ts b/remote/test/puppeteer/test/src/elementhandle.spec.ts
new file mode 100644
index 0000000000..9aaf914224
--- /dev/null
+++ b/remote/test/puppeteer/test/src/elementhandle.spec.ts
@@ -0,0 +1,953 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {Puppeteer} from 'puppeteer';
+import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
+import {
+ asyncDisposeSymbol,
+ disposeSymbol,
+} from 'puppeteer-core/internal/util/disposable.js';
+import sinon from 'sinon';
+
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ shortWaitForArrayToHaveAtLeastNElements,
+} from './mocha-utils.js';
+import {attachFrame} from './utils.js';
+
+describe('ElementHandle specs', function () {
+ setupTestBrowserHooks();
+
+ describe('ElementHandle.boundingBox', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ using elementHandle = (await page.$('.box:nth-of-type(13)'))!;
+ const box = await elementHandle.boundingBox();
+ expect(box).toEqual({x: 100, y: 50, width: 50, height: 50});
+ });
+ it('should handle nested frames', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ const nestedFrame = page.frames()[1]!.childFrames()[1]!;
+ using elementHandle = (await nestedFrame.$('div'))!;
+ const box = await elementHandle.boundingBox();
+ if (isChrome) {
+ expect(box).toEqual({x: 28, y: 182, width: 264, height: 18});
+ } else {
+ expect(box).toEqual({x: 28, y: 182, width: 254, height: 18});
+ }
+ });
+ it('should return null for invisible elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div style="display:none">hi</div>');
+ using element = (await page.$('div'))!;
+ expect(await element.boundingBox()).toBe(null);
+ });
+ it('should force a layout', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(
+ '<div style="width: 100px; height: 100px">hello</div>'
+ );
+ using elementHandle = (await page.$('div'))!;
+ await page.evaluate((element: HTMLElement) => {
+ return (element.style.height = '200px');
+ }, elementHandle);
+ const box = await elementHandle.boundingBox();
+ expect(box).toEqual({x: 8, y: 8, width: 100, height: 200});
+ });
+ it('should work with SVG nodes', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+ <rect id="theRect" x="30" y="50" width="200" height="300"></rect>
+ </svg>
+ `);
+ using element = (await page.$(
+ '#therect'
+ )) as ElementHandle<SVGRectElement>;
+ const pptrBoundingBox = await element.boundingBox();
+ const webBoundingBox = await page.evaluate(e => {
+ const rect = e.getBoundingClientRect();
+ return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
+ }, element);
+ expect(pptrBoundingBox).toEqual(webBoundingBox);
+ });
+ });
+
+ describe('ElementHandle.boxModel', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/resetcss.html');
+
+ // Step 1: Add Frame and position it absolutely.
+ await attachFrame(page, 'frame1', server.PREFIX + '/resetcss.html');
+ await page.evaluate(() => {
+ const frame = document.querySelector<HTMLElement>('#frame1')!;
+ frame.style.position = 'absolute';
+ frame.style.left = '1px';
+ frame.style.top = '2px';
+ });
+
+ // Step 2: Add div and position it absolutely inside frame.
+ const frame = page.frames()[1]!;
+ using divHandle = (
+ await frame.evaluateHandle(() => {
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ div.style.boxSizing = 'border-box';
+ div.style.position = 'absolute';
+ div.style.borderLeft = '1px solid black';
+ div.style.paddingLeft = '2px';
+ div.style.marginLeft = '3px';
+ div.style.left = '4px';
+ div.style.top = '5px';
+ div.style.width = '6px';
+ div.style.height = '7px';
+ return div;
+ })
+ ).asElement()!;
+
+ // Step 3: query div's boxModel and assert box values.
+ const box = (await divHandle.boxModel())!;
+ expect(box.width).toBe(6);
+ expect(box.height).toBe(7);
+ expect(box.margin[0]).toEqual({
+ x: 1 + 4, // frame.left + div.left
+ y: 2 + 5,
+ });
+ expect(box.border[0]).toEqual({
+ x: 1 + 4 + 3, // frame.left + div.left + div.margin-left
+ y: 2 + 5,
+ });
+ expect(box.padding[0]).toEqual({
+ x: 1 + 4 + 3 + 1, // frame.left + div.left + div.marginLeft + div.borderLeft
+ y: 2 + 5,
+ });
+ expect(box.content[0]).toEqual({
+ x: 1 + 4 + 3 + 1 + 2, // frame.left + div.left + div.marginLeft + div.borderLeft + div.paddingLeft
+ y: 2 + 5,
+ });
+ });
+
+ it('should return null for invisible elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div style="display:none">hi</div>');
+ using element = (await page.$('div'))!;
+ expect(await element.boxModel()).toBe(null);
+ });
+ });
+
+ describe('ElementHandle.contentFrame', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ using elementHandle = (await page.$('#frame1'))!;
+ const frame = await elementHandle.contentFrame();
+ expect(frame).toBe(page.frames()[1]);
+ });
+ });
+
+ describe('ElementHandle.isVisible and ElementHandle.isHidden', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ await page.setContent('<div style="display: none">text</div>');
+ using element = (await page.waitForSelector('div'))!;
+ await expect(element.isVisible()).resolves.toBeFalsy();
+ await expect(element.isHidden()).resolves.toBeTruthy();
+ await element.evaluate(e => {
+ e.style.removeProperty('display');
+ });
+ await expect(element.isVisible()).resolves.toBeTruthy();
+ await expect(element.isHidden()).resolves.toBeFalsy();
+ });
+ });
+
+ describe('ElementHandle.click', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await button.click();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should return Point data', async () => {
+ const {page} = await getTestState();
+
+ const clicks: Array<[x: number, y: number]> = [];
+
+ await page.exposeFunction('reportClick', (x: number, y: number): void => {
+ clicks.push([x, y]);
+ });
+
+ await page.evaluate(() => {
+ document.body.style.padding = '0';
+ document.body.style.margin = '0';
+ document.body.innerHTML = `
+ <div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
+ `;
+ document.body.addEventListener('click', e => {
+ (window as any).reportClick(e.clientX, e.clientY);
+ });
+ });
+
+ using divHandle = (await page.$('div'))!;
+ await divHandle.click();
+ await divHandle.click({
+ offset: {
+ x: 10,
+ y: 15,
+ },
+ });
+ await shortWaitForArrayToHaveAtLeastNElements(clicks, 2);
+ expect(clicks).toEqual([
+ [45 + 60, 45 + 30], // margin + middle point offset
+ [30 + 10, 30 + 15], // margin + offset
+ ]);
+ });
+ it('should work for Shadow DOM v1', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/shadow.html');
+ using buttonHandle = await page.evaluateHandle(() => {
+ // @ts-expect-error button is expected to be in the page's scope.
+ return button as HTMLButtonElement;
+ });
+ await buttonHandle.click();
+ expect(
+ await page.evaluate(() => {
+ // @ts-expect-error clicked is expected to be in the page's scope.
+ return clicked;
+ })
+ ).toBe(true);
+ });
+ it('should not work for TextNodes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using buttonTextNode = await page.evaluateHandle(() => {
+ return document.querySelector('button')!.firstChild as HTMLElement;
+ });
+ let error!: Error;
+ await buttonTextNode.click().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is not of type HTMLElement',
+ 'no such node',
+ ]);
+ });
+ it('should throw for detached nodes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await page.evaluate((button: HTMLElement) => {
+ return button.remove();
+ }, button);
+ let error!: Error;
+ await button.click().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is detached from document',
+ 'no such node',
+ ]);
+ });
+ it('should throw for hidden nodes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await page.evaluate((button: HTMLElement) => {
+ return (button.style.display = 'none');
+ }, button);
+ const error = await button.click().catch(error_ => {
+ return error_;
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is either not clickable or not an Element',
+ 'no such element',
+ ]);
+ });
+ it('should throw for recursively hidden nodes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await page.evaluate((button: HTMLElement) => {
+ return (button.parentElement!.style.display = 'none');
+ }, button);
+ const error = await button.click().catch(error_ => {
+ return error_;
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is either not clickable or not an Element',
+ 'no such element',
+ ]);
+ });
+ it('should throw for <br> elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('hello<br>goodbye');
+ using br = (await page.$('br'))!;
+ const error = await br.click().catch(error_ => {
+ return error_;
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is either not clickable or not an Element',
+ 'no such node',
+ ]);
+ });
+ });
+
+ describe('ElementHandle.clickablePoint', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ document.body.style.padding = '0';
+ document.body.style.margin = '0';
+ document.body.innerHTML = `
+ <div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
+ `;
+ });
+ await page.evaluate(async () => {
+ return await new Promise(resolve => {
+ return window.requestAnimationFrame(resolve);
+ });
+ });
+ using divHandle = (await page.$('div'))!;
+ expect(await divHandle.clickablePoint()).toEqual({
+ x: 45 + 60, // margin + middle point offset
+ y: 45 + 30, // margin + middle point offset
+ });
+ expect(
+ await divHandle.clickablePoint({
+ x: 10,
+ y: 15,
+ })
+ ).toEqual({
+ x: 30 + 10, // margin + offset
+ y: 30 + 15, // margin + offset
+ });
+ });
+
+ it('should not work if the click box is not visible', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<button style="width: 10px; height: 10px; position: absolute; left: -20px"></button>'
+ );
+ using handle = await page.locator('button').waitHandle();
+ await expect(handle.clickablePoint()).rejects.toBeInstanceOf(Error);
+
+ await page.setContent(
+ '<button style="width: 10px; height: 10px; position: absolute; right: -20px"></button>'
+ );
+ using handle2 = await page.locator('button').waitHandle();
+ await expect(handle2.clickablePoint()).rejects.toBeInstanceOf(Error);
+
+ await page.setContent(
+ '<button style="width: 10px; height: 10px; position: absolute; top: -20px"></button>'
+ );
+ using handle3 = await page.locator('button').waitHandle();
+ await expect(handle3.clickablePoint()).rejects.toBeInstanceOf(Error);
+
+ await page.setContent(
+ '<button style="width: 10px; height: 10px; position: absolute; bottom: -20px"></button>'
+ );
+ using handle4 = await page.locator('button').waitHandle();
+ await expect(handle4.clickablePoint()).rejects.toBeInstanceOf(Error);
+ });
+
+ it('should not work if the click box is not visible due to the iframe', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ `<iframe name='frame' style='position: absolute; left: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
+ );
+ const frame = await page.waitForFrame(frame => {
+ return frame.name() === 'frame';
+ });
+
+ using handle = await frame.locator('button').waitHandle();
+ await expect(handle.clickablePoint()).rejects.toBeInstanceOf(Error);
+
+ await page.setContent(
+ `<iframe name='frame2' style='position: absolute; top: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
+ );
+ const frame2 = await page.waitForFrame(frame => {
+ return frame.name() === 'frame2';
+ });
+
+ using handle2 = await frame2.locator('button').waitHandle();
+ await expect(handle2.clickablePoint()).rejects.toBeInstanceOf(Error);
+ });
+
+ it('should work for iframes', async () => {
+ const {page} = await getTestState();
+ await page.evaluate(() => {
+ document.body.style.padding = '10px';
+ document.body.style.margin = '10px';
+ document.body.innerHTML = `
+ <iframe style="border: none; margin: 0; padding: 0;" seamless sandbox srcdoc="<style>* { margin: 0; padding: 0;}</style><div style='cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;' />"></iframe>
+ `;
+ });
+ await page.evaluate(async () => {
+ return await new Promise(resolve => {
+ return window.requestAnimationFrame(resolve);
+ });
+ });
+ const frame = page.frames()[1]!;
+ using divHandle = (await frame.$('div'))!;
+ expect(await divHandle.clickablePoint()).toEqual({
+ x: 20 + 45 + 60, // iframe pos + margin + middle point offset
+ y: 20 + 45 + 30, // iframe pos + margin + middle point offset
+ });
+ expect(
+ await divHandle.clickablePoint({
+ x: 10,
+ y: 15,
+ })
+ ).toEqual({
+ x: 20 + 30 + 10, // iframe pos + margin + offset
+ y: 20 + 30 + 15, // iframe pos + margin + offset
+ });
+ });
+ });
+
+ describe('Element.waitForSelector', () => {
+ it('should wait correctly with waitForSelector on an element', async () => {
+ const {page} = await getTestState();
+ const waitFor = page.waitForSelector('.foo').catch(err => {
+ return err;
+ }) as Promise<ElementHandle<HTMLDivElement>>;
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>'
+ );
+ using element = (await waitFor)!;
+ if (element instanceof Error) {
+ throw element;
+ }
+ expect(element).toBeDefined();
+
+ const innerWaitFor = element.waitForSelector('.bar').catch(err => {
+ return err;
+ }) as Promise<ElementHandle<HTMLDivElement>>;
+ await element.evaluate(el => {
+ el.innerHTML = '<div class="bar">bar1</div>';
+ });
+ using element2 = (await innerWaitFor)!;
+ if (element2 instanceof Error) {
+ throw element2;
+ }
+ expect(element2).toBeDefined();
+ expect(
+ await element2.evaluate(el => {
+ return (el as HTMLElement).innerText;
+ })
+ ).toStrictEqual('bar1');
+ });
+ });
+
+ describe('Element.waitForXPath', () => {
+ it('should wait correctly with waitForXPath on an element', async () => {
+ const {page} = await getTestState();
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ `<div id=el1>
+ el1
+ <div id=el2>
+ el2
+ </div>
+ </div>
+ <div id=el3>
+ el3
+ </div>`
+ );
+
+ using el1 = (await page.waitForSelector(
+ '#el1'
+ )) as ElementHandle<HTMLDivElement>;
+
+ for (const path of ['//div', './/div']) {
+ using e = (await el1.waitForXPath(
+ path
+ )) as ElementHandle<HTMLDivElement>;
+ expect(
+ await e.evaluate(el => {
+ return el.id;
+ })
+ ).toStrictEqual('el2');
+ }
+ });
+ });
+
+ describe('ElementHandle.hover', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ using button = (await page.$('#button-6'))!;
+ await button.hover();
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-6');
+ });
+ });
+
+ describe('ElementHandle.isIntersectingViewport', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ async function getVisibilityForButton(selector: string) {
+ using button = (await page.$(selector))!;
+ return await button.isIntersectingViewport();
+ }
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ const buttonsPromises = [];
+ // Firefox seems slow when using `isIntersectingViewport`
+ // so we do all the tasks asynchronously
+ for (let i = 0; i < 11; ++i) {
+ buttonsPromises.push(getVisibilityForButton('#btn' + i));
+ }
+ const buttonVisibility = await Promise.all(buttonsPromises);
+ for (let i = 0; i < 11; ++i) {
+ // All but last button are visible.
+ const visible = i < 10;
+ expect(buttonVisibility[i]).toBe(visible);
+ }
+ });
+ it('should work with threshold', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ // a button almost cannot be seen
+ // sometimes we expect to return false by isIntersectingViewport1
+ using button = (await page.$('#btn11'))!;
+ expect(
+ await button.isIntersectingViewport({
+ threshold: 0.001,
+ })
+ ).toBe(false);
+ });
+ it('should work with threshold of 1', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ // a button almost cannot be seen
+ // sometimes we expect to return false by isIntersectingViewport1
+ using button = (await page.$('#btn0'))!;
+ expect(
+ await button.isIntersectingViewport({
+ threshold: 1,
+ })
+ ).toBe(true);
+ });
+ it('should work with svg elements', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/inline-svg.html');
+ const [visibleCircle, visibleSvg] = await Promise.all([
+ page.$('circle'),
+ page.$('svg'),
+ ]);
+
+ // Firefox seems slow when using `isIntersectingViewport`
+ // so we do all the tasks asynchronously
+ const [
+ circleThresholdOne,
+ circleThresholdZero,
+ svgThresholdOne,
+ svgThresholdZero,
+ ] = await Promise.all([
+ visibleCircle!.isIntersectingViewport({
+ threshold: 1,
+ }),
+ visibleCircle!.isIntersectingViewport({
+ threshold: 0,
+ }),
+ visibleSvg!.isIntersectingViewport({
+ threshold: 1,
+ }),
+ visibleSvg!.isIntersectingViewport({
+ threshold: 0,
+ }),
+ ]);
+
+ expect(circleThresholdOne).toBe(true);
+ expect(circleThresholdZero).toBe(true);
+ expect(svgThresholdOne).toBe(true);
+ expect(svgThresholdZero).toBe(true);
+
+ const [invisibleCircle, invisibleSvg] = await Promise.all([
+ page.$('div circle'),
+ page.$('div svg'),
+ ]);
+
+ // Firefox seems slow when using `isIntersectingViewport`
+ // so we do all the tasks asynchronously
+ const [
+ invisibleCircleThresholdOne,
+ invisibleCircleThresholdZero,
+ invisibleSvgThresholdOne,
+ invisibleSvgThresholdZero,
+ ] = await Promise.all([
+ invisibleCircle!.isIntersectingViewport({
+ threshold: 1,
+ }),
+ invisibleCircle!.isIntersectingViewport({
+ threshold: 0,
+ }),
+ invisibleSvg!.isIntersectingViewport({
+ threshold: 1,
+ }),
+ invisibleSvg!.isIntersectingViewport({
+ threshold: 0,
+ }),
+ ]);
+
+ expect(invisibleCircleThresholdOne).toBe(false);
+ expect(invisibleCircleThresholdZero).toBe(false);
+ expect(invisibleSvgThresholdOne).toBe(false);
+ expect(invisibleSvgThresholdZero).toBe(false);
+ });
+ });
+
+ describe('Custom queries', function () {
+ afterEach(() => {
+ Puppeteer.clearCustomQueryHandlers();
+ });
+ it('should register and unregister', async () => {
+ const {page} = await getTestState();
+ await page.setContent('<div id="not-foo"></div><div id="foo"></div>');
+
+ // Register.
+ Puppeteer.registerCustomQueryHandler('getById', {
+ queryOne: (_element, selector) => {
+ return document.querySelector(`[id="${selector}"]`);
+ },
+ });
+ using element = (await page.$(
+ 'getById/foo'
+ )) as ElementHandle<HTMLDivElement>;
+ expect(
+ await page.evaluate(element => {
+ return element.id;
+ }, element)
+ ).toBe('foo');
+ const handlerNamesAfterRegistering = Puppeteer.customQueryHandlerNames();
+ expect(handlerNamesAfterRegistering.includes('getById')).toBeTruthy();
+
+ // Unregister.
+ Puppeteer.unregisterCustomQueryHandler('getById');
+ try {
+ await page.$('getById/foo');
+ throw new Error('Custom query handler name not set - throw expected');
+ } catch (error) {
+ expect(error).not.toStrictEqual(
+ new Error('Custom query handler name not set - throw expected')
+ );
+ }
+ const handlerNamesAfterUnregistering =
+ Puppeteer.customQueryHandlerNames();
+ expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy();
+ });
+ it('should throw with invalid query names', async () => {
+ try {
+ Puppeteer.registerCustomQueryHandler('1/2/3', {
+ queryOne: () => {
+ return document.querySelector('foo');
+ },
+ });
+ throw new Error(
+ 'Custom query handler name was invalid - throw expected'
+ );
+ } catch (error) {
+ expect(error).toStrictEqual(
+ new Error('Custom query handler names may only contain [a-zA-Z]')
+ );
+ }
+ });
+ it('should work for multiple elements', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
+ );
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryAll: (_element, selector) => {
+ return [...document.querySelectorAll(`.${selector}`)];
+ },
+ });
+ const elements = (await page.$$('getByClass/foo')) as Array<
+ ElementHandle<HTMLDivElement>
+ >;
+ const classNames = await Promise.all(
+ elements.map(async element => {
+ return await page.evaluate(element => {
+ return element.className;
+ }, element);
+ })
+ );
+
+ expect(classNames).toStrictEqual(['foo', 'foo baz']);
+ });
+ it('should eval correctly', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
+ );
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryAll: (_element, selector) => {
+ return [...document.querySelectorAll(`.${selector}`)];
+ },
+ });
+ const elements = await page.$$eval('getByClass/foo', divs => {
+ return divs.length;
+ });
+
+ expect(elements).toBe(2);
+ });
+ it('should wait correctly with waitForSelector', async () => {
+ const {page} = await getTestState();
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ });
+ const waitFor = page.waitForSelector('getByClass/foo').catch(err => {
+ return err;
+ });
+
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">Foo1</div>'
+ );
+ const element = await waitFor;
+
+ if (element instanceof Error) {
+ throw element;
+ }
+
+ expect(element).toBeDefined();
+ });
+
+ it('should wait correctly with waitForSelector on an element', async () => {
+ const {page} = await getTestState();
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ });
+ const waitFor = page.waitForSelector('getByClass/foo').catch(err => {
+ return err;
+ }) as Promise<ElementHandle<HTMLElement>>;
+
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>'
+ );
+ using element = (await waitFor)!;
+ if (element instanceof Error) {
+ throw element;
+ }
+ expect(element).toBeDefined();
+
+ const innerWaitFor = element
+ .waitForSelector('getByClass/bar')
+ .catch(err => {
+ return err;
+ }) as Promise<ElementHandle<HTMLElement>>;
+
+ await element.evaluate(el => {
+ el.innerHTML = '<div class="bar">bar1</div>';
+ });
+
+ using element2 = (await innerWaitFor)!;
+ if (element2 instanceof Error) {
+ throw element2;
+ }
+ expect(element2).toBeDefined();
+ expect(
+ await element2.evaluate(el => {
+ return el.innerText;
+ })
+ ).toStrictEqual('bar1');
+ });
+
+ it('should wait correctly with waitFor', async () => {
+ /* page.waitFor is deprecated so we silence the warning to avoid test noise */
+ sinon.stub(console, 'warn').callsFake(() => {});
+ const {page} = await getTestState();
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ });
+ const waitFor = page.waitForSelector('getByClass/foo').catch(err => {
+ return err;
+ });
+
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">Foo1</div>'
+ );
+ const element = await waitFor;
+
+ if (element instanceof Error) {
+ throw element;
+ }
+
+ expect(element).toBeDefined();
+ });
+ it('should work when both queryOne and queryAll are registered', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo"><div id="nested-foo" class="foo"/></div><div class="foo baz">Foo2</div>'
+ );
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ queryAll: (element, selector) => {
+ return [...(element as Element).querySelectorAll(`.${selector}`)];
+ },
+ });
+
+ using element = (await page.$('getByClass/foo'))!;
+ expect(element).toBeDefined();
+
+ const elements = await page.$$('getByClass/foo');
+ expect(elements).toHaveLength(3);
+ });
+ it('should eval when both queryOne and queryAll are registered', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">text</div><div class="foo baz">content</div>'
+ );
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ queryAll: (element, selector) => {
+ return [...(element as Element).querySelectorAll(`.${selector}`)];
+ },
+ });
+
+ const txtContent = await page.$eval('getByClass/foo', div => {
+ return div.textContent;
+ });
+ expect(txtContent).toBe('text');
+
+ const txtContents = await page.$$eval('getByClass/foo', divs => {
+ return divs
+ .map(d => {
+ return d.textContent;
+ })
+ .join('');
+ });
+ expect(txtContents).toBe('textcontent');
+ });
+
+ it('should work with function shorthands', async () => {
+ const {page} = await getTestState();
+ await page.setContent('<div id="not-foo"></div><div id="foo"></div>');
+
+ Puppeteer.registerCustomQueryHandler('getById', {
+ // This is a function shorthand
+ queryOne(_element, selector) {
+ return document.querySelector(`[id="${selector}"]`);
+ },
+ });
+
+ using element = (await page.$(
+ 'getById/foo'
+ )) as ElementHandle<HTMLDivElement>;
+ expect(
+ await page.evaluate(element => {
+ return element.id;
+ }, element)
+ ).toBe('foo');
+ });
+ });
+
+ describe('ElementHandle.toElement', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ await page.setContent('<div class="foo">Foo1</div>');
+ using element = await page.$('.foo');
+ using div = await element?.toElement('div');
+ expect(div).toBeDefined();
+ });
+ });
+
+ describe('ElementHandle[Symbol.dispose]', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('document');
+ const spy = sinon.spy(handle, disposeSymbol);
+ {
+ using _ = handle;
+ }
+ expect(handle).toBeInstanceOf(ElementHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeTruthy();
+ });
+ });
+
+ describe('ElementHandle[Symbol.asyncDispose]', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('document');
+ const spy = sinon.spy(handle, asyncDisposeSymbol);
+ {
+ await using _ = handle;
+ }
+ expect(handle).toBeInstanceOf(ElementHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeTruthy();
+ });
+ });
+
+ describe('ElementHandle.move', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('document');
+ const spy = sinon.spy(handle, disposeSymbol);
+ {
+ using _ = handle;
+ handle.move();
+ }
+ expect(handle).toBeInstanceOf(ElementHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeFalsy();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/emulation.spec.ts b/remote/test/puppeteer/test/src/emulation.spec.ts
new file mode 100644
index 0000000000..823061c450
--- /dev/null
+++ b/remote/test/puppeteer/test/src/emulation.spec.ts
@@ -0,0 +1,553 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {KnownDevices, PredefinedNetworkConditions} from 'puppeteer';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+const iPhone = KnownDevices['iPhone 6'];
+const iPhoneLandscape = KnownDevices['iPhone 6 landscape'];
+
+describe('Emulation', () => {
+ setupTestBrowserHooks();
+
+ describe('Page.viewport', function () {
+ it('should get the proper viewport size', async () => {
+ const {page} = await getTestState();
+
+ expect(page.viewport()).toEqual({width: 800, height: 600});
+ await page.setViewport({width: 123, height: 456});
+ expect(page.viewport()).toEqual({width: 123, height: 456});
+ });
+ it('should support mobile emulation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(
+ await page.evaluate(() => {
+ return window.innerWidth;
+ })
+ ).toBe(800);
+ await page.setViewport(iPhone.viewport);
+ expect(
+ await page.evaluate(() => {
+ return window.innerWidth;
+ })
+ ).toBe(375);
+ await page.setViewport({width: 400, height: 300});
+ expect(
+ await page.evaluate(() => {
+ return window.innerWidth;
+ })
+ ).toBe(400);
+ });
+ it('should support touch emulation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(
+ await page.evaluate(() => {
+ return 'ontouchstart' in window;
+ })
+ ).toBe(false);
+ await page.setViewport(iPhone.viewport);
+ expect(
+ await page.evaluate(() => {
+ return 'ontouchstart' in window;
+ })
+ ).toBe(true);
+ expect(await page.evaluate(dispatchTouch)).toBe('Received touch');
+ await page.setViewport({width: 100, height: 100});
+ expect(
+ await page.evaluate(() => {
+ return 'ontouchstart' in window;
+ })
+ ).toBe(false);
+
+ function dispatchTouch() {
+ let fulfill!: (value: string) => void;
+ const promise = new Promise(x => {
+ fulfill = x;
+ });
+ window.ontouchstart = () => {
+ fulfill('Received touch');
+ };
+ window.dispatchEvent(new Event('touchstart'));
+
+ fulfill('Did not receive touch');
+
+ return promise;
+ }
+ });
+ it('should be detectable by Modernizr', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/detect-touch.html');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent!.trim();
+ })
+ ).toBe('NO');
+ await page.setViewport(iPhone.viewport);
+ await page.goto(server.PREFIX + '/detect-touch.html');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent!.trim();
+ })
+ ).toBe('YES');
+ });
+ it('should detect touch when applying viewport with touches', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 800, height: 600, hasTouch: true});
+ await page.addScriptTag({url: server.PREFIX + '/modernizr.js'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).Modernizr.touchevents;
+ })
+ ).toBe(true);
+ });
+ it('should support landscape emulation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(
+ await page.evaluate(() => {
+ return screen.orientation.type;
+ })
+ ).toBe('portrait-primary');
+ await page.setViewport(iPhoneLandscape.viewport);
+ expect(
+ await page.evaluate(() => {
+ return screen.orientation.type;
+ })
+ ).toBe('landscape-primary');
+ await page.setViewport({width: 100, height: 100});
+ expect(
+ await page.evaluate(() => {
+ return screen.orientation.type;
+ })
+ ).toBe('portrait-primary');
+ });
+ it('should update media queries when resoltion changes', async () => {
+ const {page, server} = await getTestState();
+
+ async function getFontSize() {
+ return await page.evaluate(() => {
+ return parseInt(
+ window.getComputedStyle(document.querySelector('p')!).fontSize,
+ 10
+ );
+ });
+ }
+
+ for (const dpr of [1, 2, 3]) {
+ await page.setViewport({
+ width: 800,
+ height: 600,
+ deviceScaleFactor: dpr,
+ });
+
+ await page.goto(server.PREFIX + '/resolution.html');
+
+ await expect(getFontSize()).resolves.toEqual(dpr);
+
+ const screenshot = await page.screenshot({
+ fullPage: false,
+ });
+ expect(screenshot).toBeGolden(`device-pixel-ratio${dpr}.png`);
+ }
+ });
+ it('should load correct pictures when emulation dpr', async () => {
+ const {page, server} = await getTestState();
+
+ async function getCurrentSrc() {
+ return await page.evaluate(() => {
+ return document.querySelector('img')!.currentSrc;
+ });
+ }
+
+ for (const dpr of [1, 2, 3]) {
+ await page.setViewport({
+ width: 800,
+ height: 600,
+ deviceScaleFactor: dpr,
+ });
+
+ await page.goto(server.PREFIX + '/picture.html');
+
+ await expect(getCurrentSrc()).resolves.toMatch(
+ new RegExp(`logo-${dpr}x.png`)
+ );
+ }
+ });
+ });
+
+ describe('Page.emulate', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ await page.emulate(iPhone);
+ expect(
+ await page.evaluate(() => {
+ return window.innerWidth;
+ })
+ ).toBe(375);
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).toContain('iPhone');
+ });
+ it('should support clicking', async () => {
+ const {page, server} = await getTestState();
+
+ await page.emulate(iPhone);
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await page.evaluate((button: HTMLElement) => {
+ return (button.style.marginTop = '200px');
+ }, button);
+ await button.click();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ });
+
+ describe('Page.emulateMediaType', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('screen').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('print').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaType('print');
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('screen').matches;
+ })
+ ).toBe(false);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('print').matches;
+ })
+ ).toBe(true);
+ await page.emulateMediaType();
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('screen').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('print').matches;
+ })
+ ).toBe(false);
+ });
+ it('should throw in case of bad argument', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.emulateMediaType('bad').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Unsupported media type: bad');
+ });
+ });
+
+ describe('Page.emulateMediaFeatures', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.emulateMediaFeatures([
+ {name: 'prefers-reduced-motion', value: 'reduce'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-reduced-motion: reduce)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-reduced-motion: no-preference)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ {name: 'prefers-color-scheme', value: 'light'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: light)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: dark)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ {name: 'prefers-color-scheme', value: 'dark'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: dark)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: light)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ {name: 'prefers-reduced-motion', value: 'reduce'},
+ {name: 'prefers-color-scheme', value: 'light'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-reduced-motion: reduce)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-reduced-motion: no-preference)').matches;
+ })
+ ).toBe(false);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: light)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: dark)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([{name: 'color-gamut', value: 'srgb'}]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: p3)').matches;
+ })
+ ).toBe(false);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: srgb)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: rec2020)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([{name: 'color-gamut', value: 'p3'}]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: p3)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: srgb)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: rec2020)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ {name: 'color-gamut', value: 'rec2020'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: p3)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: srgb)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: rec2020)').matches;
+ })
+ ).toBe(true);
+ });
+ it('should throw in case of bad argument', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .emulateMediaFeatures([{name: 'bad', value: ''}])
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Unsupported media feature: bad');
+ });
+ });
+
+ describe('Page.emulateTimezone', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ (globalThis as any).date = new Date(1479579154987);
+ });
+ await page.emulateTimezone('America/Jamaica');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).date.toString();
+ })
+ ).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)');
+
+ await page.emulateTimezone('Pacific/Honolulu');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).date.toString();
+ })
+ ).toBe(
+ 'Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'
+ );
+
+ await page.emulateTimezone('America/Buenos_Aires');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).date.toString();
+ })
+ ).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)');
+
+ await page.emulateTimezone('Europe/Berlin');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).date.toString();
+ })
+ ).toBe(
+ 'Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'
+ );
+ });
+
+ it('should throw for invalid timezone IDs', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.emulateTimezone('Foo/Bar').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Invalid timezone ID: Foo/Bar');
+ await page.emulateTimezone('Baz/Qux').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Invalid timezone ID: Baz/Qux');
+ });
+ });
+
+ describe('Page.emulateVisionDeficiency', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+
+ {
+ await page.emulateVisionDeficiency('none');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('screenshot-sanity.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('achromatopsia');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-achromatopsia.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('blurredVision');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-blurredVision.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('deuteranopia');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-deuteranopia.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('protanopia');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-protanopia.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('tritanopia');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-tritanopia.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('none');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('screenshot-sanity.png');
+ }
+ });
+
+ it('should throw for invalid vision deficiencies', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ // @ts-expect-error deliberately passing invalid deficiency
+ .emulateVisionDeficiency('invalid')
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Unsupported vision deficiency: invalid');
+ });
+ });
+
+ describe('Page.emulateNetworkConditions', function () {
+ it('should change navigator.connection.effectiveType', async () => {
+ const {page} = await getTestState();
+
+ const slow3G = PredefinedNetworkConditions['Slow 3G']!;
+ const fast3G = PredefinedNetworkConditions['Fast 3G']!;
+
+ expect(
+ await page.evaluate('window.navigator.connection.effectiveType')
+ ).toBe('4g');
+ await page.emulateNetworkConditions(fast3G);
+ expect(
+ await page.evaluate('window.navigator.connection.effectiveType')
+ ).toBe('3g');
+ await page.emulateNetworkConditions(slow3G);
+ expect(
+ await page.evaluate('window.navigator.connection.effectiveType')
+ ).toBe('2g');
+ await page.emulateNetworkConditions(null);
+ });
+ });
+
+ describe('Page.emulateCPUThrottling', function () {
+ it('should change the CPU throttling rate successfully', async () => {
+ const {page} = await getTestState();
+
+ await page.emulateCPUThrottling(100);
+ await page.emulateCPUThrottling(null);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/evaluation.spec.ts b/remote/test/puppeteer/test/src/evaluation.spec.ts
new file mode 100644
index 0000000000..3305b59cc2
--- /dev/null
+++ b/remote/test/puppeteer/test/src/evaluation.spec.ts
@@ -0,0 +1,607 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame} from './utils.js';
+
+describe('Evaluation specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.evaluate', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return 7 * 3;
+ });
+ expect(result).toBe(21);
+ });
+ it('should transfer BigInt', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate((a: bigint) => {
+ return a;
+ }, BigInt(42));
+ expect(result).toBe(BigInt(42));
+ });
+ it('should transfer NaN', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(a => {
+ return a;
+ }, NaN);
+ expect(Object.is(result, NaN)).toBe(true);
+ });
+ it('should transfer -0', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(a => {
+ return a;
+ }, -0);
+ expect(Object.is(result, -0)).toBe(true);
+ });
+ it('should transfer Infinity', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(a => {
+ return a;
+ }, Infinity);
+ expect(Object.is(result, Infinity)).toBe(true);
+ });
+ it('should transfer -Infinity', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(a => {
+ return a;
+ }, -Infinity);
+ expect(Object.is(result, -Infinity)).toBe(true);
+ });
+ it('should transfer arrays', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(
+ a => {
+ return a;
+ },
+ [1, 2, 3]
+ );
+ expect(result).toEqual([1, 2, 3]);
+ });
+ it('should transfer arrays as arrays, not objects', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(
+ a => {
+ return Array.isArray(a);
+ },
+ [1, 2, 3]
+ );
+ expect(result).toBe(true);
+ });
+ it('should modify global environment', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ return ((globalThis as any).globalVar = 123);
+ });
+ expect(await page.evaluate('globalVar')).toBe(123);
+ });
+ it('should evaluate in the page context', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/global-var.html');
+ expect(await page.evaluate('globalVar')).toBe(123);
+ });
+ it('should replace symbols with undefined', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return [Symbol('foo4'), 'foo'];
+ })
+ ).toEqual([undefined, 'foo']);
+ });
+ it('should work with function shorthands', async () => {
+ const {page} = await getTestState();
+
+ const a = {
+ sum(a: number, b: number) {
+ return a + b;
+ },
+
+ async mult(a: number, b: number) {
+ return a * b;
+ },
+ };
+ expect(await page.evaluate(a.sum, 1, 2)).toBe(3);
+ expect(await page.evaluate(a.mult, 2, 4)).toBe(8);
+ });
+ it('should work with unicode chars', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(
+ a => {
+ return a['中文字符'];
+ },
+ {
+ 中文字符: 42,
+ }
+ );
+ expect(result).toBe(42);
+ });
+ it('should throw when evaluation triggers reload', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ location.reload();
+ return new Promise(() => {});
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should await promise', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return Promise.resolve(8 * 7);
+ });
+ expect(result).toBe(56);
+ });
+ it('should work right after framenavigated', async () => {
+ const {page, server} = await getTestState();
+
+ let frameEvaluation = null;
+ page.on('framenavigated', async frame => {
+ frameEvaluation = frame.evaluate(() => {
+ return 6 * 7;
+ });
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(await frameEvaluation).toBe(42);
+ });
+ it('should work from-inside an exposed function', async () => {
+ const {page} = await getTestState();
+
+ // Setup inpage callback, which calls Page.evaluate
+ await page.exposeFunction(
+ 'callController',
+ async function (a: number, b: number) {
+ return await page.evaluate(
+ (a: number, b: number): number => {
+ return a * b;
+ },
+ a,
+ b
+ );
+ }
+ );
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).callController(9, 3);
+ });
+ expect(result).toBe(27);
+ });
+ it('should reject promise with exception', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ // @ts-expect-error we know the object doesn't exist
+ return notExistingObject.property;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ expect(error.message).toContain('notExistingObject');
+ });
+ it('should support thrown strings as error messages', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ throw 'qwerty';
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toEqual('qwerty');
+ });
+ it('should support thrown numbers as error messages', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ throw 100500;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toEqual(100500);
+ });
+ it('should return complex objects', async () => {
+ const {page} = await getTestState();
+
+ const object = {foo: 'bar!'};
+ const result = await page.evaluate(a => {
+ return a;
+ }, object);
+ expect(result).not.toBe(object);
+ expect(result).toEqual(object);
+ });
+ it('should return BigInt', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return BigInt(42);
+ });
+ expect(result).toBe(BigInt(42));
+ });
+ it('should return NaN', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return NaN;
+ });
+ expect(Object.is(result, NaN)).toBe(true);
+ });
+ it('should return -0', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return -0;
+ });
+ expect(Object.is(result, -0)).toBe(true);
+ });
+ it('should return Infinity', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return Infinity;
+ });
+ expect(Object.is(result, Infinity)).toBe(true);
+ });
+ it('should return -Infinity', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return -Infinity;
+ });
+ expect(Object.is(result, -Infinity)).toBe(true);
+ });
+ it('should accept "null" as one of multiple parameters', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(
+ (a, b) => {
+ return Object.is(a, null) && Object.is(b, 'foo');
+ },
+ null,
+ 'foo'
+ );
+ expect(result).toBe(true);
+ });
+ it('should properly serialize null fields', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return {a: undefined};
+ })
+ ).toEqual({});
+ });
+ it('should return undefined for non-serializable objects', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return window;
+ })
+ ).toBe(undefined);
+ });
+ it('should return promise as empty object', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return {
+ promise: new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ }),
+ };
+ });
+ expect(result).toEqual({
+ promise: {},
+ });
+ });
+ it('should work for circular object', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ const a: Record<string, unknown> = {
+ c: 5,
+ d: {
+ foo: 'bar',
+ },
+ };
+ const b = {a};
+ a['b'] = b;
+ return a;
+ });
+ expect(result).toMatchObject({
+ c: 5,
+ d: {
+ foo: 'bar',
+ },
+ b: {
+ a: undefined,
+ },
+ });
+ });
+ it('should accept a string', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate('1 + 2');
+ expect(result).toBe(3);
+ });
+ it('should accept a string with semi colons', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate('1 + 5;');
+ expect(result).toBe(6);
+ });
+ it('should accept a string with comments', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate('2 + 5;\n// do some math!');
+ expect(result).toBe(7);
+ });
+ it('should accept element handle as an argument', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>42</section>');
+ using element = (await page.$('section'))!;
+ const text = await page.evaluate(e => {
+ return e.textContent;
+ }, element);
+ expect(text).toBe('42');
+ });
+ it('should throw if underlying element was disposed', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>39</section>');
+ using element = (await page.$('section'))!;
+ expect(element).toBeTruthy();
+ // We want to dispose early.
+ await element.dispose();
+ let error!: Error;
+ await page
+ .evaluate((e: HTMLElement) => {
+ return e.textContent;
+ }, element)
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('JSHandle is disposed');
+ });
+ it('should throw if elementHandles are from other frames', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ using bodyHandle = await page.frames()[1]!.$('body');
+ let error!: Error;
+ await page
+ .evaluate(body => {
+ return body?.innerHTML;
+ }, bodyHandle)
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ expect(error.message).toContain(
+ 'JSHandles can be evaluated only in the context they were created'
+ );
+ });
+ it('should simulate a user gesture', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ document.body.appendChild(document.createTextNode('test'));
+ document.execCommand('selectAll');
+ return document.execCommand('copy');
+ });
+ expect(result).toBe(true);
+ });
+ it('should not throw an error when evaluation does a navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/one-style.html');
+ const onRequest = server.waitForRequest('/empty.html');
+ const result = await page.evaluate(() => {
+ (window as any).location = '/empty.html';
+ return [42];
+ });
+ expect(result).toEqual([42]);
+ await onRequest;
+ });
+ it('should transfer 100Mb of data from page to node.js', async function () {
+ this.timeout(25_000);
+ const {page} = await getTestState();
+
+ const a = await page.evaluate(() => {
+ return Array(100 * 1024 * 1024 + 1).join('a');
+ });
+ expect(a.length).toBe(100 * 1024 * 1024);
+ });
+ it('should throw error with detailed information on exception inside promise', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ return new Promise(() => {
+ throw new Error('Error in promise');
+ });
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Error in promise');
+ });
+
+ it('should return properly serialize objects with unknown type fields', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ "<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='>"
+ );
+
+ const result = await page.evaluate(async () => {
+ const image = document.querySelector('img')!;
+ const imageBitmap = await createImageBitmap(image);
+
+ return {
+ a: 'foo',
+ b: imageBitmap,
+ };
+ });
+
+ expect(result).toEqual({
+ a: 'foo',
+ b: undefined,
+ });
+ });
+ });
+
+ describe('Page.evaluateOnNewDocument', function () {
+ it('should evaluate before anything else on the page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.evaluateOnNewDocument(function () {
+ (globalThis as any).injected = 123;
+ });
+ await page.goto(server.PREFIX + '/tamperable.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe(123);
+ });
+ it('should work with CSP', async () => {
+ const {page, server} = await getTestState();
+
+ server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
+ await page.evaluateOnNewDocument(function () {
+ (globalThis as any).injected = 123;
+ });
+ await page.goto(server.PREFIX + '/empty.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).injected;
+ })
+ ).toBe(123);
+
+ // Make sure CSP works.
+ await page.addScriptTag({content: 'window.e = 10;'}).catch(error => {
+ return void error;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (window as any).e;
+ })
+ ).toBe(undefined);
+ });
+ });
+
+ describe('Page.removeScriptToEvaluateOnNewDocument', function () {
+ it('should remove new document script', async () => {
+ const {page, server} = await getTestState();
+
+ const {identifier} = await page.evaluateOnNewDocument(function () {
+ (globalThis as any).injected = 123;
+ });
+ await page.goto(server.PREFIX + '/tamperable.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe(123);
+
+ await page.removeScriptToEvaluateOnNewDocument(identifier);
+ await page.reload();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result || null;
+ })
+ ).toBe(null);
+ });
+ });
+
+ describe('Frame.evaluate', function () {
+ it('should have different execution contexts', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(page.frames()).toHaveLength(2);
+ await page.frames()[0]!.evaluate(() => {
+ return ((globalThis as any).FOO = 'foo');
+ });
+ await page.frames()[1]!.evaluate(() => {
+ return ((globalThis as any).FOO = 'bar');
+ });
+ expect(
+ await page.frames()[0]!.evaluate(() => {
+ return (globalThis as any).FOO;
+ })
+ ).toBe('foo');
+ expect(
+ await page.frames()[1]!.evaluate(() => {
+ return (globalThis as any).FOO;
+ })
+ ).toBe('bar');
+ });
+ it('should have correct execution contexts', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ expect(page.frames()).toHaveLength(2);
+ expect(
+ await page.frames()[0]!.evaluate(() => {
+ return document.body.textContent!.trim();
+ })
+ ).toBe('');
+ expect(
+ await page.frames()[1]!.evaluate(() => {
+ return document.body.textContent!.trim();
+ })
+ ).toBe(`Hi, I'm frame`);
+ });
+ it('should execute after cross-site navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ expect(
+ await mainFrame.evaluate(() => {
+ return window.location.href;
+ })
+ ).toContain('localhost');
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
+ expect(
+ await mainFrame.evaluate(() => {
+ return window.location.href;
+ })
+ ).toContain('127');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/fixtures.spec.ts b/remote/test/puppeteer/test/src/fixtures.spec.ts
new file mode 100644
index 0000000000..ca11e94cac
--- /dev/null
+++ b/remote/test/puppeteer/test/src/fixtures.spec.ts
@@ -0,0 +1,114 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {spawn, execSync} from 'child_process';
+import path from 'path';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+describe('Fixtures', function () {
+ setupTestBrowserHooks();
+
+ it('dumpio option should work with pipe option', async () => {
+ const {defaultBrowserOptions, puppeteerPath, headless} =
+ await getTestState();
+ if (headless !== 'true') {
+ // This test only works in the old headless mode.
+ return;
+ }
+
+ let dumpioData = '';
+ const options = Object.assign({}, defaultBrowserOptions, {
+ pipe: true,
+ dumpio: true,
+ });
+ const res = spawn('node', [
+ path.join(__dirname, '../fixtures', 'dumpio.js'),
+ puppeteerPath,
+ JSON.stringify(options),
+ ]);
+ res.stderr.on('data', data => {
+ dumpioData += data.toString('utf8');
+ });
+ await new Promise(resolve => {
+ return res.on('close', resolve);
+ });
+ expect(dumpioData).toContain('message from dumpio');
+ });
+ it('should dump browser process stderr', async () => {
+ const {defaultBrowserOptions, puppeteerPath} = await getTestState();
+
+ let dumpioData = '';
+ const options = Object.assign({}, defaultBrowserOptions, {dumpio: true});
+ const res = spawn('node', [
+ path.join(__dirname, '../fixtures', 'dumpio.js'),
+ puppeteerPath,
+ JSON.stringify(options),
+ ]);
+ res.stderr.on('data', data => {
+ dumpioData += data.toString('utf8');
+ });
+ await new Promise(resolve => {
+ return res.on('close', resolve);
+ });
+ expect(dumpioData).toContain('DevTools listening on ws://');
+ });
+ it('should close the browser when the node process closes', async () => {
+ const {defaultBrowserOptions, puppeteerPath, puppeteer} =
+ await getTestState();
+
+ const options = Object.assign({}, defaultBrowserOptions, {
+ // Disable DUMPIO to cleanly read stdout.
+ dumpio: false,
+ });
+ const res = spawn('node', [
+ path.join(__dirname, '../fixtures', 'closeme.js'),
+ puppeteerPath,
+ JSON.stringify(options),
+ ]);
+ let killed = false;
+ function killProcess() {
+ if (killed) {
+ return;
+ }
+ if (process.platform === 'win32') {
+ execSync(`taskkill /pid ${res.pid} /T /F`);
+ } else {
+ process.kill(res.pid!);
+ }
+ killed = true;
+ }
+ try {
+ let wsEndPointCallback: (value: string) => void;
+ const wsEndPointPromise = new Promise<string>(x => {
+ wsEndPointCallback = x;
+ });
+ let output = '';
+ res.stdout.on('data', data => {
+ output += data;
+ if (output.indexOf('\n')) {
+ wsEndPointCallback(output.substring(0, output.indexOf('\n')));
+ }
+ });
+ const browser = await puppeteer.connect({
+ browserWSEndpoint: await wsEndPointPromise,
+ });
+ const promises = [
+ waitEvent(browser, 'disconnected'),
+ new Promise(resolve => {
+ res.on('close', resolve);
+ }),
+ ];
+ killProcess();
+ await Promise.all(promises);
+ } finally {
+ killProcess();
+ }
+ });
+});
diff --git a/remote/test/puppeteer/test/src/frame.spec.ts b/remote/test/puppeteer/test/src/frame.spec.ts
new file mode 100644
index 0000000000..3b2456821a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/frame.spec.ts
@@ -0,0 +1,297 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
+import type {Frame} from 'puppeteer-core/internal/api/Frame.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {
+ attachFrame,
+ detachFrame,
+ dumpFrames,
+ navigateFrame,
+ waitEvent,
+} from './utils.js';
+
+describe('Frame specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Frame.evaluateHandle', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ using windowHandle = await mainFrame.evaluateHandle(() => {
+ return window;
+ });
+ expect(windowHandle).toBeTruthy();
+ });
+ });
+
+ describe('Frame.evaluate', function () {
+ it('should throw for detached frames', async () => {
+ const {page, server} = await getTestState();
+
+ const frame1 = (await attachFrame(page, 'frame1', server.EMPTY_PAGE))!;
+ await detachFrame(page, 'frame1');
+ let error: Error | undefined;
+ try {
+ await frame1.evaluate(() => {
+ return 7 * 8;
+ });
+ } catch (err) {
+ error = err as Error;
+ }
+ expect(error?.message).toContain('Attempted to use detached Frame');
+ });
+
+ it('allows readonly array to be an argument', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+
+ // This test checks if Frame.evaluate allows a readonly array to be an argument.
+ // See https://github.com/puppeteer/puppeteer/issues/6953.
+ const readonlyArray: readonly string[] = ['a', 'b', 'c'];
+ await mainFrame.evaluate(arr => {
+ return arr;
+ }, readonlyArray);
+ });
+ });
+
+ describe('Frame.page', function () {
+ it('should retrieve the page from a frame', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ expect(mainFrame.page()).toEqual(page);
+ });
+ });
+
+ describe('Frame Management', function () {
+ it('should handle nested frames', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ expect(dumpFrames(page.mainFrame())).toEqual([
+ 'http://localhost:<PORT>/frames/nested-frames.html',
+ ' http://localhost:<PORT>/frames/two-frames.html (2frames)',
+ ' http://localhost:<PORT>/frames/frame.html (uno)',
+ ' http://localhost:<PORT>/frames/frame.html (dos)',
+ ' http://localhost:<PORT>/frames/frame.html (aframe)',
+ ]);
+ });
+ it('should send events when frames are manipulated dynamically', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ // validate frameattached events
+ const attachedFrames: Frame[] = [];
+ page.on('frameattached', frame => {
+ return attachedFrames.push(frame);
+ });
+ await attachFrame(page, 'frame1', './assets/frame.html');
+ expect(attachedFrames).toHaveLength(1);
+ expect(attachedFrames[0]!.url()).toContain('/assets/frame.html');
+
+ // validate framenavigated events
+ const navigatedFrames: Frame[] = [];
+ page.on('framenavigated', frame => {
+ return navigatedFrames.push(frame);
+ });
+ await navigateFrame(page, 'frame1', './empty.html');
+ expect(navigatedFrames).toHaveLength(1);
+ expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE);
+
+ // validate framedetached events
+ const detachedFrames: Frame[] = [];
+ page.on('framedetached', frame => {
+ return detachedFrames.push(frame);
+ });
+ await detachFrame(page, 'frame1');
+ expect(detachedFrames).toHaveLength(1);
+ expect(detachedFrames[0]!.isDetached()).toBe(true);
+ });
+ it('should send "framenavigated" when navigating on anchor URLs', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await Promise.all([
+ page.goto(server.EMPTY_PAGE + '#foo'),
+ waitEvent(page, 'framenavigated'),
+ ]);
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
+ });
+ it('should persist mainFrame on cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
+ expect(page.mainFrame() === mainFrame).toBeTruthy();
+ });
+ it('should not send attach/detach events for main frame', async () => {
+ const {page, server} = await getTestState();
+
+ let hasEvents = false;
+ page.on('frameattached', () => {
+ return (hasEvents = true);
+ });
+ page.on('framedetached', () => {
+ return (hasEvents = true);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(hasEvents).toBe(false);
+ });
+ it('should detach child frames on navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let attachedFrames: Frame[] = [];
+ let detachedFrames: Frame[] = [];
+ let navigatedFrames: Frame[] = [];
+ page.on('frameattached', frame => {
+ return attachedFrames.push(frame);
+ });
+ page.on('framedetached', frame => {
+ return detachedFrames.push(frame);
+ });
+ page.on('framenavigated', frame => {
+ return navigatedFrames.push(frame);
+ });
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+
+ expect(attachedFrames).toHaveLength(4);
+ expect(detachedFrames).toHaveLength(0);
+ expect(navigatedFrames).toHaveLength(5);
+
+ attachedFrames = [];
+ detachedFrames = [];
+ navigatedFrames = [];
+ await page.goto(server.EMPTY_PAGE);
+ expect(attachedFrames).toHaveLength(0);
+ expect(detachedFrames).toHaveLength(4);
+ expect(navigatedFrames).toHaveLength(1);
+ });
+ it('should support framesets', async () => {
+ const {page, server} = await getTestState();
+
+ let attachedFrames: Frame[] = [];
+ let detachedFrames: Frame[] = [];
+ let navigatedFrames: Frame[] = [];
+ page.on('frameattached', frame => {
+ return attachedFrames.push(frame);
+ });
+ page.on('framedetached', frame => {
+ return detachedFrames.push(frame);
+ });
+ page.on('framenavigated', frame => {
+ return navigatedFrames.push(frame);
+ });
+ await page.goto(server.PREFIX + '/frames/frameset.html');
+ expect(attachedFrames).toHaveLength(4);
+ expect(detachedFrames).toHaveLength(0);
+ expect(navigatedFrames).toHaveLength(5);
+
+ attachedFrames = [];
+ detachedFrames = [];
+ navigatedFrames = [];
+ await page.goto(server.EMPTY_PAGE);
+ expect(attachedFrames).toHaveLength(0);
+ expect(detachedFrames).toHaveLength(4);
+ expect(navigatedFrames).toHaveLength(1);
+ });
+ it('should report frame from-inside shadow DOM', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/shadow.html');
+ await page.evaluate(async (url: string) => {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.shadowRoot!.appendChild(frame);
+ await new Promise(x => {
+ return (frame.onload = x);
+ });
+ }, server.EMPTY_PAGE);
+ expect(page.frames()).toHaveLength(2);
+ expect(page.frames()[1]!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should report frame.name()', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'theFrameId', server.EMPTY_PAGE);
+ await page.evaluate((url: string) => {
+ const frame = document.createElement('iframe');
+ frame.name = 'theFrameName';
+ frame.src = url;
+ document.body.appendChild(frame);
+ return new Promise(x => {
+ return (frame.onload = x);
+ });
+ }, server.EMPTY_PAGE);
+ expect(page.frames()[0]!.name()).toBe('');
+ expect(page.frames()[1]!.name()).toBe('theFrameId');
+ expect(page.frames()[2]!.name()).toBe('theFrameName');
+ });
+ it('should report frame.parent()', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(page, 'frame2', server.EMPTY_PAGE);
+ expect(page.frames()[0]!.parentFrame()).toBe(null);
+ expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame());
+ expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame());
+ });
+ it('should report different frame instance when frame re-attaches', async () => {
+ const {page, server} = await getTestState();
+
+ const frame1 = await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ (globalThis as any).frame = document.querySelector('#frame1');
+ (globalThis as any).frame.remove();
+ });
+ expect(frame1!.isDetached()).toBe(true);
+ const [frame2] = await Promise.all([
+ waitEvent(page, 'frameattached'),
+ page.evaluate(() => {
+ return document.body.appendChild((globalThis as any).frame);
+ }),
+ ]);
+ expect(frame2.isDetached()).toBe(false);
+ expect(frame1).not.toBe(frame2);
+ });
+ it('should support url fragment', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame-url-fragment.html');
+
+ expect(page.frames()).toHaveLength(2);
+ expect(page.frames()[1]!.url()).toBe(
+ server.PREFIX + '/frames/frame.html?param=value#fragment'
+ );
+ });
+ it('should support lazy frames', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 1000, height: 1000});
+ await page.goto(server.PREFIX + '/frames/lazy-frame.html');
+
+ expect(
+ page.frames().map(frame => {
+ return frame._hasStartedLoading;
+ })
+ ).toEqual([true, true, false]);
+ });
+ });
+
+ describe('Frame.client', function () {
+ it('should return the client instance', async () => {
+ const {page} = await getTestState();
+ expect(page.mainFrame().client).toBeInstanceOf(CDPSession);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/golden-utils.ts b/remote/test/puppeteer/test/src/golden-utils.ts
new file mode 100644
index 0000000000..939f69c968
--- /dev/null
+++ b/remote/test/puppeteer/test/src/golden-utils.ts
@@ -0,0 +1,169 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert';
+import fs from 'fs';
+import path from 'path';
+
+import {diffLines} from 'diff';
+import jpeg from 'jpeg-js';
+import mime from 'mime';
+import pixelmatch from 'pixelmatch';
+import {PNG} from 'pngjs';
+
+interface DiffFile {
+ diff: string | Buffer;
+ ext?: string;
+}
+
+const GoldenComparators = new Map<
+ string,
+ (
+ actualBuffer: string | Buffer,
+ expectedBuffer: string | Buffer,
+ mimeType: string
+ ) => DiffFile | undefined
+>();
+
+const addSuffix = (
+ filePath: string,
+ suffix: string,
+ customExtension?: string
+): string => {
+ const dirname = path.dirname(filePath);
+ const ext = path.extname(filePath);
+ const name = path.basename(filePath, ext);
+ return path.join(dirname, name + suffix + (customExtension || ext));
+};
+
+const compareImages = (
+ actualBuffer: string | Buffer,
+ expectedBuffer: string | Buffer,
+ mimeType: string
+): DiffFile | undefined => {
+ assert(typeof actualBuffer !== 'string');
+ assert(typeof expectedBuffer !== 'string');
+
+ const actual =
+ mimeType === 'image/png'
+ ? PNG.sync.read(actualBuffer)
+ : jpeg.decode(actualBuffer);
+
+ const expected =
+ mimeType === 'image/png'
+ ? PNG.sync.read(expectedBuffer)
+ : jpeg.decode(expectedBuffer);
+ if (expected.width !== actual.width || expected.height !== actual.height) {
+ throw new Error(
+ `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px.`
+ );
+ }
+ const diff = new PNG({width: expected.width, height: expected.height});
+ const count = pixelmatch(
+ expected.data,
+ actual.data,
+ diff.data,
+ expected.width,
+ expected.height,
+ {threshold: 0.1}
+ );
+ return count > 0 ? {diff: PNG.sync.write(diff)} : undefined;
+};
+
+const compareText = (
+ actual: string | Buffer,
+ expectedBuffer: string | Buffer
+): DiffFile | undefined => {
+ assert(typeof actual === 'string');
+ const expected = expectedBuffer.toString('utf-8');
+ if (expected === actual) {
+ return;
+ }
+ const result = diffLines(expected, actual);
+ const html = result.reduce(
+ (text, change) => {
+ text += change.added
+ ? `<span class='ins'>${change.value}</span>`
+ : change.removed
+ ? `<span class='del'>${change.value}</span>`
+ : change.value;
+ return text;
+ },
+ `<link rel="stylesheet" href="file://${path.join(
+ __dirname,
+ 'diffstyle.css'
+ )}">`
+ );
+ return {
+ diff: html,
+ ext: '.html',
+ };
+};
+
+GoldenComparators.set('image/png', compareImages);
+GoldenComparators.set('image/jpeg', compareImages);
+GoldenComparators.set('text/plain', compareText);
+
+export const compare = (
+ goldenPath: string,
+ outputPath: string,
+ actual: string | Buffer,
+ goldenName: string
+): {pass: true} | {pass: false; message: string} => {
+ goldenPath = path.normalize(goldenPath);
+ outputPath = path.normalize(outputPath);
+ const expectedPath = path.join(goldenPath, goldenName);
+ const actualPath = path.join(outputPath, goldenName);
+
+ const messageSuffix = `Output is saved in "${path.basename(
+ outputPath + '" directory'
+ )}`;
+
+ if (!fs.existsSync(expectedPath)) {
+ ensureOutputDir();
+ fs.writeFileSync(actualPath, actual);
+ return {
+ pass: false,
+ message: `${goldenName} is missing in golden results. ${messageSuffix}`,
+ };
+ }
+ const expected = fs.readFileSync(expectedPath);
+ const mimeType = mime.getType(goldenName);
+ assert(mimeType);
+ const comparator = GoldenComparators.get(mimeType);
+ if (!comparator) {
+ return {
+ pass: false,
+ message: `Failed to find comparator with type ${mimeType}: ${goldenName}`,
+ };
+ }
+ const result = comparator(actual, expected, mimeType);
+ if (!result) {
+ return {pass: true};
+ }
+ ensureOutputDir();
+ if (goldenPath === outputPath) {
+ fs.writeFileSync(addSuffix(actualPath, '-actual'), actual);
+ } else {
+ fs.writeFileSync(actualPath, actual);
+ // Copy expected to the output/ folder for convenience.
+ fs.writeFileSync(addSuffix(actualPath, '-expected'), expected);
+ }
+ if (result) {
+ const diffPath = addSuffix(actualPath, '-diff', result.ext);
+ fs.writeFileSync(diffPath, result.diff);
+ }
+
+ return {
+ pass: false,
+ message: `${goldenName} mismatch! ${messageSuffix}`,
+ };
+
+ function ensureOutputDir() {
+ if (!fs.existsSync(outputPath)) {
+ fs.mkdirSync(outputPath);
+ }
+ }
+};
diff --git a/remote/test/puppeteer/test/src/headful.spec.ts b/remote/test/puppeteer/test/src/headful.spec.ts
new file mode 100644
index 0000000000..1e3248b4ff
--- /dev/null
+++ b/remote/test/puppeteer/test/src/headful.spec.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {mkdtemp} from 'fs/promises';
+import os from 'os';
+import path from 'path';
+
+import expect from 'expect';
+import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+import {rmSync} from 'puppeteer-core/internal/node/util/fs.js';
+
+import {getTestState, isHeadless, launch} from './mocha-utils.js';
+
+const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
+
+(!isHeadless ? describe : describe.skip)('headful tests', function () {
+ /* These tests fire up an actual browser so let's
+ * allow a higher timeout
+ */
+ this.timeout(20_000);
+
+ let headfulOptions: PuppeteerLaunchOptions | undefined;
+ let headlessOptions: PuppeteerLaunchOptions & {headless: boolean};
+
+ const browsers: Array<() => Promise<void>> = [];
+
+ beforeEach(async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+ headfulOptions = Object.assign({}, defaultBrowserOptions, {
+ headless: false,
+ });
+ headlessOptions = Object.assign({}, defaultBrowserOptions, {
+ headless: true,
+ });
+ });
+
+ async function launchBrowser(options: any) {
+ const {browser, close} = await launch(options, {createContext: false});
+ browsers.push(close);
+ return browser;
+ }
+
+ afterEach(async () => {
+ await Promise.all(
+ browsers.map((close, index) => {
+ delete browsers[index];
+ return close();
+ })
+ );
+ });
+
+ describe('HEADFUL', function () {
+ it('headless should be able to read cookies written by headful', async () => {
+ /* Needs investigation into why but this fails consistently on Windows CI. */
+ const {server} = await getTestState({skipLaunch: true});
+
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ // Write a cookie in headful chrome
+ const headfulBrowser = await launchBrowser(
+ Object.assign({userDataDir}, headfulOptions)
+ );
+ const headfulPage = await headfulBrowser.newPage();
+ await headfulPage.goto(server.EMPTY_PAGE);
+ await headfulPage.evaluate(() => {
+ return (document.cookie =
+ 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
+ });
+ await headfulBrowser.close();
+ // Read the cookie from headless chrome
+ const headlessBrowser = await launchBrowser(
+ Object.assign({userDataDir}, headlessOptions)
+ );
+ const headlessPage = await headlessBrowser.newPage();
+ await headlessPage.goto(server.EMPTY_PAGE);
+ const cookie = await headlessPage.evaluate(() => {
+ return document.cookie;
+ });
+ await headlessBrowser.close();
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ expect(cookie).toBe('foo=true');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/idle_override.spec.ts b/remote/test/puppeteer/test/src/idle_override.spec.ts
new file mode 100644
index 0000000000..cbcfd34640
--- /dev/null
+++ b/remote/test/puppeteer/test/src/idle_override.spec.ts
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Emulate idle state', () => {
+ setupTestBrowserHooks();
+
+ async function getIdleState(page: Page) {
+ using stateElement = (await page.$('#state')) as ElementHandle<HTMLElement>;
+ return await page.evaluate(element => {
+ return element.innerText;
+ }, stateElement);
+ }
+
+ async function verifyState(page: Page, expectedState: string) {
+ const actualState = await getIdleState(page);
+ expect(actualState).toEqual(expectedState);
+ }
+
+ it('changing idle state emulation causes change of the IdleDetector state', async () => {
+ const {page, server, context} = await getTestState();
+ await context.overridePermissions(server.PREFIX + '/idle-detector.html', [
+ 'idle-detection',
+ ]);
+
+ await page.goto(server.PREFIX + '/idle-detector.html');
+
+ // Store initial state, as soon as it is not guaranteed to be `active, unlocked`.
+ const initialState = await getIdleState(page);
+
+ // Emulate Idle states and verify IdleDetector updates state accordingly.
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: false,
+ });
+ await verifyState(page, 'Idle state: idle, locked.');
+
+ await page.emulateIdleState({
+ isUserActive: true,
+ isScreenUnlocked: false,
+ });
+ await verifyState(page, 'Idle state: active, locked.');
+
+ await page.emulateIdleState({
+ isUserActive: true,
+ isScreenUnlocked: true,
+ });
+ await verifyState(page, 'Idle state: active, unlocked.');
+
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: true,
+ });
+ await verifyState(page, 'Idle state: idle, unlocked.');
+
+ // Remove Idle emulation and verify IdleDetector is in initial state.
+ await page.emulateIdleState();
+ await verifyState(page, initialState);
+
+ // Emulate idle state again after removing emulation.
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: false,
+ });
+ await verifyState(page, 'Idle state: idle, locked.');
+
+ // Remove emulation second time.
+ await page.emulateIdleState();
+ await verifyState(page, initialState);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts
new file mode 100644
index 0000000000..8fb557cb88
--- /dev/null
+++ b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts
@@ -0,0 +1,128 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {TLSSocket} from 'tls';
+
+import expect from 'expect';
+import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js';
+
+import {launch} from './mocha-utils.js';
+
+describe('ignoreHTTPSErrors', function () {
+ /* Note that this test creates its own browser rather than use
+ * the one provided by the test set-up as we need one
+ * with ignoreHTTPSErrors set to true
+ */
+ let state: Awaited<ReturnType<typeof launch>>;
+
+ before(async () => {
+ state = await launch(
+ {ignoreHTTPSErrors: true},
+ {
+ after: 'all',
+ }
+ );
+ });
+
+ after(async () => {
+ await state.close();
+ });
+
+ beforeEach(async () => {
+ state.context = await state.browser.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ });
+
+ afterEach(async () => {
+ await state.context.close();
+ });
+
+ describe('Response.securityDetails', function () {
+ it('should work', async () => {
+ const {httpsServer, page} = state;
+
+ const [serverRequest, response] = await Promise.all([
+ httpsServer.waitForRequest('/empty.html'),
+ page.goto(httpsServer.EMPTY_PAGE),
+ ]);
+ const securityDetails = response!.securityDetails()!;
+ expect(securityDetails.issuer()).toBe('puppeteer-tests');
+ const protocol = (serverRequest.socket as TLSSocket)
+ .getProtocol()!
+ .replace('v', ' ');
+ expect(securityDetails.protocol()).toBe(protocol);
+ expect(securityDetails.subjectName()).toBe('puppeteer-tests');
+ expect(securityDetails.validFrom()).toBe(1589357069);
+ expect(securityDetails.validTo()).toBe(1904717069);
+ expect(securityDetails.subjectAlternativeNames()).toEqual([
+ 'www.puppeteer-tests.test',
+ 'www.puppeteer-tests-1.test',
+ ]);
+ });
+ it('should be |null| for non-secure requests', async () => {
+ const {server, page} = state;
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.securityDetails()).toBe(null);
+ });
+ it('Network redirects should report SecurityDetails', async () => {
+ const {httpsServer, page} = state;
+
+ httpsServer.setRedirect('/plzredirect', '/empty.html');
+ const responses: HTTPResponse[] = [];
+ page.on('response', response => {
+ return responses.push(response);
+ });
+ const [serverRequest] = await Promise.all([
+ httpsServer.waitForRequest('/plzredirect'),
+ page.goto(httpsServer.PREFIX + '/plzredirect'),
+ ]);
+ expect(responses).toHaveLength(2);
+ expect(responses[0]!.status()).toBe(302);
+ const securityDetails = responses[0]!.securityDetails()!;
+ const protocol = (serverRequest.socket as TLSSocket)
+ .getProtocol()!
+ .replace('v', ' ');
+ expect(securityDetails.protocol()).toBe(protocol);
+ });
+ });
+
+ it('should work', async () => {
+ const {httpsServer, page} = state;
+
+ let error!: Error;
+ const response = await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ expect(response.ok()).toBe(true);
+ });
+ it('should work with request interception', async () => {
+ const {httpsServer, page} = state;
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const response = (await page.goto(httpsServer.EMPTY_PAGE))!;
+ expect(response.status()).toBe(200);
+ });
+ it('should work with mixed content', async () => {
+ const {server, httpsServer, page} = state;
+
+ httpsServer.setRoute('/mixedcontent.html', (_req, res) => {
+ res.end(`<iframe src=${server.EMPTY_PAGE}></iframe>`);
+ });
+ await page.goto(httpsServer.PREFIX + '/mixedcontent.html', {
+ waitUntil: 'load',
+ });
+ expect(page.frames()).toHaveLength(2);
+ // Make sure blocked iframe has functional execution context
+ // @see https://github.com/puppeteer/puppeteer/issues/2709
+ expect(await page.frames()[0]!.evaluate('1 + 2')).toBe(3);
+ expect(await page.frames()[1]!.evaluate('2 + 3')).toBe(5);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/injected.spec.ts b/remote/test/puppeteer/test/src/injected.spec.ts
new file mode 100644
index 0000000000..5f3696d3f6
--- /dev/null
+++ b/remote/test/puppeteer/test/src/injected.spec.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {LazyArg} from 'puppeteer-core/internal/common/LazyArg.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('PuppeteerUtil tests', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const world = page.mainFrame().isolatedRealm();
+ const value = await world.evaluate(
+ PuppeteerUtil => {
+ return typeof PuppeteerUtil === 'object';
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ })
+ );
+ expect(value).toBeTruthy();
+ });
+
+ describe('createFunction tests', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const world = page.mainFrame().isolatedRealm();
+ const value = await world.evaluate(
+ ({createFunction}, fnString) => {
+ return createFunction(fnString)(4);
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ (() => {
+ return 4;
+ }).toString()
+ );
+ expect(value).toBe(4);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/input.spec.ts b/remote/test/puppeteer/test/src/input.spec.ts
new file mode 100644
index 0000000000..7e4cae6709
--- /dev/null
+++ b/remote/test/puppeteer/test/src/input.spec.ts
@@ -0,0 +1,394 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt');
+
+describe('input tests', function () {
+ setupTestBrowserHooks();
+
+ describe('input', function () {
+ it('should upload the file', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/fileupload.html');
+ const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD);
+ using input = (await page.$('input'))!;
+ await page.evaluate((e: HTMLElement) => {
+ (globalThis as any)._inputEvents = [];
+ e.addEventListener('change', ev => {
+ return (globalThis as any)._inputEvents.push(ev.type);
+ });
+ e.addEventListener('input', ev => {
+ return (globalThis as any)._inputEvents.push(ev.type);
+ });
+ }, input);
+ await input.uploadFile(filePath);
+ expect(
+ await page.evaluate((e: HTMLInputElement) => {
+ return e.files![0]!.name;
+ }, input)
+ ).toBe('file-to-upload.txt');
+ expect(
+ await page.evaluate((e: HTMLInputElement) => {
+ return e.files![0]!.type;
+ }, input)
+ ).toBe('text/plain');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any)._inputEvents;
+ })
+ ).toEqual(['input', 'change']);
+ expect(
+ await page.evaluate((e: HTMLInputElement) => {
+ const reader = new FileReader();
+ const promise = new Promise(fulfill => {
+ return (reader.onload = fulfill);
+ });
+ reader.readAsText(e.files![0]!);
+ return promise.then(() => {
+ return reader.result;
+ });
+ }, input)
+ ).toBe('contents of the file');
+ });
+ });
+
+ describe('Page.waitForFileChooser', function () {
+ it('should work when file input is attached to DOM', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ expect(chooser).toBeTruthy();
+ });
+ it('should work when file input is not attached to DOM', async () => {
+ const {page} = await getTestState();
+
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.evaluate(() => {
+ const el = document.createElement('input');
+ el.type = 'file';
+ el.click();
+ }),
+ ]);
+ expect(chooser).toBeTruthy();
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForFileChooser({timeout: 1}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect default timeout when there is no custom timeout', async () => {
+ const {page} = await getTestState();
+
+ page.setDefaultTimeout(1);
+ let error!: Error;
+ await page.waitForFileChooser().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should prioritize exact timeout over default timeout', async () => {
+ const {page} = await getTestState();
+
+ page.setDefaultTimeout(0);
+ let error!: Error;
+ await page.waitForFileChooser({timeout: 1}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should work with no timeout', async () => {
+ const {page} = await getTestState();
+
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser({timeout: 0}),
+ page.evaluate(() => {
+ return setTimeout(() => {
+ const el = document.createElement('input');
+ el.type = 'file';
+ el.click();
+ }, 50);
+ }),
+ ]);
+ expect(chooser).toBeTruthy();
+ });
+ it('should return the same file chooser when there are many watchdogs simultaneously', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser1, fileChooser2] = await Promise.all([
+ page.waitForFileChooser(),
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLInputElement).click();
+ }),
+ ]);
+ expect(fileChooser1 === fileChooser2).toBe(true);
+ });
+ });
+
+ describe('FileChooser.accept', function () {
+ it('should accept single file', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ `<input type=file oninput='javascript:console.timeStamp()'>`
+ );
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ await Promise.all([
+ chooser.accept([FILE_TO_UPLOAD]),
+ waitEvent(page, 'metrics'),
+ ]);
+ expect(
+ await page.$eval('input', input => {
+ return (input as HTMLInputElement).files!.length;
+ })
+ ).toBe(1);
+ expect(
+ await page.$eval('input', input => {
+ return (input as HTMLInputElement).files![0]!.name;
+ })
+ ).toBe('file-to-upload.txt');
+ });
+ it('should be able to read selected file', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ void page.waitForFileChooser().then(chooser => {
+ return chooser.accept([FILE_TO_UPLOAD]);
+ });
+ expect(
+ await page.$eval('input', async picker => {
+ const pick = picker as HTMLInputElement;
+ pick.click();
+ await new Promise(x => {
+ return (pick.oninput = x);
+ });
+ const reader = new FileReader();
+ const promise = new Promise(fulfill => {
+ return (reader.onload = fulfill);
+ });
+ reader.readAsText(pick.files![0]!);
+ return await promise.then(() => {
+ return reader.result;
+ });
+ })
+ ).toBe('contents of the file');
+ });
+ it('should be able to reset selected files with empty file list', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ void page.waitForFileChooser().then(chooser => {
+ return chooser.accept([FILE_TO_UPLOAD]);
+ });
+ expect(
+ await page.$eval('input', async picker => {
+ const pick = picker as HTMLInputElement;
+ pick.click();
+ await new Promise(x => {
+ return (pick.oninput = x);
+ });
+ return pick.files!.length;
+ })
+ ).toBe(1);
+ void page.waitForFileChooser().then(chooser => {
+ return chooser.accept([]);
+ });
+ expect(
+ await page.$eval('input', async picker => {
+ const pick = picker as HTMLInputElement;
+ pick.click();
+ await new Promise(x => {
+ return (pick.oninput = x);
+ });
+ return pick.files!.length;
+ })
+ ).toBe(0);
+ });
+ it('should not accept multiple files for single-file input', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ let error!: Error;
+ await chooser
+ .accept([
+ path.relative(
+ process.cwd(),
+ __dirname + '/../assets/file-to-upload.txt'
+ ),
+ path.relative(process.cwd(), __dirname + '/../assets/pptr.png'),
+ ])
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).not.toBe(null);
+ });
+ it('should succeed even for non-existent files', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ let error!: Error;
+ await chooser.accept(['file-does-not-exist.txt']).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ });
+ it('should error on read of non-existent files', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ void page.waitForFileChooser().then(chooser => {
+ return chooser.accept(['file-does-not-exist.txt']);
+ });
+ expect(
+ await page.$eval('input', async picker => {
+ const pick = picker as HTMLInputElement;
+ pick.click();
+ await new Promise(x => {
+ return (pick.oninput = x);
+ });
+ const reader = new FileReader();
+ const promise = new Promise(fulfill => {
+ return (reader.onerror = fulfill);
+ });
+ reader.readAsText(pick.files![0]!);
+ return await promise.then(() => {
+ return false;
+ });
+ })
+ ).toBeFalsy();
+ });
+ it('should fail when accepting file chooser twice', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLInputElement).click();
+ }),
+ ]);
+ await fileChooser.accept([]);
+ let error!: Error;
+ await fileChooser.accept([]).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe(
+ 'Cannot accept FileChooser which is already handled!'
+ );
+ });
+ });
+
+ describe('FileChooser.cancel', function () {
+ it('should cancel dialog', async () => {
+ const {page} = await getTestState();
+
+ // Consider file chooser canceled if we can summon another one.
+ // There's no reliable way in WebPlatform to see that FileChooser was
+ // canceled.
+ await page.setContent(`<input type=file>`);
+ const [fileChooser1] = await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLInputElement).click();
+ }),
+ ]);
+ await fileChooser1.cancel();
+ // If this resolves, than we successfully canceled file chooser.
+ await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLInputElement).click();
+ }),
+ ]);
+ });
+ it('should fail when canceling file chooser twice', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLElement).click();
+ }),
+ ]);
+ await fileChooser.cancel();
+ let error!: Error;
+
+ try {
+ await fileChooser.cancel();
+ } catch (error_) {
+ error = error_ as Error;
+ }
+
+ expect(error.message).toBe(
+ 'Cannot cancel FileChooser which is already handled!'
+ );
+ });
+ });
+
+ describe('FileChooser.isMultiple', () => {
+ it('should work for single file pick', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ expect(chooser.isMultiple()).toBe(false);
+ });
+ it('should work for "multiple"', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input multiple type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ expect(chooser.isMultiple()).toBe(true);
+ });
+ it('should work for "webkitdirectory"', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input multiple webkitdirectory type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ expect(chooser.isMultiple()).toBe(true);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/jshandle.spec.ts b/remote/test/puppeteer/test/src/jshandle.spec.ts
new file mode 100644
index 0000000000..28097811e4
--- /dev/null
+++ b/remote/test/puppeteer/test/src/jshandle.spec.ts
@@ -0,0 +1,373 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {JSHandle} from 'puppeteer-core/internal/api/JSHandle.js';
+import {
+ asyncDisposeSymbol,
+ disposeSymbol,
+} from 'puppeteer-core/internal/util/disposable.js';
+import sinon from 'sinon';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('JSHandle', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.evaluateHandle', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using windowHandle = await page.evaluateHandle(() => {
+ return window;
+ });
+ expect(windowHandle).toBeTruthy();
+ });
+ it('should return the RemoteObject', async () => {
+ const {page} = await getTestState();
+
+ using windowHandle = await page.evaluateHandle(() => {
+ return window;
+ });
+ expect(windowHandle.remoteObject()).toBeTruthy();
+ });
+ it('should accept object handle as an argument', async () => {
+ const {page} = await getTestState();
+
+ using navigatorHandle = await page.evaluateHandle(() => {
+ return navigator;
+ });
+ const text = await page.evaluate(e => {
+ return e.userAgent;
+ }, navigatorHandle);
+ expect(text).toContain('Mozilla');
+ });
+ it('should accept object handle to primitive types', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return 5;
+ });
+ const isFive = await page.evaluate(e => {
+ return Object.is(e, 5);
+ }, aHandle);
+ expect(isFive).toBeTruthy();
+ });
+ it('should warn about recursive objects', async () => {
+ const {page} = await getTestState();
+
+ const test: {obj?: unknown} = {};
+ test.obj = test;
+ let error!: Error;
+ await page
+ .evaluateHandle(opts => {
+ return opts;
+ }, test)
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Recursive objects are not allowed.');
+ });
+ it('should accept object handle to unserializable value', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return Infinity;
+ });
+ expect(
+ await page.evaluate(e => {
+ return Object.is(e, Infinity);
+ }, aHandle)
+ ).toBe(true);
+ });
+ it('should use the same JS wrappers', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ (globalThis as any).FOO = 123;
+ return window;
+ });
+ expect(
+ await page.evaluate(e => {
+ return (e as any).FOO;
+ }, aHandle)
+ ).toBe(123);
+ });
+ });
+
+ describe('JSHandle.getProperty', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return {
+ one: 1,
+ two: 2,
+ three: 3,
+ };
+ });
+ using twoHandle = await aHandle.getProperty('two');
+ expect(await twoHandle.jsonValue()).toEqual(2);
+ });
+ });
+
+ describe('JSHandle.jsonValue', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return {foo: 'bar'};
+ });
+ const json = await aHandle.jsonValue();
+ expect(json).toEqual({foo: 'bar'});
+ });
+
+ it('works with jsonValues that are not objects', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return ['a', 'b'];
+ });
+ const json = await aHandle.jsonValue();
+ expect(json).toEqual(['a', 'b']);
+ });
+
+ it('works with jsonValues that are primitives', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return 'foo';
+ });
+ expect(await aHandle.jsonValue()).toEqual('foo');
+
+ using bHandle = await page.evaluateHandle(() => {
+ return undefined;
+ });
+ expect(await bHandle.jsonValue()).toEqual(undefined);
+ });
+
+ it('should work with dates', async () => {
+ const {page} = await getTestState();
+
+ using dateHandle = await page.evaluateHandle(() => {
+ return new Date('2017-09-26T00:00:00.000Z');
+ });
+ const date = await dateHandle.jsonValue();
+ expect(date).toBeInstanceOf(Date);
+ expect(date.toISOString()).toEqual('2017-09-26T00:00:00.000Z');
+ });
+ it('should not throw for circular objects', async () => {
+ const {page} = await getTestState();
+
+ using handle = await page.evaluateHandle(() => {
+ const t: {t?: unknown; g: number} = {g: 1};
+ t.t = t;
+ return t;
+ });
+ await handle.jsonValue();
+ });
+ });
+
+ describe('JSHandle.getProperties', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return {
+ foo: 'bar',
+ };
+ });
+ const properties = await aHandle.getProperties();
+ using foo = properties.get('foo')!;
+ expect(foo).toBeTruthy();
+ expect(await foo.jsonValue()).toBe('bar');
+ });
+ it('should return even non-own properties', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ class A {
+ a: string;
+ constructor() {
+ this.a = '1';
+ }
+ }
+ class B extends A {
+ b: string;
+ constructor() {
+ super();
+ this.b = '2';
+ }
+ }
+ return new B();
+ });
+ const properties = await aHandle.getProperties();
+ expect(await properties.get('a')!.jsonValue()).toBe('1');
+ expect(await properties.get('b')!.jsonValue()).toBe('2');
+ });
+ });
+
+ describe('JSHandle.asElement', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return document.body;
+ });
+ using element = aHandle.asElement();
+ expect(element).toBeTruthy();
+ });
+ it('should return null for non-elements', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return 2;
+ });
+ using element = aHandle.asElement();
+ expect(element).toBeFalsy();
+ });
+ it('should return ElementHandle for TextNodes', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>ee!</div>');
+ using aHandle = await page.evaluateHandle(() => {
+ return document.querySelector('div')!.firstChild;
+ });
+ using element = aHandle.asElement();
+ expect(element).toBeTruthy();
+ expect(
+ await page.evaluate(e => {
+ return e?.nodeType === Node.TEXT_NODE;
+ }, element)
+ );
+ });
+ });
+
+ describe('JSHandle.toString', function () {
+ it('should work for primitives', async () => {
+ const {page} = await getTestState();
+
+ using numberHandle = await page.evaluateHandle(() => {
+ return 2;
+ });
+ expect(numberHandle.toString()).toBe('JSHandle:2');
+ using stringHandle = await page.evaluateHandle(() => {
+ return 'a';
+ });
+ expect(stringHandle.toString()).toBe('JSHandle:a');
+ });
+ it('should work for complicated objects', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return window;
+ });
+ expect(aHandle.toString()).atLeastOneToContain([
+ 'JSHandle@object',
+ 'JSHandle@window',
+ ]);
+ });
+ it('should work with different subtypes', async () => {
+ const {page} = await getTestState();
+
+ expect((await page.evaluateHandle('(function(){})')).toString()).toBe(
+ 'JSHandle@function'
+ );
+ expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle:12');
+ expect((await page.evaluateHandle('true')).toString()).toBe(
+ 'JSHandle:true'
+ );
+ expect((await page.evaluateHandle('undefined')).toString()).toBe(
+ 'JSHandle:undefined'
+ );
+ expect((await page.evaluateHandle('"foo"')).toString()).toBe(
+ 'JSHandle:foo'
+ );
+ expect((await page.evaluateHandle('Symbol()')).toString()).toBe(
+ 'JSHandle@symbol'
+ );
+ expect((await page.evaluateHandle('new Map()')).toString()).toBe(
+ 'JSHandle@map'
+ );
+ expect((await page.evaluateHandle('new Set()')).toString()).toBe(
+ 'JSHandle@set'
+ );
+ expect((await page.evaluateHandle('[]')).toString()).toBe(
+ 'JSHandle@array'
+ );
+ expect((await page.evaluateHandle('null')).toString()).toBe(
+ 'JSHandle:null'
+ );
+ expect((await page.evaluateHandle('/foo/')).toString()).toBe(
+ 'JSHandle@regexp'
+ );
+ expect((await page.evaluateHandle('document.body')).toString()).toBe(
+ 'JSHandle@node'
+ );
+ expect((await page.evaluateHandle('new Date()')).toString()).toBe(
+ 'JSHandle@date'
+ );
+ expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe(
+ 'JSHandle@weakmap'
+ );
+ expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe(
+ 'JSHandle@weakset'
+ );
+ expect((await page.evaluateHandle('new Error()')).toString()).toBe(
+ 'JSHandle@error'
+ );
+ expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe(
+ 'JSHandle@typedarray'
+ );
+ expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe(
+ 'JSHandle@proxy'
+ );
+ });
+ });
+
+ describe('JSHandle[Symbol.dispose]', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('new Set()');
+ const spy = sinon.spy(handle, disposeSymbol);
+ {
+ using _ = handle;
+ }
+ expect(handle).toBeInstanceOf(JSHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeTruthy();
+ });
+ });
+
+ describe('JSHandle[Symbol.asyncDispose]', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('new Set()');
+ const spy = sinon.spy(handle, asyncDisposeSymbol);
+ {
+ await using _ = handle;
+ }
+ expect(handle).toBeInstanceOf(JSHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeTruthy();
+ });
+ });
+
+ describe('JSHandle.move', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('new Set()');
+ const spy = sinon.spy(handle, disposeSymbol);
+ {
+ using _ = handle;
+ handle.move();
+ }
+ expect(handle).toBeInstanceOf(JSHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeFalsy();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/keyboard.spec.ts b/remote/test/puppeteer/test/src/keyboard.spec.ts
new file mode 100644
index 0000000000..9157465242
--- /dev/null
+++ b/remote/test/puppeteer/test/src/keyboard.spec.ts
@@ -0,0 +1,550 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import os from 'os';
+
+import expect from 'expect';
+import type {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame} from './utils.js';
+
+describe('Keyboard', function () {
+ setupTestBrowserHooks();
+
+ it('should type into a textarea', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ const textarea = document.createElement('textarea');
+ document.body.appendChild(textarea);
+ textarea.focus();
+ });
+ const text = 'Hello world. I am the text that was typed!';
+ await page.keyboard.type(text);
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe(text);
+ });
+ it('should move with the arrow keys', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.type('textarea', 'Hello World!');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('Hello World!');
+ for (const _ of 'World!') {
+ await page.keyboard.press('ArrowLeft');
+ }
+ await page.keyboard.type('inserted ');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('Hello inserted World!');
+ await page.keyboard.down('Shift');
+ for (const _ of 'inserted ') {
+ await page.keyboard.press('ArrowLeft');
+ }
+ await page.keyboard.up('Shift');
+ await page.keyboard.press('Backspace');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('Hello World!');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/1313
+ it('should trigger commands of keyboard shortcuts', async () => {
+ const {page, server} = await getTestState();
+ const cmdKey = os.platform() === 'darwin' ? 'Meta' : 'Control';
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.type('textarea', 'hello');
+
+ await page.keyboard.down(cmdKey);
+ await page.keyboard.press('a', {commands: ['SelectAll']});
+ await page.keyboard.up(cmdKey);
+
+ await page.keyboard.down(cmdKey);
+ await page.keyboard.down('c', {commands: ['Copy']});
+ await page.keyboard.up('c');
+ await page.keyboard.up(cmdKey);
+
+ await page.keyboard.down(cmdKey);
+ await page.keyboard.press('v', {commands: ['Paste']});
+ await page.keyboard.press('v', {commands: ['Paste']});
+ await page.keyboard.up(cmdKey);
+
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('hellohello');
+ });
+ it('should send a character with ElementHandle.press', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ using textarea = (await page.$('textarea'))!;
+ await textarea.press('a');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('a');
+
+ await page.evaluate(() => {
+ return window.addEventListener(
+ 'keydown',
+ e => {
+ return e.preventDefault();
+ },
+ true
+ );
+ });
+
+ await textarea.press('b');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('a');
+ });
+ it('ElementHandle.press should not support |text| option', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ using textarea = (await page.$('textarea'))!;
+ await textarea.press('a', {text: 'ё'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('a');
+ });
+ it('should send a character with sendCharacter', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+
+ await page.evaluate(() => {
+ (globalThis as any).inputCount = 0;
+ (globalThis as any).keyDownCount = 0;
+ window.addEventListener(
+ 'input',
+ () => {
+ (globalThis as any).inputCount += 1;
+ },
+ true
+ );
+ window.addEventListener(
+ 'keydown',
+ () => {
+ (globalThis as any).keyDownCount += 1;
+ },
+ true
+ );
+ });
+
+ await page.keyboard.sendCharacter('嗨');
+ expect(
+ await page.$eval('textarea', textarea => {
+ return {
+ value: textarea.value,
+ inputs: (globalThis as any).inputCount,
+ keyDowns: (globalThis as any).keyDownCount,
+ };
+ })
+ ).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0});
+
+ await page.keyboard.sendCharacter('a');
+ expect(
+ await page.$eval('textarea', textarea => {
+ return {
+ value: textarea.value,
+ inputs: (globalThis as any).inputCount,
+ keyDowns: (globalThis as any).keyDownCount,
+ };
+ })
+ ).toMatchObject({value: '嗨a', inputs: 2, keyDowns: 0});
+ });
+ it('should send a character with sendCharacter in iframe', async () => {
+ this.timeout(2000);
+
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"</iframe>
+ `);
+ const frame = await page.waitForFrame(frame => {
+ return frame.name() === 'test';
+ });
+ await frame.focus('textarea');
+
+ await frame.evaluate(() => {
+ (globalThis as any).inputCount = 0;
+ (globalThis as any).keyDownCount = 0;
+ window.addEventListener(
+ 'input',
+ () => {
+ (globalThis as any).inputCount += 1;
+ },
+ true
+ );
+ window.addEventListener(
+ 'keydown',
+ () => {
+ (globalThis as any).keyDownCount += 1;
+ },
+ true
+ );
+ });
+
+ await page.keyboard.sendCharacter('嗨');
+ expect(
+ await frame.$eval('textarea', textarea => {
+ return {
+ value: textarea.value,
+ inputs: (globalThis as any).inputCount,
+ keyDowns: (globalThis as any).keyDownCount,
+ };
+ })
+ ).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0});
+
+ await page.keyboard.sendCharacter('a');
+ expect(
+ await frame.$eval('textarea', textarea => {
+ return {
+ value: textarea.value,
+ inputs: (globalThis as any).inputCount,
+ keyDowns: (globalThis as any).keyDownCount,
+ };
+ })
+ ).toMatchObject({value: '嗨a', inputs: 2, keyDowns: 0});
+ });
+ it('should report shiftKey', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ const keyboard = page.keyboard;
+ const codeForKey = new Set<KeyInput>(['Shift', 'Alt', 'Control']);
+ for (const modifierKey of codeForKey) {
+ await keyboard.down(modifierKey);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(`Keydown: ${modifierKey} ${modifierKey}Left [${modifierKey}]`);
+ await keyboard.down('!');
+ if (modifierKey === 'Shift') {
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(
+ `Keydown: ! Digit1 [${modifierKey}]\n` + `input: ! insertText false`
+ );
+ } else {
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(`Keydown: ! Digit1 [${modifierKey}]`);
+ }
+
+ await keyboard.up('!');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(`Keyup: ! Digit1 [${modifierKey}]`);
+ await keyboard.up(modifierKey);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(`Keyup: ${modifierKey} ${modifierKey}Left []`);
+ }
+ });
+ it('should report multiple modifiers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ const keyboard = page.keyboard;
+ await keyboard.down('Control');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keydown: Control ControlLeft [Control]');
+ await keyboard.down('Alt');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keydown: Alt AltLeft [Alt Control]');
+ await keyboard.down(';');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keydown: ; Semicolon [Alt Control]');
+ await keyboard.up(';');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keyup: ; Semicolon [Alt Control]');
+ await keyboard.up('Control');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keyup: Control ControlLeft [Alt]');
+ await keyboard.up('Alt');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keyup: Alt AltLeft []');
+ });
+ it('should send proper codes while typing', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ await page.keyboard.type('!');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(
+ [
+ 'Keydown: ! Digit1 []',
+ 'input: ! insertText false',
+ 'Keyup: ! Digit1 []',
+ ].join('\n')
+ );
+ await page.keyboard.type('^');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(
+ [
+ 'Keydown: ^ Digit6 []',
+ 'input: ^ insertText false',
+ 'Keyup: ^ Digit6 []',
+ ].join('\n')
+ );
+ });
+ it('should send proper codes while typing with shift', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ const keyboard = page.keyboard;
+ await keyboard.down('Shift');
+ await page.keyboard.type('~');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(
+ [
+ 'Keydown: Shift ShiftLeft [Shift]',
+ 'Keydown: ~ Backquote [Shift]',
+ 'input: ~ insertText false',
+ 'Keyup: ~ Backquote [Shift]',
+ ].join('\n')
+ );
+ await keyboard.up('Shift');
+ });
+ it('should not type canceled events', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ await page.evaluate(() => {
+ window.addEventListener(
+ 'keydown',
+ event => {
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+ if (event.key === 'l') {
+ event.preventDefault();
+ }
+ if (event.key === 'o') {
+ event.preventDefault();
+ }
+ },
+ false
+ );
+ });
+ await page.keyboard.type('Hello World!');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).textarea.value;
+ })
+ ).toBe('He Wrd!');
+ });
+ it('should specify repeat property', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.addEventListener(
+ 'keydown',
+ e => {
+ return ((globalThis as any).lastEvent = e);
+ },
+ true
+ );
+ });
+ await page.keyboard.down('a');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(false);
+ await page.keyboard.press('a');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(true);
+
+ await page.keyboard.down('b');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(false);
+ await page.keyboard.down('b');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(true);
+
+ await page.keyboard.up('a');
+ await page.keyboard.down('a');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(false);
+ });
+ it('should type all kinds of characters', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ const text = 'This text goes onto two lines.\nThis character is 嗨.';
+ await page.keyboard.type(text);
+ expect(await page.evaluate('result')).toBe(text);
+ });
+ it('should specify location', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.evaluate(() => {
+ window.addEventListener(
+ 'keydown',
+ event => {
+ return ((globalThis as any).keyLocation = event.location);
+ },
+ true
+ );
+ });
+ using textarea = (await page.$('textarea'))!;
+
+ await textarea.press('Digit5');
+ expect(await page.evaluate('keyLocation')).toBe(0);
+
+ await textarea.press('ControlLeft');
+ expect(await page.evaluate('keyLocation')).toBe(1);
+
+ await textarea.press('ControlRight');
+ expect(await page.evaluate('keyLocation')).toBe(2);
+
+ await textarea.press('NumpadSubtract');
+ expect(await page.evaluate('keyLocation')).toBe(3);
+ });
+ it('should throw on unknown keys', async () => {
+ const {page} = await getTestState();
+
+ const error = await page.keyboard
+ // @ts-expect-error bad input
+ .press('NotARealKey')
+ .catch(error_ => {
+ return error_;
+ });
+ expect(error.message).toBe('Unknown key: "NotARealKey"');
+ });
+ it('should type emoji', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.type('textarea', '👹 Tokyo street Japan 🇯🇵');
+ expect(
+ await page.$eval('textarea', textarea => {
+ return textarea.value;
+ })
+ ).toBe('👹 Tokyo street Japan 🇯🇵');
+ });
+ it('should type emoji into an iframe', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(
+ page,
+ 'emoji-test',
+ server.PREFIX + '/input/textarea.html'
+ );
+ const frame = page.frames()[1]!;
+ using textarea = (await frame.$('textarea'))!;
+ await textarea.type('👹 Tokyo street Japan 🇯🇵');
+ expect(
+ await frame.$eval('textarea', textarea => {
+ return textarea.value;
+ })
+ ).toBe('👹 Tokyo street Japan 🇯🇵');
+ });
+ it('should press the meta key', async () => {
+ // This test only makes sense on macOS.
+ if (os.platform() !== 'darwin') {
+ return;
+ }
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ (globalThis as any).result = null;
+ document.addEventListener('keydown', event => {
+ (globalThis as any).result = [event.key, event.code, event.metaKey];
+ });
+ });
+ await page.keyboard.press('Meta');
+ // Have to do this because we lose a lot of type info when evaluating a
+ // string not a function. This is why functions are recommended rather than
+ // using strings (although we'll leave this test so we have coverage of both
+ // approaches.)
+ const [key, code, metaKey] = (await page.evaluate('result')) as [
+ string,
+ string,
+ boolean,
+ ];
+ expect(key).toBe('Meta');
+ expect(code).toBe('MetaLeft');
+ expect(metaKey).toBe(true);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/launcher.spec.ts b/remote/test/puppeteer/test/src/launcher.spec.ts
new file mode 100644
index 0000000000..f31b22b1e5
--- /dev/null
+++ b/remote/test/puppeteer/test/src/launcher.spec.ts
@@ -0,0 +1,1025 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert';
+import fs from 'fs';
+import {mkdtemp, readFile, writeFile} from 'fs/promises';
+import os from 'os';
+import path from 'path';
+import type {TLSSocket} from 'tls';
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+import {rmSync} from 'puppeteer-core/internal/node/util/fs.js';
+import sinon from 'sinon';
+
+import {getTestState, isHeadless, launch} from './mocha-utils.js';
+import {dumpFrames, waitEvent} from './utils.js';
+
+const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
+const FIREFOX_TIMEOUT = 30_000;
+
+describe('Launcher specs', function () {
+ this.timeout(FIREFOX_TIMEOUT);
+
+ describe('Puppeteer', function () {
+ describe('Browser.disconnect', function () {
+ it('should reject navigation when browser closes', async () => {
+ const {browser, close, puppeteer, server} = await launch({});
+ server.setRoute('/one-style.css', () => {});
+ try {
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const page = await remote.newPage();
+ const navigationPromise = page
+ .goto(server.PREFIX + '/one-style.html', {timeout: 60000})
+ .catch(error_ => {
+ return error_;
+ });
+ await server.waitForRequest('/one-style.css');
+ await remote.disconnect();
+ const error = await navigationPromise;
+ expect(
+ [
+ 'Navigating frame was detached',
+ 'Protocol error (Page.navigate): Target closed.',
+ ].includes(error.message)
+ ).toBeTruthy();
+ } finally {
+ await close();
+ }
+ });
+ it('should reject waitForSelector when browser closes', async () => {
+ const {browser, close, server, puppeteer} = await launch({});
+ server.setRoute('/empty.html', () => {});
+ try {
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const page = await remote.newPage();
+ const watchdog = page
+ .waitForSelector('div', {timeout: 60000})
+ .catch(error_ => {
+ return error_;
+ });
+ await remote.disconnect();
+ const error = await watchdog;
+ expect(error.message).toContain('Session closed.');
+ } finally {
+ await close();
+ }
+ });
+ });
+ describe('Browser.close', function () {
+ it('should terminate network waiters', async () => {
+ const {browser, close, server, puppeteer} = await launch({});
+ try {
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const newPage = await remote.newPage();
+ const results = await Promise.all([
+ newPage.waitForRequest(server.EMPTY_PAGE).catch(error => {
+ return error;
+ }),
+ newPage.waitForResponse(server.EMPTY_PAGE).catch(error => {
+ return error;
+ }),
+ browser.close(),
+ ]);
+ for (let i = 0; i < 2; i++) {
+ const message = results[i].message;
+ expect(message).atLeastOneToContain([
+ 'Target closed',
+ 'Page closed!',
+ ]);
+ expect(message).not.toContain('Timeout');
+ }
+ } finally {
+ await close();
+ }
+ });
+ });
+ describe('Puppeteer.launch', function () {
+ it('can launch and close the browser', async () => {
+ const {close} = await launch({});
+ await close();
+ });
+ it('should have default url when launching browser', async function () {
+ const {browser, close} = await launch({}, {createContext: false});
+ try {
+ const pages = (await browser.pages()).map(
+ (page: {url: () => any}) => {
+ return page.url();
+ }
+ );
+ expect(pages).toEqual(['about:blank']);
+ } finally {
+ await close();
+ }
+ });
+ it('should close browser with beforeunload page', async () => {
+ const {browser, server, close} = await launch(
+ {},
+ {createContext: false}
+ );
+ try {
+ const page = await browser.newPage();
+
+ await page.goto(server.PREFIX + '/beforeunload.html');
+ // We have to interact with a page so that 'beforeunload' handlers
+ // fire.
+ await page.click('body');
+ } finally {
+ await close();
+ }
+ });
+ it('should reject all promises when browser is closed', async () => {
+ const {page, close} = await launch({});
+ let error!: Error;
+ const neverResolves = page
+ .evaluate(() => {
+ return new Promise(() => {});
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ await close();
+ await neverResolves;
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should reject if executable path is invalid', async () => {
+ let waitError!: Error;
+ await launch({
+ executablePath: 'random-invalid-path',
+ }).catch(error => {
+ return (waitError = error);
+ });
+ expect(waitError.message).toContain('Failed to launch');
+ });
+ it('userDataDir option', async () => {
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ const {context, close} = await launch({userDataDir});
+ // Open a page to make sure its functional.
+ try {
+ await context.newPage();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ } finally {
+ await close();
+ }
+
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('tmp profile should be cleaned up', async () => {
+ const {puppeteer} = await getTestState({skipLaunch: true});
+
+ // Set a custom test tmp dir so that we can validate that
+ // the profile dir is created and then cleaned up.
+ const testTmpDir = await fs.promises.mkdtemp(
+ path.join(os.tmpdir(), 'puppeteer_test_chrome_profile-')
+ );
+ const oldTmpDir = puppeteer.configuration.temporaryDirectory;
+ puppeteer.configuration.temporaryDirectory = testTmpDir;
+
+ // Path should be empty before starting the browser.
+ expect(fs.readdirSync(testTmpDir)).toHaveLength(0);
+ const {context, close} = await launch({});
+ try {
+ // One profile folder should have been created at this moment.
+ const profiles = fs.readdirSync(testTmpDir);
+ expect(profiles).toHaveLength(1);
+ expect(profiles[0]?.startsWith('puppeteer_dev_chrome_profile-')).toBe(
+ true
+ );
+
+ // Open a page to make sure its functional.
+ await context.newPage();
+ } finally {
+ await close();
+ }
+
+ // Profile should be deleted after closing the browser
+ expect(fs.readdirSync(testTmpDir)).toHaveLength(0);
+
+ // Restore env var
+ puppeteer.configuration.temporaryDirectory = oldTmpDir;
+ });
+ it('userDataDir option restores preferences', async () => {
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+
+ const prefsJSPath = path.join(userDataDir, 'prefs.js');
+ const prefsJSContent = 'user_pref("browser.warnOnQuit", true)';
+ await writeFile(prefsJSPath, prefsJSContent);
+
+ const {context, close} = await launch({userDataDir});
+ try {
+ // Open a page to make sure its functional.
+ await context.newPage();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ await close();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+
+ expect(await readFile(prefsJSPath, 'utf8')).toBe(prefsJSContent);
+ } finally {
+ await close();
+ }
+
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('userDataDir argument', async () => {
+ const {isChrome, defaultBrowserOptions: options} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ if (isChrome) {
+ options.args = [
+ ...(options.args || []),
+ `--user-data-dir=${userDataDir}`,
+ ];
+ } else {
+ options.args = [...(options.args || []), '-profile', userDataDir];
+ }
+ const {close} = await launch(options);
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ await close();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('userDataDir argument with non-existent dir', async () => {
+ const {isChrome, defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ rmSync(userDataDir);
+ const options = Object.assign({}, defaultBrowserOptions);
+ if (isChrome) {
+ options.args = [
+ ...(defaultBrowserOptions.args || []),
+ `--user-data-dir=${userDataDir}`,
+ ];
+ } else {
+ options.args = [
+ ...(defaultBrowserOptions.args || []),
+ '-profile',
+ userDataDir,
+ ];
+ }
+ const {close} = await launch(options);
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ await close();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('userDataDir option should restore state', async () => {
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ const {server, browser, close} = await launch({userDataDir});
+ try {
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return (localStorage['hey'] = 'hello');
+ });
+ } finally {
+ await close();
+ }
+
+ const {browser: browser2, close: close2} = await launch({userDataDir});
+
+ try {
+ const page2 = await browser2.newPage();
+ await page2.goto(server.EMPTY_PAGE);
+ expect(
+ await page2.evaluate(() => {
+ return localStorage['hey'];
+ })
+ ).toBe('hello');
+ } finally {
+ await close2();
+ }
+
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('userDataDir option should restore cookies', async () => {
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ const {server, browser, close} = await launch({userDataDir});
+ try {
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return (document.cookie =
+ 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
+ });
+ } finally {
+ await close();
+ }
+
+ const {browser: browser2, close: close2} = await launch({userDataDir});
+ try {
+ const page2 = await browser2.newPage();
+ await page2.goto(server.EMPTY_PAGE);
+ expect(
+ await page2.evaluate(() => {
+ return document.cookie;
+ })
+ ).toBe('doSomethingOnlyOnce=true');
+ } finally {
+ await close2();
+ }
+
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('should return the default arguments', async () => {
+ const {isChrome, isFirefox, puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+
+ if (isChrome) {
+ expect(puppeteer.defaultArgs()).toContain('--no-first-run');
+ expect(puppeteer.defaultArgs()).toContain('--headless');
+ expect(puppeteer.defaultArgs({headless: false})).not.toContain(
+ '--headless'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain(
+ `--user-data-dir=${path.resolve('foo')}`
+ );
+ } else if (isFirefox) {
+ expect(puppeteer.defaultArgs()).toContain('--headless');
+ expect(puppeteer.defaultArgs()).toContain('--no-remote');
+ if (os.platform() === 'darwin') {
+ expect(puppeteer.defaultArgs()).toContain('--foreground');
+ } else {
+ expect(puppeteer.defaultArgs()).not.toContain('--foreground');
+ }
+ expect(puppeteer.defaultArgs({headless: false})).not.toContain(
+ '--headless'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain(
+ '--profile'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('foo');
+ } else {
+ expect(puppeteer.defaultArgs()).toContain('-headless');
+ expect(puppeteer.defaultArgs({headless: false})).not.toContain(
+ '-headless'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain(
+ '-profile'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain(
+ path.resolve('foo')
+ );
+ }
+ });
+ it('should report the correct product', async () => {
+ const {isChrome, isFirefox, puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+ if (isChrome) {
+ expect(puppeteer.product).toBe('chrome');
+ } else if (isFirefox) {
+ expect(puppeteer.product).toBe('firefox');
+ }
+ });
+ (!isHeadless ? it : it.skip)(
+ 'should work with no default arguments',
+ async () => {
+ const {context, close} = await launch({
+ ignoreDefaultArgs: true,
+ });
+ try {
+ const page = await context.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ } finally {
+ await close();
+ }
+ }
+ );
+ it('should filter out ignored default arguments in Chrome', async () => {
+ const {defaultBrowserOptions, puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+ // Make sure we launch with `--enable-automation` by default.
+ const defaultArgs = puppeteer.defaultArgs();
+ const {browser, close} = await launch(
+ Object.assign({}, defaultBrowserOptions, {
+ // Ignore first and third default argument.
+ ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]],
+ })
+ );
+ try {
+ const spawnargs = browser.process()!.spawnargs;
+ if (!spawnargs) {
+ throw new Error('spawnargs not present');
+ }
+ expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
+ expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
+ expect(spawnargs.indexOf(defaultArgs[2]!)).toBe(-1);
+ } finally {
+ await close();
+ }
+ });
+ it('should filter out ignored default argument in Firefox', async () => {
+ const {defaultBrowserOptions, puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const defaultArgs = puppeteer.defaultArgs();
+ const {browser, close} = await launch(
+ Object.assign({}, defaultBrowserOptions, {
+ // Only the first argument is fixed, others are optional.
+ ignoreDefaultArgs: [defaultArgs[0]!],
+ })
+ );
+ try {
+ const spawnargs = browser.process()!.spawnargs;
+ if (!spawnargs) {
+ throw new Error('spawnargs not present');
+ }
+ expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
+ expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
+ } finally {
+ await close();
+ }
+ });
+ it('should have default URL when launching browser', async function () {
+ const {browser, close} = await launch(
+ {},
+ {
+ createContext: false,
+ }
+ );
+ try {
+ const pages = (await browser.pages()).map(page => {
+ return page.url();
+ });
+ expect(pages).toEqual(['about:blank']);
+ } finally {
+ await close();
+ }
+ });
+ it('should have custom URL when launching browser', async () => {
+ const {server, defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const options = Object.assign({}, defaultBrowserOptions);
+ options.args = [server.EMPTY_PAGE].concat(options.args || []);
+ const {browser, close} = await launch(options, {
+ createContext: false,
+ });
+ try {
+ const pages = await browser.pages();
+ expect(pages).toHaveLength(1);
+ const page = pages[0]!;
+ if (page.url() !== server.EMPTY_PAGE) {
+ await page.waitForNavigation();
+ }
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ } finally {
+ await close();
+ }
+ });
+ it('should pass the timeout parameter to browser.waitForTarget', async () => {
+ const options = {
+ timeout: 1,
+ };
+ let error!: Error;
+ await launch(options).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should work with timeout = 0', async () => {
+ const {close} = await launch({
+ timeout: 0,
+ });
+ await close();
+ });
+ it('should set the default viewport', async () => {
+ const {context, close} = await launch({
+ defaultViewport: {
+ width: 456,
+ height: 789,
+ },
+ });
+
+ try {
+ const page = await context.newPage();
+ expect(await page.evaluate('window.innerWidth')).toBe(456);
+ expect(await page.evaluate('window.innerHeight')).toBe(789);
+ } finally {
+ await close();
+ }
+ });
+ it('should disable the default viewport', async () => {
+ const {context, close} = await launch({
+ defaultViewport: null,
+ });
+ try {
+ const page = await context.newPage();
+ expect(page.viewport()).toBe(null);
+ } finally {
+ await close();
+ }
+ });
+ it('should take fullPage screenshots when defaultViewport is null', async () => {
+ const {server, context, close} = await launch({
+ defaultViewport: null,
+ });
+ try {
+ const page = await context.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ });
+ expect(screenshot).toBeInstanceOf(Buffer);
+ } finally {
+ await close();
+ }
+ });
+ it('should set the debugging port', async () => {
+ const {browser, close} = await launch({
+ defaultViewport: null,
+ debuggingPort: 9999,
+ });
+ try {
+ const url = new URL(browser.wsEndpoint());
+ expect(url.port).toBe('9999');
+ } finally {
+ await close();
+ }
+ });
+ it('should not allow setting debuggingPort and pipe', async () => {
+ const options = {
+ defaultViewport: null,
+ debuggingPort: 9999,
+ pipe: true,
+ };
+ let error!: Error;
+ await launch(options).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('either pipe or debugging port');
+ });
+ (!isHeadless ? it : it.skip)(
+ 'should launch Chrome properly with --no-startup-window and waitForInitialPage=false',
+ async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+ const options = {
+ waitForInitialPage: false,
+ // This is needed to prevent Puppeteer from adding an initial blank page.
+ // See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200
+ ignoreDefaultArgs: true,
+ ...defaultBrowserOptions,
+ args: ['--no-startup-window'],
+ };
+ const {browser, close} = await launch(options, {
+ createContext: false,
+ });
+ try {
+ const pages = await browser.pages();
+ expect(pages).toHaveLength(0);
+ } finally {
+ await close();
+ }
+ }
+ );
+ });
+
+ describe('Puppeteer.launch', function () {
+ it('should be able to launch Chrome', async () => {
+ const {browser, close} = await launch({product: 'chrome'});
+ try {
+ const userAgent = await browser.userAgent();
+ expect(userAgent).toContain('Chrome');
+ } finally {
+ await close();
+ }
+ });
+
+ it('should be able to launch Firefox', async function () {
+ this.timeout(FIREFOX_TIMEOUT);
+ const {browser, close} = await launch({product: 'firefox'});
+ try {
+ const userAgent = await browser.userAgent();
+ expect(userAgent).toContain('Firefox');
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Puppeteer.connect', function () {
+ it('should be able to connect multiple times to the same browser', async () => {
+ const {puppeteer, browser, close} = await launch({});
+ try {
+ const otherBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const page = await otherBrowser.newPage();
+ expect(
+ await page.evaluate(() => {
+ return 7 * 8;
+ })
+ ).toBe(56);
+ await otherBrowser.disconnect();
+
+ const secondPage = await browser.newPage();
+ expect(
+ await secondPage.evaluate(() => {
+ return 7 * 6;
+ })
+ ).toBe(42);
+ } finally {
+ await close();
+ }
+ });
+ it('should be able to close remote browser', async () => {
+ const {puppeteer, browser, close} = await launch({});
+ try {
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ await Promise.all([
+ waitEvent(browser, 'disconnected'),
+ remoteBrowser.close(),
+ ]);
+ } finally {
+ await close();
+ }
+ });
+ it('should be able to connect to a browser with no page targets', async () => {
+ const {puppeteer, browser, close} = await launch({});
+
+ try {
+ const pages = await browser.pages();
+ await Promise.all(
+ pages.map(page => {
+ return page.close();
+ })
+ );
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ await Promise.all([
+ waitEvent(browser, 'disconnected'),
+ remoteBrowser.close(),
+ ]);
+ } finally {
+ await close();
+ }
+ });
+ it('should support ignoreHTTPSErrors option', async () => {
+ const {puppeteer, httpsServer, browser, close} = await launch(
+ {},
+ {
+ createContext: false,
+ }
+ );
+
+ try {
+ const browserWSEndpoint = browser.wsEndpoint();
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ ignoreHTTPSErrors: true,
+ protocol: browser.protocol,
+ });
+ const page = await remoteBrowser.newPage();
+ const [serverRequest, response] = await Promise.all([
+ httpsServer.waitForRequest('/empty.html'),
+ page.goto(httpsServer.EMPTY_PAGE),
+ ]);
+ expect(response!.ok()).toBe(true);
+ expect(response!.securityDetails()).toBeTruthy();
+ const protocol = (serverRequest.socket as TLSSocket)
+ .getProtocol()!
+ .replace('v', ' ');
+ expect(response!.securityDetails()!.protocol()).toBe(protocol);
+ await page.close();
+ await remoteBrowser.close();
+ } finally {
+ await close();
+ }
+ });
+
+ it('should support targetFilter option in puppeteer.launch', async () => {
+ const {browser, close} = await launch(
+ {
+ targetFilter: target => {
+ return target.type() !== 'page';
+ },
+ waitForInitialPage: false,
+ },
+ {createContext: false}
+ );
+ try {
+ const targets = browser.targets();
+ expect(targets).toHaveLength(1);
+ expect(
+ targets.find(target => {
+ return target.type() === 'page';
+ })
+ ).toBeUndefined();
+ } finally {
+ await close();
+ }
+ });
+
+ // @see https://github.com/puppeteer/puppeteer/issues/4197
+ it('should support targetFilter option', async () => {
+ const {puppeteer, server, browser, close} = await launch(
+ {},
+ {
+ createContext: false,
+ }
+ );
+ try {
+ const browserWSEndpoint = browser.wsEndpoint();
+ const page1 = await browser.newPage();
+ await page1.goto(server.EMPTY_PAGE);
+
+ const page2 = await browser.newPage();
+ await page2.goto(server.EMPTY_PAGE + '?should-be-ignored');
+
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ targetFilter: target => {
+ return !target.url().includes('should-be-ignored');
+ },
+ protocol: browser.protocol,
+ });
+
+ const pages = await remoteBrowser.pages();
+
+ expect(
+ pages
+ .map((p: Page) => {
+ return p.url();
+ })
+ .sort()
+ ).toEqual(['about:blank', server.EMPTY_PAGE]);
+
+ await page2.close();
+ await page1.close();
+ await remoteBrowser.disconnect();
+ await browser.close();
+ } finally {
+ await close();
+ }
+ });
+ it('should be able to reconnect to a disconnected browser', async () => {
+ const {puppeteer, server, browser, close} = await launch({});
+ try {
+ const browserWSEndpoint = browser.wsEndpoint();
+ const page = await browser.newPage();
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ await browser.disconnect();
+
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+ const pages = await remoteBrowser.pages();
+ const restoredPage = pages.find(page => {
+ return page.url() === server.PREFIX + '/frames/nested-frames.html';
+ })!;
+ expect(dumpFrames(restoredPage.mainFrame())).toEqual([
+ 'http://localhost:<PORT>/frames/nested-frames.html',
+ ' http://localhost:<PORT>/frames/two-frames.html (2frames)',
+ ' http://localhost:<PORT>/frames/frame.html (uno)',
+ ' http://localhost:<PORT>/frames/frame.html (dos)',
+ ' http://localhost:<PORT>/frames/frame.html (aframe)',
+ ]);
+ expect(
+ await restoredPage.evaluate(() => {
+ return 7 * 8;
+ })
+ ).toBe(56);
+ await remoteBrowser.close();
+ } finally {
+ await close();
+ }
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410
+ it('should be able to connect to the same page simultaneously', async () => {
+ const {puppeteer, browser: browserOne, close} = await launch({});
+
+ try {
+ const browserTwo = await puppeteer.connect({
+ browserWSEndpoint: browserOne.wsEndpoint(),
+ protocol: browserOne.protocol,
+ });
+ const [page1, page2] = await Promise.all([
+ new Promise<Page | null>(x => {
+ return browserOne.once('targetcreated', target => {
+ x(target.page());
+ });
+ }),
+ browserTwo.newPage(),
+ ]);
+ assert(page1);
+ expect(
+ await page1.evaluate(() => {
+ return 7 * 8;
+ })
+ ).toBe(56);
+ expect(
+ await page2.evaluate(() => {
+ return 7 * 6;
+ })
+ ).toBe(42);
+ } finally {
+ await close();
+ }
+ });
+ it('should be able to reconnect', async () => {
+ const {
+ puppeteer,
+ server,
+ browser: browserOne,
+ close,
+ } = await launch({});
+ try {
+ const browserWSEndpoint = browserOne.wsEndpoint();
+ const pageOne = await browserOne.newPage();
+ await pageOne.goto(server.EMPTY_PAGE);
+ await browserOne.disconnect();
+
+ const browserTwo = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browserOne.protocol,
+ });
+ const pages = await browserTwo.pages();
+ const pageTwo = pages.find(page => {
+ return page.url() === server.EMPTY_PAGE;
+ })!;
+ await pageTwo.reload();
+ using _ = await pageTwo.waitForSelector('body', {
+ timeout: 10000,
+ });
+ await browserTwo.close();
+ } finally {
+ await close();
+ }
+ });
+ });
+ describe('Puppeteer.executablePath', function () {
+ it('should work', async () => {
+ const {puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const executablePath = puppeteer.executablePath();
+ expect(fs.existsSync(executablePath)).toBe(true);
+ expect(fs.realpathSync(executablePath)).toBe(executablePath);
+ });
+ it('returns executablePath for channel', async () => {
+ const {puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const executablePath = puppeteer.executablePath('chrome');
+ expect(executablePath).toBeTruthy();
+ });
+ describe('when executable path is configured', () => {
+ const sandbox = sinon.createSandbox();
+
+ beforeEach(async () => {
+ const {puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+ sandbox
+ .stub(puppeteer.configuration, 'executablePath')
+ .value('SOME_CUSTOM_EXECUTABLE');
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it('its value is used', async () => {
+ const {puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+ try {
+ puppeteer.executablePath();
+ } catch (error) {
+ expect((error as Error).message).toContain(
+ 'SOME_CUSTOM_EXECUTABLE'
+ );
+ }
+ });
+ });
+ });
+ });
+
+ describe('Browser target events', function () {
+ it('should work', async () => {
+ const {browser, server, close} = await launch({});
+
+ try {
+ const events: string[] = [];
+ browser.on('targetcreated', () => {
+ events.push('CREATED');
+ });
+ browser.on('targetchanged', () => {
+ events.push('CHANGED');
+ });
+ browser.on('targetdestroyed', () => {
+ events.push('DESTROYED');
+ });
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.close();
+ expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']);
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Browser.Events.disconnected', function () {
+ it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => {
+ const {puppeteer, browser, close} = await launch({});
+ try {
+ const browserWSEndpoint = browser.wsEndpoint();
+ const remoteBrowser1 = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+ const remoteBrowser2 = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+
+ let disconnectedOriginal = 0;
+ let disconnectedRemote1 = 0;
+ let disconnectedRemote2 = 0;
+ browser.on('disconnected', () => {
+ ++disconnectedOriginal;
+ });
+ remoteBrowser1.on('disconnected', () => {
+ ++disconnectedRemote1;
+ });
+ remoteBrowser2.on('disconnected', () => {
+ ++disconnectedRemote2;
+ });
+
+ await Promise.all([
+ waitEvent(remoteBrowser2, 'disconnected'),
+ remoteBrowser2.disconnect(),
+ ]);
+
+ expect(disconnectedOriginal).toBe(0);
+ expect(disconnectedRemote1).toBe(0);
+ expect(disconnectedRemote2).toBe(1);
+
+ await Promise.all([
+ waitEvent(remoteBrowser1, 'disconnected'),
+ waitEvent(browser, 'disconnected'),
+ browser.close(),
+ ]);
+
+ expect(disconnectedOriginal).toBe(1);
+ expect(disconnectedRemote1).toBe(1);
+ expect(disconnectedRemote2).toBe(1);
+ } finally {
+ await close();
+ }
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/locator.spec.ts b/remote/test/puppeteer/test/src/locator.spec.ts
new file mode 100644
index 0000000000..9b00cc2d7c
--- /dev/null
+++ b/remote/test/puppeteer/test/src/locator.spec.ts
@@ -0,0 +1,763 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer-core';
+import {
+ Locator,
+ LocatorEvent,
+} from 'puppeteer-core/internal/api/locators/locators.js';
+import sinon from 'sinon';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Locator', function () {
+ setupTestBrowserHooks();
+
+ it('should work with a frame', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="this.innerText = 'clicked';">test</button>
+ `);
+ let willClick = false;
+ await page
+ .mainFrame()
+ .locator('button')
+ .on(LocatorEvent.Action, () => {
+ willClick = true;
+ })
+ .click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(willClick).toBe(true);
+ });
+
+ it('should work without preconditions', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="this.innerText = 'clicked';">test</button>
+ `);
+ let willClick = false;
+ await page
+ .locator('button')
+ .setEnsureElementIsInTheViewport(false)
+ .setTimeout(0)
+ .setVisibility(null)
+ .setWaitForEnabled(false)
+ .setWaitForStableBoundingBox(false)
+ .on(LocatorEvent.Action, () => {
+ willClick = true;
+ })
+ .click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(willClick).toBe(true);
+ });
+
+ describe('Locator.click', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="this.innerText = 'clicked';">test</button>
+ `);
+ let willClick = false;
+ await page
+ .locator('button')
+ .on(LocatorEvent.Action, () => {
+ willClick = true;
+ })
+ .click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(willClick).toBe(true);
+ });
+
+ it('should work for multiple selectors', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="this.innerText = 'clicked';">test</button>
+ `);
+ let clicked = false;
+ await page
+ .locator('::-p-text(test), ::-p-xpath(/button)')
+ .on(LocatorEvent.Action, () => {
+ clicked = true;
+ })
+ .click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(clicked).toBe(true);
+ });
+
+ it('should work if the element is out of viewport', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="margin-top: 600px;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ await page.locator('button').click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ });
+
+ it('should work if the element becomes visible later', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ using button = await page.$('button');
+ const result = page
+ .locator('button')
+ .click()
+ .catch(err => {
+ return err;
+ });
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('test');
+ await button?.evaluate(el => {
+ el.style.display = 'block';
+ });
+ const maybeError = await result;
+ if (maybeError instanceof Error) {
+ throw maybeError;
+ }
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('clicked');
+ });
+
+ it('should work if the element becomes enabled later', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button disabled onclick="this.innerText = 'clicked';">test</button>
+ `);
+ using button = await page.$('button');
+ const result = page.locator('button').click();
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('test');
+ await button?.evaluate(el => {
+ el.disabled = false;
+ });
+ await result;
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('clicked');
+ });
+
+ it('should work if multiple conditions are satisfied later', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="margin-top: 600px;" style="display: none;" disabled onclick="this.innerText = 'clicked';">test</button>
+ `);
+ using button = await page.$('button');
+ const result = page.locator('button').click();
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('test');
+ await button?.evaluate(el => {
+ el.disabled = false;
+ el.style.display = 'block';
+ });
+ await result;
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('clicked');
+ });
+
+ it('should time out', async () => {
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ const {page} = await getTestState();
+
+ page.setDefaultTimeout(5000);
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ const result = page.locator('button').click();
+ clock.tick(5100);
+ await expect(result).rejects.toEqual(
+ new TimeoutError('Timed out after waiting 5000ms')
+ );
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('should retry clicks on errors', async () => {
+ const {page} = await getTestState();
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ page.setDefaultTimeout(5000);
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ const result = page.locator('button').click();
+ clock.tick(5100);
+ await expect(result).rejects.toEqual(
+ new TimeoutError('Timed out after waiting 5000ms')
+ );
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('can be aborted', async () => {
+ const {page} = await getTestState();
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ page.setDefaultTimeout(5000);
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ const abortController = new AbortController();
+ const result = page.locator('button').click({
+ signal: abortController.signal,
+ });
+ clock.tick(2000);
+ abortController.abort();
+ await expect(result).rejects.toThrow(/aborted/);
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('should work with a OOPIF', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <iframe src="data:text/html,<button onclick=&quot;this.innerText = 'clicked';&quot;>test</button>"></iframe>
+ `);
+ const frame = await page.waitForFrame(frame => {
+ return frame.url().startsWith('data');
+ });
+ let willClick = false;
+ await frame
+ .locator('button')
+ .on(LocatorEvent.Action, () => {
+ willClick = true;
+ })
+ .click();
+ using button = await frame.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(willClick).toBe(true);
+ });
+ });
+
+ describe('Locator.hover', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onmouseenter="this.innerText = 'hovered';">test</button>
+ `);
+ let hovered = false;
+ await page
+ .locator('button')
+ .on(LocatorEvent.Action, () => {
+ hovered = true;
+ })
+ .hover();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('hovered');
+ expect(hovered).toBe(true);
+ });
+ });
+
+ describe('Locator.scroll', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <div style="height: 500px; width: 500px; overflow: scroll;">
+ <div style="height: 1000px; width: 1000px;">test</div>
+ </div>
+ `);
+ let scrolled = false;
+ await page
+ .locator('div')
+ .on(LocatorEvent.Action, () => {
+ scrolled = true;
+ })
+ .scroll({
+ scrollTop: 500,
+ scrollLeft: 500,
+ });
+ using scrollable = await page.$('div');
+ const scroll = await scrollable?.evaluate(el => {
+ return el.scrollTop + ' ' + el.scrollLeft;
+ });
+ expect(scroll).toBe('500 500');
+ expect(scrolled).toBe(true);
+ });
+ });
+
+ describe('Locator.fill', function () {
+ it('should work for textarea', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <textarea></textarea>
+ `);
+ let filled = false;
+ await page
+ .locator('textarea')
+ .on(LocatorEvent.Action, () => {
+ filled = true;
+ })
+ .fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')?.value === 'test';
+ })
+ ).toBe(true);
+ expect(filled).toBe(true);
+ });
+
+ it('should work for selects', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <select>
+ <option value="value1">Option 1</option>
+ <option value="value2">Option 2</option>
+ <select>
+ `);
+ let filled = false;
+ await page
+ .locator('select')
+ .on(LocatorEvent.Action, () => {
+ filled = true;
+ })
+ .fill('value2');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('select')?.value === 'value2';
+ })
+ ).toBe(true);
+ expect(filled).toBe(true);
+ });
+
+ it('should work for inputs', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <input>
+ `);
+ await page.locator('input').fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('input')?.value === 'test';
+ })
+ ).toBe(true);
+ });
+
+ it('should work if the input becomes enabled later', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <input disabled>
+ `);
+ using input = await page.$('input');
+ const result = page.locator('input').fill('test');
+ expect(
+ await input?.evaluate(el => {
+ return el.value;
+ })
+ ).toBe('');
+ await input?.evaluate(el => {
+ el.disabled = false;
+ });
+ await result;
+ expect(
+ await input?.evaluate(el => {
+ return el.value;
+ })
+ ).toBe('test');
+ });
+
+ it('should work for contenteditable', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <div contenteditable="true">
+ `);
+ await page.locator('div').fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('div')?.innerText === 'test';
+ })
+ ).toBe(true);
+ });
+
+ it('should work for pre-filled inputs', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <input value="te">
+ `);
+ await page.locator('input').fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('input')?.value === 'test';
+ })
+ ).toBe(true);
+ });
+
+ it('should override pre-filled inputs', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <input value="wrong prefix">
+ `);
+ await page.locator('input').fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('input')?.value === 'test';
+ })
+ ).toBe(true);
+ });
+
+ it('should work for non-text inputs', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <input type="color">
+ `);
+ await page.locator('input').fill('#333333');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('input')?.value === '#333333';
+ })
+ ).toBe(true);
+ });
+ });
+
+ describe('Locator.race', () => {
+ it('races multiple locators', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="window.count++;">test</button>
+ `);
+ await page.evaluate(() => {
+ // @ts-expect-error different context.
+ window.count = 0;
+ });
+ await Locator.race([
+ page.locator('button'),
+ page.locator('button'),
+ ]).click();
+ const count = await page.evaluate(() => {
+ // @ts-expect-error different context.
+ return globalThis.count;
+ });
+ expect(count).toBe(1);
+ });
+
+ it('can be aborted', async () => {
+ const {page} = await getTestState();
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ const abortController = new AbortController();
+ const result = Locator.race([
+ page.locator('button'),
+ page.locator('button'),
+ ])
+ .setTimeout(5000)
+ .click({
+ signal: abortController.signal,
+ });
+ clock.tick(2000);
+ abortController.abort();
+ await expect(result).rejects.toThrow(/aborted/);
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('should time out when all locators do not match', async () => {
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ const {page} = await getTestState();
+ await page.setContent(`<button>test</button>`);
+ const result = Locator.race([
+ page.locator('not-found'),
+ page.locator('not-found'),
+ ])
+ .setTimeout(5000)
+ .click();
+ clock.tick(5100);
+ await expect(result).rejects.toEqual(
+ new TimeoutError('Timed out after waiting 5000ms')
+ );
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('should not time out when one of the locators matches', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<button>test</button>`);
+ const result = Locator.race([
+ page.locator('not-found'),
+ page.locator('button'),
+ ]).click();
+ await expect(result).resolves.toEqual(undefined);
+ });
+ });
+
+ describe('Locator.prototype.map', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<div>test</div>`);
+ await expect(
+ page
+ .locator('::-p-text(test)')
+ .map(element => {
+ return element.getAttribute('clickable');
+ })
+ .wait()
+ ).resolves.toEqual(null);
+ await page.evaluate(() => {
+ document.querySelector('div')?.setAttribute('clickable', 'true');
+ });
+ await expect(
+ page
+ .locator('::-p-text(test)')
+ .map(element => {
+ return element.getAttribute('clickable');
+ })
+ .wait()
+ ).resolves.toEqual('true');
+ });
+ it('should work with throws', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<div>test</div>`);
+ const result = page
+ .locator('::-p-text(test)')
+ .map(element => {
+ const clickable = element.getAttribute('clickable');
+ if (!clickable) {
+ throw new Error('Missing `clickable` as an attribute');
+ }
+ return clickable;
+ })
+ .wait();
+ await page.evaluate(() => {
+ document.querySelector('div')?.setAttribute('clickable', 'true');
+ });
+ await expect(result).resolves.toEqual('true');
+ });
+ it('should work with expect', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<div>test</div>`);
+ const result = page
+ .locator('::-p-text(test)')
+ .filter(element => {
+ return element.getAttribute('clickable') !== null;
+ })
+ .map(element => {
+ return element.getAttribute('clickable');
+ })
+ .wait();
+ await page.evaluate(() => {
+ document.querySelector('div')?.setAttribute('clickable', 'true');
+ });
+ await expect(result).resolves.toEqual('true');
+ });
+ });
+
+ describe('Locator.prototype.filter', () => {
+ it('should resolve as soon as the predicate matches', async () => {
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ const {page} = await getTestState();
+ await page.setContent(`<div>test</div>`);
+ const result = page
+ .locator('::-p-text(test)')
+ .setTimeout(5000)
+ .filter(async element => {
+ return element.getAttribute('clickable') === 'true';
+ })
+ .filter(element => {
+ return element.getAttribute('clickable') === 'true';
+ })
+ .hover();
+ clock.tick(2000);
+ await page.evaluate(() => {
+ document.querySelector('div')?.setAttribute('clickable', 'true');
+ });
+ clock.restore();
+ await expect(result).resolves.toEqual(undefined);
+ } finally {
+ clock.restore();
+ }
+ });
+ });
+
+ describe('Locator.prototype.wait', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ void page.setContent(`
+ <script>
+ setTimeout(() => {
+ const element = document.createElement("div");
+ element.innerText = "test2"
+ document.body.append(element);
+ }, 50);
+ </script>
+ `);
+ // This shouldn't throw.
+ await page.locator('div').wait();
+ });
+ });
+
+ describe('Locator.prototype.waitHandle', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ void page.setContent(`
+ <script>
+ setTimeout(() => {
+ const element = document.createElement("div");
+ element.innerText = "test2"
+ document.body.append(element);
+ }, 50);
+ </script>
+ `);
+ await expect(page.locator('div').waitHandle()).resolves.toBeDefined();
+ });
+ });
+
+ describe('Locator.prototype.clone', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ const locator = page.locator('div');
+ const clone = locator.clone();
+ expect(locator).not.toStrictEqual(clone);
+ });
+ it('should work internally with delegated locators', async () => {
+ const {page} = await getTestState();
+ const locator = page.locator('div');
+ const delegatedLocators = [
+ locator.map(div => {
+ return div.textContent;
+ }),
+ locator.filter(div => {
+ return div.textContent?.length === 0;
+ }),
+ ];
+ for (let delegatedLocator of delegatedLocators) {
+ delegatedLocator = delegatedLocator.setTimeout(500);
+ expect(delegatedLocator.timeout).not.toStrictEqual(locator.timeout);
+ }
+ });
+ });
+
+ describe('FunctionLocator', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ const result = page
+ .locator(() => {
+ return new Promise<boolean>(resolve => {
+ return setTimeout(() => {
+ return resolve(true);
+ }, 100);
+ });
+ })
+ .wait();
+ await expect(result).resolves.toEqual(true);
+ });
+ it('should work with actions', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<div onclick="window.clicked = true">test</div>`);
+ await page
+ .locator(() => {
+ return document.getElementsByTagName('div')[0] as HTMLDivElement;
+ })
+ .click();
+ await expect(
+ page.evaluate(() => {
+ return (window as unknown as {clicked: boolean}).clicked;
+ })
+ ).resolves.toEqual(true);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/mocha-utils.ts b/remote/test/puppeteer/test/src/mocha-utils.ts
new file mode 100644
index 0000000000..3fff9c9930
--- /dev/null
+++ b/remote/test/puppeteer/test/src/mocha-utils.ts
@@ -0,0 +1,507 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import {TestServer} from '@pptr/testserver';
+import type {Protocol} from 'devtools-protocol';
+import expect from 'expect';
+import type * as MochaBase from 'mocha';
+import puppeteer from 'puppeteer/lib/cjs/puppeteer/puppeteer.js';
+import type {Browser} from 'puppeteer-core/internal/api/Browser.js';
+import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+import type {
+ PuppeteerLaunchOptions,
+ PuppeteerNode,
+} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+import {rmSync} from 'puppeteer-core/internal/node/util/fs.js';
+import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
+import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js';
+import sinon from 'sinon';
+
+import {extendExpectWithToBeGolden} from './utils.js';
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Mocha {
+ export interface SuiteFunction {
+ /**
+ * Use it if you want to capture debug logs for a specitic test suite in CI.
+ * This describe function enables capturing of debug logs and would print them
+ * only if a test fails to reduce the amount of output.
+ */
+ withDebugLogs: (
+ description: string,
+ body: (this: MochaBase.Suite) => void
+ ) => void;
+ }
+ export interface TestFunction {
+ /*
+ * Use to rerun the test and capture logs for the failed attempts
+ * that way we don't push all the logs making it easier to read.
+ */
+ deflake: (
+ repeats: number,
+ title: string,
+ fn: MochaBase.AsyncFunc
+ ) => void;
+ /*
+ * Use to rerun a single test and capture logs for the failed attempts
+ */
+ deflakeOnly: (
+ repeats: number,
+ title: string,
+ fn: MochaBase.AsyncFunc
+ ) => void;
+ }
+ }
+}
+
+const product =
+ process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome';
+
+const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase() as
+ | 'true'
+ | 'false'
+ | 'new';
+export const isHeadless = headless === 'true' || headless === 'new';
+const isFirefox = product === 'firefox';
+const isChrome = product === 'chrome';
+const protocol = (process.env['PUPPETEER_PROTOCOL'] || 'cdp') as
+ | 'cdp'
+ | 'webDriverBiDi';
+
+let extraLaunchOptions = {};
+try {
+ extraLaunchOptions = JSON.parse(process.env['EXTRA_LAUNCH_OPTIONS'] || '{}');
+} catch (error) {
+ if (isErrorLike(error)) {
+ console.warn(
+ `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.`
+ );
+ } else {
+ throw error;
+ }
+}
+
+const defaultBrowserOptions = Object.assign(
+ {
+ handleSIGINT: true,
+ executablePath: process.env['BINARY'],
+ headless: headless === 'new' ? ('new' as const) : isHeadless,
+ dumpio: !!process.env['DUMPIO'],
+ protocol,
+ },
+ extraLaunchOptions
+);
+
+if (defaultBrowserOptions.executablePath) {
+ console.warn(
+ `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}`
+ );
+} else {
+ const executablePath = puppeteer.executablePath();
+ if (!fs.existsSync(executablePath)) {
+ throw new Error(
+ `Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests`
+ );
+ }
+}
+
+const processVariables: {
+ product: string;
+ headless: 'true' | 'false' | 'new';
+ isHeadless: boolean;
+ isFirefox: boolean;
+ isChrome: boolean;
+ protocol: 'cdp' | 'webDriverBiDi';
+ defaultBrowserOptions: PuppeteerLaunchOptions;
+} = {
+ product,
+ headless,
+ isHeadless,
+ isFirefox,
+ isChrome,
+ protocol,
+ defaultBrowserOptions,
+};
+
+const setupServer = async () => {
+ const assetsPath = path.join(__dirname, '../assets');
+ const cachedPath = path.join(__dirname, '../assets', 'cached');
+
+ const server = await TestServer.create(assetsPath);
+ const port = server.port;
+ server.enableHTTPCache(cachedPath);
+ server.PORT = port;
+ server.PREFIX = `http://localhost:${port}`;
+ server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`;
+ server.EMPTY_PAGE = `http://localhost:${port}/empty.html`;
+
+ const httpsServer = await TestServer.createHTTPS(assetsPath);
+ const httpsPort = httpsServer.port;
+ httpsServer.enableHTTPCache(cachedPath);
+ httpsServer.PORT = httpsPort;
+ httpsServer.PREFIX = `https://localhost:${httpsPort}`;
+ httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`;
+ httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`;
+
+ return {server, httpsServer};
+};
+
+export const setupTestBrowserHooks = (): void => {
+ before(async function () {
+ try {
+ if (!state.browser) {
+ state.browser = await puppeteer.launch({
+ ...processVariables.defaultBrowserOptions,
+ timeout: this.timeout() - 1_000,
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ // Intentionally empty as `getTestState` will throw
+ // if browser is not found
+ }
+ });
+
+ after(() => {
+ if (typeof gc !== 'undefined') {
+ gc();
+ const memory = process.memoryUsage();
+ console.log('Memory stats:');
+ for (const key of Object.keys(memory)) {
+ console.log(
+ key,
+ // @ts-expect-error TS cannot the key type.
+ `${Math.round(((memory[key] / 1024 / 1024) * 100) / 100)} MB`
+ );
+ }
+ }
+ });
+};
+
+export const getTestState = async (
+ options: {
+ skipLaunch?: boolean;
+ skipContextCreation?: boolean;
+ } = {}
+): Promise<PuppeteerTestState> => {
+ const {skipLaunch = false, skipContextCreation = false} = options;
+
+ state.defaultBrowserOptions = JSON.parse(
+ JSON.stringify(processVariables.defaultBrowserOptions)
+ );
+
+ state.server?.reset();
+ state.httpsServer?.reset();
+
+ if (skipLaunch) {
+ return state as PuppeteerTestState;
+ }
+
+ if (!state.browser) {
+ throw new Error('Browser was not set-up in time!');
+ }
+
+ if (state.context) {
+ await state.context.close();
+ state.context = undefined;
+ state.page = undefined;
+ }
+
+ if (!skipContextCreation) {
+ state.context = await state.browser!.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ }
+ return state as PuppeteerTestState;
+};
+
+const setupGoldenAssertions = (): void => {
+ const suffix = processVariables.product.toLowerCase();
+ const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`);
+ const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`);
+ if (fs.existsSync(OUTPUT_DIR)) {
+ rmSync(OUTPUT_DIR);
+ }
+ extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR);
+};
+
+setupGoldenAssertions();
+
+export interface PuppeteerTestState {
+ browser: Browser;
+ context: BrowserContext;
+ page: Page;
+ puppeteer: PuppeteerNode;
+ defaultBrowserOptions: PuppeteerLaunchOptions;
+ server: TestServer;
+ httpsServer: TestServer;
+ isFirefox: boolean;
+ isChrome: boolean;
+ isHeadless: boolean;
+ headless: 'true' | 'false' | 'new';
+ puppeteerPath: string;
+}
+const state: Partial<PuppeteerTestState> = {};
+
+if (
+ process.env['MOCHA_WORKER_ID'] === undefined ||
+ process.env['MOCHA_WORKER_ID'] === '0'
+) {
+ console.log(
+ `Running unit tests with:
+ -> product: ${processVariables.product}
+ -> binary: ${
+ processVariables.defaultBrowserOptions.executablePath ||
+ path.relative(process.cwd(), puppeteer.executablePath())
+ }
+ -> mode: ${
+ processVariables.isHeadless
+ ? processVariables.headless === 'new'
+ ? '--headless=new'
+ : '--headless'
+ : 'headful'
+ }`
+ );
+}
+
+const browserNotClosedError = new Error(
+ 'A manually launched browser was not closed!'
+);
+
+export const mochaHooks = {
+ async beforeAll(): Promise<void> {
+ async function setUpDefaultState() {
+ const {server, httpsServer} = await setupServer();
+
+ state.puppeteer = puppeteer;
+ state.server = server;
+ state.httpsServer = httpsServer;
+ state.isFirefox = processVariables.isFirefox;
+ state.isChrome = processVariables.isChrome;
+ state.isHeadless = processVariables.isHeadless;
+ state.headless = processVariables.headless;
+ state.puppeteerPath = path.resolve(
+ path.join(__dirname, '..', '..', 'packages', 'puppeteer')
+ );
+ }
+
+ try {
+ await Deferred.race([
+ setUpDefaultState(),
+ Deferred.create({
+ message: `Failed in after Hook`,
+ timeout: (this as any).timeout() - 1000,
+ }),
+ ]);
+ } catch {}
+ },
+
+ async afterAll(): Promise<void> {
+ (this as any).timeout(0);
+ const lastTestFile = (this as any)?.test?.parent?.suites?.[0]?.file
+ ?.split('/')
+ ?.at(-1);
+ try {
+ await Promise.all([
+ state.server?.stop(),
+ state.httpsServer?.stop(),
+ state.browser?.close(),
+ ]);
+ } catch (error) {
+ throw new Error(
+ `Closing defaults (HTTP TestServer, HTTPS TestServer, Browser ) failed in ${lastTestFile}}`
+ );
+ }
+ if (browserCleanupsAfterAll.length > 0) {
+ await closeLaunched(browserCleanupsAfterAll)();
+ throw new Error(`Browser was not closed in ${lastTestFile}`);
+ }
+ },
+
+ async afterEach(): Promise<void> {
+ if (browserCleanups.length > 0) {
+ (this as any).test.error(browserNotClosedError);
+ await Deferred.race([
+ closeLaunched(browserCleanups)(),
+ Deferred.create({
+ message: `Failed in after Hook`,
+ timeout: (this as any).timeout() - 1000,
+ }),
+ ]);
+ }
+ sinon.restore();
+ },
+};
+
+declare module 'expect' {
+ interface Matchers<R> {
+ atLeastOneToContain(expected: string[]): R;
+ }
+}
+
+expect.extend({
+ atLeastOneToContain: (actual: string, expected: string[]) => {
+ for (const test of expected) {
+ try {
+ expect(actual).toContain(test);
+ return {
+ pass: true,
+ message: () => {
+ return '';
+ },
+ };
+ } catch (err) {}
+ }
+
+ return {
+ pass: false,
+ message: () => {
+ return `"${actual}" didn't contain any of the strings ${JSON.stringify(
+ expected
+ )}`;
+ },
+ };
+ },
+});
+
+export const expectCookieEquals = async (
+ cookies: Protocol.Network.Cookie[],
+ expectedCookies: Array<Partial<Protocol.Network.Cookie>>
+): Promise<void> => {
+ if (!processVariables.isChrome) {
+ // Only keep standard properties when testing on a browser other than Chrome.
+ expectedCookies = expectedCookies.map(cookie => {
+ return {
+ domain: cookie.domain,
+ expires: cookie.expires,
+ httpOnly: cookie.httpOnly,
+ name: cookie.name,
+ path: cookie.path,
+ secure: cookie.secure,
+ session: cookie.session,
+ size: cookie.size,
+ value: cookie.value,
+ };
+ });
+ }
+
+ expect(cookies).toHaveLength(expectedCookies.length);
+ for (let i = 0; i < cookies.length; i++) {
+ expect(cookies[i]).toMatchObject(expectedCookies[i]!);
+ }
+};
+
+export const shortWaitForArrayToHaveAtLeastNElements = async (
+ data: unknown[],
+ minLength: number,
+ attempts = 3,
+ timeout = 50
+): Promise<void> => {
+ for (let i = 0; i < attempts; i++) {
+ if (data.length >= minLength) {
+ break;
+ }
+ await new Promise(resolve => {
+ return setTimeout(resolve, timeout);
+ });
+ }
+};
+
+export const createTimeout = <T>(
+ n: number,
+ value?: T
+): Promise<T | undefined> => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ return resolve(value);
+ }, n);
+ });
+};
+
+const browserCleanupsAfterAll: Array<() => Promise<void>> = [];
+const browserCleanups: Array<() => Promise<void>> = [];
+
+const closeLaunched = (storage: Array<() => Promise<void>>) => {
+ return async () => {
+ let cleanup = storage.pop();
+ try {
+ while (cleanup) {
+ await cleanup();
+ cleanup = storage.pop();
+ }
+ } catch (error) {
+ // If the browser was closed by other means, swallow the error
+ // and mark the browser as closed.
+ if ((error as Error)?.message.includes('Connection closed')) {
+ storage.splice(0, storage.length);
+ return;
+ }
+
+ throw error;
+ }
+ };
+};
+
+export const launch = async (
+ launchOptions: Readonly<PuppeteerLaunchOptions>,
+ options: {
+ after?: 'each' | 'all';
+ createContext?: boolean;
+ createPage?: boolean;
+ } = {}
+): Promise<
+ PuppeteerTestState & {
+ close: () => Promise<void>;
+ }
+> => {
+ const {after = 'each', createContext = true, createPage = true} = options;
+ const initState = await getTestState({
+ skipLaunch: true,
+ });
+ const cleanupStorage =
+ after === 'each' ? browserCleanups : browserCleanupsAfterAll;
+ try {
+ const browser = await puppeteer.launch({
+ ...initState.defaultBrowserOptions,
+ ...launchOptions,
+ });
+ cleanupStorage.push(() => {
+ return browser.close();
+ });
+
+ let context: BrowserContext;
+ let page: Page;
+ if (createContext) {
+ context = await browser.createIncognitoBrowserContext();
+ cleanupStorage.push(() => {
+ return context.close();
+ });
+
+ if (createPage) {
+ page = await context.newPage();
+ cleanupStorage.push(() => {
+ return page.close();
+ });
+ }
+ }
+
+ return {
+ ...initState,
+ browser,
+ context: context!,
+ page: page!,
+ close: closeLaunched(cleanupStorage),
+ };
+ } catch (error) {
+ await closeLaunched(cleanupStorage)();
+
+ throw error;
+ }
+};
diff --git a/remote/test/puppeteer/test/src/mouse.spec.ts b/remote/test/puppeteer/test/src/mouse.spec.ts
new file mode 100644
index 0000000000..69229eb147
--- /dev/null
+++ b/remote/test/puppeteer/test/src/mouse.spec.ts
@@ -0,0 +1,472 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import os from 'os';
+
+import expect from 'expect';
+import {MouseButton} from 'puppeteer-core/internal/api/Input.js';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+import type {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+interface ClickData {
+ type: string;
+ detail: number;
+ clientX: number;
+ clientY: number;
+ isTrusted: boolean;
+ button: number;
+ buttons: number;
+}
+
+interface Dimensions {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+
+function dimensions(): Dimensions {
+ const rect = document.querySelector('textarea')!.getBoundingClientRect();
+ return {
+ x: rect.left,
+ y: rect.top,
+ width: rect.width,
+ height: rect.height,
+ };
+}
+
+describe('Mouse', function () {
+ setupTestBrowserHooks();
+
+ it('should click the document', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ (globalThis as any).clickPromise = new Promise(resolve => {
+ document.addEventListener('click', event => {
+ resolve({
+ type: event.type,
+ detail: event.detail,
+ clientX: event.clientX,
+ clientY: event.clientY,
+ isTrusted: event.isTrusted,
+ button: event.button,
+ });
+ });
+ });
+ });
+ await page.mouse.click(50, 60);
+ const event = await page.evaluate(() => {
+ return (globalThis as any).clickPromise;
+ });
+ expect(event.type).toBe('click');
+ expect(event.detail).toBe(1);
+ expect(event.clientX).toBe(50);
+ expect(event.clientY).toBe(60);
+ expect(event.isTrusted).toBe(true);
+ expect(event.button).toBe(0);
+ });
+ it('should resize the textarea', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ const {x, y, width, height} = await page.evaluate(dimensions);
+ const mouse = page.mouse;
+ await mouse.move(x + width - 4, y + height - 4);
+ await mouse.down();
+ await mouse.move(x + width + 100, y + height + 100);
+ await mouse.up();
+ const newDimensions = await page.evaluate(dimensions);
+ expect(newDimensions.width).toBe(Math.round(width + 104));
+ expect(newDimensions.height).toBe(Math.round(height + 104));
+ });
+ it('should select the text with mouse', async () => {
+ const {page, server} = await getTestState();
+
+ const text =
+ "This is the text that we are going to try to select. Let's see how it goes.";
+
+ await page.goto(`${server.PREFIX}/input/textarea.html`);
+ await page.focus('textarea');
+ await page.keyboard.type(text);
+ using handle = await page
+ .locator('textarea')
+ .filterHandle(async element => {
+ return await element.evaluate((element, text) => {
+ return element.value === text;
+ }, text);
+ })
+ .waitHandle();
+ const {x, y} = await page.evaluate(dimensions);
+ await page.mouse.move(x + 2, y + 2);
+ await page.mouse.down();
+ await page.mouse.move(100, 100);
+ await page.mouse.up();
+ expect(
+ await handle.evaluate(element => {
+ return element.value.substring(
+ element.selectionStart,
+ element.selectionEnd
+ );
+ })
+ ).toBe(text);
+ });
+ it('should trigger hover state', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.hover('#button-6');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-6');
+ await page.hover('#button-2');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-2');
+ await page.hover('#button-91');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-91');
+ });
+ it('should trigger hover state with removed window.Node', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.evaluate(() => {
+ // @ts-expect-error Expected.
+ return delete window.Node;
+ });
+ await page.hover('#button-6');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-6');
+ });
+ it('should set modifier keys on click', async () => {
+ const {page, server, isFirefox} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.evaluate(() => {
+ return document.querySelector('#button-3')!.addEventListener(
+ 'mousedown',
+ e => {
+ return ((globalThis as any).lastEvent = e);
+ },
+ true
+ );
+ });
+ const modifiers = new Map<KeyInput, string>([
+ ['Shift', 'shiftKey'],
+ ['Control', 'ctrlKey'],
+ ['Alt', 'altKey'],
+ ['Meta', 'metaKey'],
+ ]);
+ // In Firefox, the Meta modifier only exists on Mac
+ if (isFirefox && os.platform() !== 'darwin') {
+ modifiers.delete('Meta');
+ }
+ for (const [modifier, key] of modifiers) {
+ await page.keyboard.down(modifier);
+ await page.click('#button-3');
+ if (
+ !(await page.evaluate((mod: string) => {
+ return (globalThis as any).lastEvent[mod];
+ }, key))
+ ) {
+ throw new Error(key + ' should be true');
+ }
+ await page.keyboard.up(modifier);
+ }
+ await page.click('#button-3');
+ for (const [modifier, key] of modifiers) {
+ if (
+ await page.evaluate((mod: string) => {
+ return (globalThis as any).lastEvent[mod];
+ }, key)
+ ) {
+ throw new Error(modifiers.get(modifier) + ' should be false');
+ }
+ }
+ });
+ it('should send mouse wheel events', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/wheel.html');
+ using elem = (await page.$('div'))!;
+ const boundingBoxBefore = (await elem.boundingBox())!;
+ expect(boundingBoxBefore).toMatchObject({
+ width: 115,
+ height: 115,
+ });
+
+ await page.mouse.move(
+ boundingBoxBefore.x + boundingBoxBefore.width / 2,
+ boundingBoxBefore.y + boundingBoxBefore.height / 2
+ );
+
+ await page.mouse.wheel({deltaY: -100});
+ const boundingBoxAfter = await elem.boundingBox();
+ expect(boundingBoxAfter).toMatchObject({
+ width: 230,
+ height: 230,
+ });
+ });
+ it('should tween mouse movement', async () => {
+ const {page} = await getTestState();
+
+ await page.mouse.move(100, 100);
+ await page.evaluate(() => {
+ (globalThis as any).result = [];
+ document.addEventListener('mousemove', event => {
+ (globalThis as any).result.push([event.clientX, event.clientY]);
+ });
+ });
+ await page.mouse.move(200, 300, {steps: 5});
+ expect(await page.evaluate('result')).toEqual([
+ [120, 140],
+ [140, 180],
+ [160, 220],
+ [180, 260],
+ [200, 300],
+ ]);
+ });
+ // @see https://crbug.com/929806
+ it('should work with mobile viewports and cross process navigations', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setViewport({width: 360, height: 640, isMobile: true});
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html');
+ await page.evaluate(() => {
+ document.addEventListener('click', event => {
+ (globalThis as any).result = {x: event.clientX, y: event.clientY};
+ });
+ });
+
+ await page.mouse.click(30, 40);
+
+ expect(await page.evaluate('result')).toEqual({x: 30, y: 40});
+ });
+ it('should not throw if buttons are pressed twice', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.mouse.down();
+ await page.mouse.down();
+ });
+
+ interface AddMouseDataListenersOptions {
+ includeMove?: boolean;
+ }
+
+ const addMouseDataListeners = (
+ page: Page,
+ options: AddMouseDataListenersOptions = {}
+ ) => {
+ return page.evaluate(({includeMove}) => {
+ const clicks: ClickData[] = [];
+ const mouseEventListener = (event: MouseEvent) => {
+ clicks.push({
+ type: event.type,
+ detail: event.detail,
+ clientX: event.clientX,
+ clientY: event.clientY,
+ isTrusted: event.isTrusted,
+ button: event.button,
+ buttons: event.buttons,
+ });
+ };
+ document.addEventListener('mousedown', mouseEventListener);
+ if (includeMove) {
+ document.addEventListener('mousemove', mouseEventListener);
+ }
+ document.addEventListener('mouseup', mouseEventListener);
+ document.addEventListener('click', mouseEventListener);
+ document.addEventListener('auxclick', mouseEventListener);
+ (window as unknown as {clicks: ClickData[]}).clicks = clicks;
+ }, options);
+ };
+
+ it('should not throw if clicking in parallel', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await addMouseDataListeners(page);
+
+ await Promise.all([page.mouse.click(0, 5), page.mouse.click(6, 10)]);
+
+ const data = await page.evaluate(() => {
+ return (window as unknown as {clicks: ClickData[]}).clicks;
+ });
+ const commonAttrs = {
+ isTrusted: true,
+ detail: 1,
+ clientY: 5,
+ clientX: 0,
+ button: 0,
+ };
+ expect(data.splice(0, 3)).toMatchObject({
+ 0: {
+ type: 'mousedown',
+ buttons: 1,
+ ...commonAttrs,
+ },
+ 1: {
+ type: 'mouseup',
+ buttons: 0,
+ ...commonAttrs,
+ },
+ 2: {
+ type: 'click',
+ buttons: 0,
+ ...commonAttrs,
+ },
+ });
+ Object.assign(commonAttrs, {
+ clientX: 6,
+ clientY: 10,
+ });
+ expect(data).toMatchObject({
+ 0: {
+ type: 'mousedown',
+ buttons: 1,
+ ...commonAttrs,
+ },
+ 1: {
+ type: 'mouseup',
+ buttons: 0,
+ ...commonAttrs,
+ },
+ 2: {
+ type: 'click',
+ buttons: 0,
+ ...commonAttrs,
+ },
+ });
+ });
+
+ it('should reset properly', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.mouse.move(5, 5);
+ await Promise.all([
+ page.mouse.down({button: MouseButton.Left}),
+ page.mouse.down({button: MouseButton.Middle}),
+ page.mouse.down({button: MouseButton.Right}),
+ ]);
+
+ await addMouseDataListeners(page, {includeMove: true});
+ await page.mouse.reset();
+
+ const data = await page.evaluate(() => {
+ return (window as unknown as {clicks: ClickData[]}).clicks;
+ });
+ const commonAttrs = {
+ isTrusted: true,
+ clientY: 5,
+ clientX: 5,
+ };
+
+ expect(data.slice(0, 2)).toMatchObject([
+ {
+ ...commonAttrs,
+ button: 2,
+ buttons: 5,
+ detail: 1,
+ type: 'mouseup',
+ },
+ {
+ ...commonAttrs,
+ button: 2,
+ buttons: 5,
+ detail: 1,
+ type: 'auxclick',
+ },
+ ]);
+ // TODO(crbug/1485040): This should align with the firefox implementation.
+ if (isChrome) {
+ expect(data.slice(2)).toMatchObject([
+ {
+ ...commonAttrs,
+ button: 1,
+ buttons: 1,
+ detail: 0,
+ type: 'mouseup',
+ },
+ {
+ ...commonAttrs,
+ button: 0,
+ buttons: 0,
+ detail: 0,
+ type: 'mouseup',
+ },
+ ]);
+ return;
+ }
+ expect(data.slice(2)).toMatchObject([
+ {
+ ...commonAttrs,
+ button: 1,
+ buttons: 1,
+ detail: 1,
+ type: 'mouseup',
+ },
+ {
+ ...commonAttrs,
+ button: 1,
+ buttons: 1,
+ detail: 1,
+ type: 'auxclick',
+ },
+ {
+ ...commonAttrs,
+ button: 0,
+ buttons: 0,
+ detail: 1,
+ type: 'mouseup',
+ },
+ {
+ ...commonAttrs,
+ button: 0,
+ buttons: 0,
+ detail: 1,
+ type: 'click',
+ },
+ ]);
+ });
+
+ it('should evaluate before mouse event', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/input/button.html');
+
+ using button = await page.waitForSelector('button');
+
+ const point = await button!.clickablePoint();
+
+ const result = page.evaluate(() => {
+ return new Promise(resolve => {
+ document
+ .querySelector('button')
+ ?.addEventListener('click', resolve, {once: true});
+ });
+ });
+ await page.mouse.click(point?.x, point?.y);
+ await result;
+ });
+});
diff --git a/remote/test/puppeteer/test/src/navigation.spec.ts b/remote/test/puppeteer/test/src/navigation.spec.ts
new file mode 100644
index 0000000000..1f3a51f58a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/navigation.spec.ts
@@ -0,0 +1,918 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ServerResponse} from 'http';
+
+import expect from 'expect';
+import {type Frame, TimeoutError} from 'puppeteer';
+import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js';
+import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame, isFavicon, waitEvent} from './utils.js';
+
+describe('navigation', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.goto', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with anchor navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ await page.goto(server.EMPTY_PAGE + '#foo');
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
+ await page.goto(server.EMPTY_PAGE + '#bar');
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#bar');
+ });
+ it('should work with redirects', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/redirect/1.html', '/redirect/2.html');
+ server.setRedirect('/redirect/2.html', '/empty.html');
+ await page.goto(server.PREFIX + '/redirect/1.html');
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should navigate to about:blank', async () => {
+ const {page} = await getTestState();
+
+ const response = await page.goto('about:blank');
+ expect(response).toBe(null);
+ });
+ it('should return response when page changes its URL after load', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(server.PREFIX + '/historyapi.html');
+ expect(response!.status()).toBe(200);
+ });
+ it('should work with subframes return 204', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/frames/frame.html', (_req, res) => {
+ res.statusCode = 204;
+ res.end();
+ });
+ let error!: Error;
+ await page
+ .goto(server.PREFIX + '/frames/one-frame.html')
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ });
+ it('should fail when server returns 204', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ server.setRoute('/empty.html', (_req, res) => {
+ res.statusCode = 204;
+ res.end();
+ });
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).not.toBe(null);
+ if (isChrome) {
+ expect(error.message).toContain('net::ERR_ABORTED');
+ } else {
+ expect(error.message).toContain('NS_BINDING_ABORTED');
+ }
+ });
+ it('should navigate to empty page with domcontentloaded', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE, {
+ waitUntil: 'domcontentloaded',
+ });
+ expect(response!.status()).toBe(200);
+ });
+ it('should work when page calls history API in beforeunload', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ window.addEventListener(
+ 'beforeunload',
+ () => {
+ return history.replaceState(null, 'initial', window.location.href);
+ },
+ false
+ );
+ });
+ const response = await page.goto(server.PREFIX + '/grid.html');
+ expect(response!.status()).toBe(200);
+ });
+ it('should navigate to empty page with networkidle0', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE, {
+ waitUntil: 'networkidle0',
+ });
+ expect(response!.status()).toBe(200);
+ });
+ it('should navigate to page with iframe and networkidle0', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(
+ server.PREFIX + '/frames/one-frame.html',
+ {
+ waitUntil: 'networkidle0',
+ }
+ );
+ expect(response!.status()).toBe(200);
+ });
+ it('should navigate to empty page with networkidle2', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE, {
+ waitUntil: 'networkidle2',
+ });
+ expect(response!.status()).toBe(200);
+ });
+ it('should fail when navigating to bad url', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.goto('asdfasdf').catch(error_ => {
+ return (error = error_);
+ });
+
+ expect(error.message).atLeastOneToContain([
+ 'Cannot navigate to invalid URL', // Firefox WebDriver BiDi.
+ 'invalid argument', // Others.
+ ]);
+ });
+
+ const EXPECTED_SSL_CERT_MESSAGE_REGEX =
+ /net::ERR_CERT_INVALID|net::ERR_CERT_AUTHORITY_INVALID/;
+
+ it('should fail when navigating to bad SSL', async () => {
+ const {page, httpsServer, isChrome} = await getTestState();
+
+ // Make sure that network events do not emit 'undefined'.
+ // @see https://crbug.com/750469
+ const requests: string[] = [];
+ page.on('request', () => {
+ return requests.push('request');
+ });
+ page.on('requestfinished', () => {
+ return requests.push('requestfinished');
+ });
+ page.on('requestfailed', () => {
+ return requests.push('requestfailed');
+ });
+
+ let error!: Error;
+ await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ if (isChrome) {
+ expect(error.message).toMatch(EXPECTED_SSL_CERT_MESSAGE_REGEX);
+ } else {
+ expect(error.message).toContain('SSL_ERROR_UNKNOWN');
+ }
+
+ expect(requests).toHaveLength(2);
+ expect(requests[0]).toBe('request');
+ expect(requests[1]).toBe('requestfailed');
+ });
+ it('should fail when navigating to bad SSL after redirects', async () => {
+ const {page, server, httpsServer, isChrome} = await getTestState();
+
+ server.setRedirect('/redirect/1.html', '/redirect/2.html');
+ server.setRedirect('/redirect/2.html', '/empty.html');
+ let error!: Error;
+ await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(error_ => {
+ return (error = error_);
+ });
+ if (isChrome) {
+ expect(error.message).toMatch(EXPECTED_SSL_CERT_MESSAGE_REGEX);
+ } else {
+ expect(error.message).atLeastOneToContain([
+ 'MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT', // Firefox WebDriver BiDi.
+ 'SSL_ERROR_UNKNOWN ', // Others.
+ ]);
+ }
+ });
+ it('should fail when main resources failed to load', async () => {
+ const {page, isChrome} = await getTestState();
+
+ let error!: Error;
+ await page
+ .goto('http://localhost:44123/non-existing-url')
+ .catch(error_ => {
+ return (error = error_);
+ });
+ if (isChrome) {
+ expect(error.message).toContain('net::ERR_CONNECTION_REFUSED');
+ } else {
+ expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED');
+ }
+ });
+ it('should fail when exceeding maximum navigation timeout', async () => {
+ const {page, server} = await getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ await page
+ .goto(server.PREFIX + '/empty.html', {timeout: 1})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should fail when exceeding default maximum navigation timeout', async () => {
+ const {page, server} = await getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ page.setDefaultNavigationTimeout(1);
+ await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should fail when exceeding default maximum timeout', async () => {
+ const {page, server} = await getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ page.setDefaultTimeout(1);
+ await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should prioritize default navigation timeout over default timeout', async () => {
+ const {page, server} = await getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ page.setDefaultTimeout(0);
+ page.setDefaultNavigationTimeout(1);
+ await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should disable timeout when its set to 0', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ let loaded = false;
+ page.once('load', () => {
+ loaded = true;
+ });
+ await page
+ .goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ expect(loaded).toBe(true);
+ });
+ it('should work when navigating to valid url', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ });
+ it('should work when navigating to data url', async () => {
+ const {page} = await getTestState();
+
+ const response = (await page.goto('data:text/html,hello'))!;
+ expect(response.ok()).toBe(true);
+ });
+ it('should work when navigating to 404', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/not-found'))!;
+ expect(response.ok()).toBe(false);
+ expect(response.status()).toBe(404);
+ });
+ it('should not throw an error for a 404 response with an empty body', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/404-error', (_, res) => {
+ res.statusCode = 404;
+ res.end();
+ });
+
+ const response = (await page.goto(server.PREFIX + '/404-error'))!;
+ expect(response.ok()).toBe(false);
+ expect(response.status()).toBe(404);
+ });
+ it('should not throw an error for a 500 response with an empty body', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/500-error', (_, res) => {
+ res.statusCode = 500;
+ res.end();
+ });
+
+ const response = (await page.goto(server.PREFIX + '/500-error'))!;
+ expect(response.ok()).toBe(false);
+ expect(response.status()).toBe(500);
+ });
+ it('should return last response in redirect chain', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/redirect/1.html', '/redirect/2.html');
+ server.setRedirect('/redirect/2.html', '/redirect/3.html');
+ server.setRedirect('/redirect/3.html', server.EMPTY_PAGE);
+ const response = (await page.goto(server.PREFIX + '/redirect/1.html'))!;
+ expect(response.ok()).toBe(true);
+ expect(response.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should wait for network idle to succeed navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let responses: ServerResponse[] = [];
+ // Hold on to a bunch of requests without answering.
+ server.setRoute('/fetch-request-a.js', (_req, res) => {
+ return responses.push(res);
+ });
+ server.setRoute('/fetch-request-b.js', (_req, res) => {
+ return responses.push(res);
+ });
+ server.setRoute('/fetch-request-c.js', (_req, res) => {
+ return responses.push(res);
+ });
+ server.setRoute('/fetch-request-d.js', (_req, res) => {
+ return responses.push(res);
+ });
+ const initialFetchResourcesRequested = Promise.all([
+ server.waitForRequest('/fetch-request-a.js'),
+ server.waitForRequest('/fetch-request-b.js'),
+ server.waitForRequest('/fetch-request-c.js'),
+ ]).catch(() => {
+ // Ignore Error that arise from test server during hooks
+ });
+ const secondFetchResourceRequested = server
+ .waitForRequest('/fetch-request-d.js')
+ .catch(() => {
+ // Ignore Error that arise from test server during hooks
+ });
+
+ // Track when the navigation gets completed.
+ let navigationFinished = false;
+ let navigationError: Error | undefined;
+ // Navigate to a page which loads immediately and then does a bunch of
+ // requests via javascript's fetch method.
+ const navigationPromise = page
+ .goto(server.PREFIX + '/networkidle.html', {
+ waitUntil: 'networkidle0',
+ })
+ .then(response => {
+ navigationFinished = true;
+ return response;
+ })
+ .catch(error => {
+ navigationError = error;
+ return null;
+ });
+
+ let afterNavigationError: Error | undefined;
+ const afterNavigationPromise = (async () => {
+ // Wait for the page's 'load' event.
+ await waitEvent(page, 'load');
+ expect(navigationFinished).toBe(false);
+
+ // Wait for the initial three resources to be requested.
+ await initialFetchResourcesRequested;
+
+ // Expect navigation still to be not finished.
+ expect(navigationFinished).toBe(false);
+
+ // Respond to initial requests.
+ for (const response of responses) {
+ response.statusCode = 404;
+ response.end(`File not found`);
+ }
+
+ // Reset responses array
+ responses = [];
+
+ // Wait for the second round to be requested.
+ await secondFetchResourceRequested;
+ // Expect navigation still to be not finished.
+ expect(navigationFinished).toBe(false);
+
+ // Respond to requests.
+ for (const response of responses) {
+ response.statusCode = 404;
+ response.end(`File not found`);
+ }
+ })().catch(error => {
+ afterNavigationError = error;
+ });
+
+ await Promise.race([navigationPromise, afterNavigationPromise]);
+ if (navigationError) {
+ throw navigationError;
+ }
+ await Promise.all([navigationPromise, afterNavigationPromise]);
+ if (afterNavigationError) {
+ throw afterNavigationError;
+ }
+ // Expect navigation to succeed.
+ expect(navigationFinished).toBeTruthy();
+ expect((await navigationPromise)?.ok()).toBe(true);
+ });
+ it('should not leak listeners during navigation', async function () {
+ this.timeout(25_000);
+
+ const {page, server} = await getTestState();
+
+ let warning = null;
+ const warningHandler: NodeJS.WarningListener = w => {
+ return (warning = w);
+ };
+ process.on('warning', warningHandler);
+ for (let i = 0; i < 20; ++i) {
+ await page.goto(server.EMPTY_PAGE);
+ }
+ process.removeListener('warning', warningHandler);
+ expect(warning).toBe(null);
+ });
+ it('should not leak listeners during bad navigation', async function () {
+ this.timeout(25_000);
+
+ const {page} = await getTestState();
+
+ let warning = null;
+ const warningHandler: NodeJS.WarningListener = w => {
+ return (warning = w);
+ };
+ process.on('warning', warningHandler);
+ for (let i = 0; i < 20; ++i) {
+ await page.goto('asdf').catch(() => {
+ /* swallow navigation error */
+ });
+ }
+ process.removeListener('warning', warningHandler);
+ expect(warning).toBe(null);
+ });
+ it('should not leak listeners during navigation of 11 pages', async function () {
+ this.timeout(25_000);
+
+ const {context, server} = await getTestState();
+
+ let warning = null;
+ const warningHandler: NodeJS.WarningListener = w => {
+ return (warning = w);
+ };
+ process.on('warning', warningHandler);
+ await Promise.all(
+ [...Array(20)].map(async () => {
+ const page = await context.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.close();
+ })
+ );
+ process.removeListener('warning', warningHandler);
+ expect(warning).toBe(null);
+ });
+ it('should navigate to dataURL and fire dataURL requests', async () => {
+ const {page} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const response = (await page.goto(dataURL))!;
+ expect(response.status()).toBe(200);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should navigate to URL with hash and fire requests without hash', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toBe(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with self requesting page', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/self-request.html'))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toContain('self-request.html');
+ });
+ it('should fail when navigating and show the url at the error message', async () => {
+ const {page, httpsServer} = await getTestState();
+
+ const url = httpsServer.PREFIX + '/redirect/1.html';
+ let error!: Error;
+ try {
+ await page.goto(url);
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain(url);
+ });
+ it('should send referer', async () => {
+ const {page, server} = await getTestState();
+
+ const requests = Promise.all([
+ server.waitForRequest('/grid.html'),
+ server.waitForRequest('/digits/1.png'),
+ page.goto(server.PREFIX + '/grid.html', {
+ referer: 'http://google.com/',
+ }),
+ ]).catch(() => {
+ return [];
+ });
+
+ const [request1, request2] = await requests;
+ expect(request1.headers['referer']).toBe('http://google.com/');
+ // Make sure subresources do not inherit referer.
+ expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html');
+ });
+
+ it('should send referer policy', async () => {
+ const {page, server} = await getTestState();
+
+ const [request1, request2] = await Promise.all([
+ server.waitForRequest('/grid.html'),
+ server.waitForRequest('/digits/1.png'),
+ page.goto(server.PREFIX + '/grid.html', {
+ referrerPolicy: 'no-referer',
+ }),
+ ]).catch(() => {
+ return [];
+ });
+ expect(request1.headers['referer']).toBeUndefined();
+ expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html');
+ });
+ });
+
+ describe('Page.waitForNavigation', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.evaluate((url: string) => {
+ return (window.location.href = url);
+ }, server.PREFIX + '/grid.html'),
+ ]);
+ expect(response!.ok()).toBe(true);
+ expect(response!.url()).toContain('grid.html');
+ });
+ it('should work with both domcontentloaded and load', async () => {
+ const {page, server} = await getTestState();
+
+ let response!: ServerResponse;
+ server.setRoute('/one-style.css', (_req, res) => {
+ return (response = res);
+ });
+ let error: Error | undefined;
+ let bothFired = false;
+ const navigationPromise = page
+ .goto(server.PREFIX + '/one-style.html')
+ .catch(_error => {
+ return (error = _error);
+ });
+ const domContentLoadedPromise = page
+ .waitForNavigation({
+ waitUntil: 'domcontentloaded',
+ })
+ .catch(_error => {
+ return (error = _error);
+ });
+
+ const loadFiredPromise = page
+ .waitForNavigation({
+ waitUntil: 'load',
+ })
+ .then(() => {
+ return (bothFired = true);
+ })
+ .catch(_error => {
+ return (error = _error);
+ });
+
+ await server.waitForRequest('/one-style.css').catch(() => {});
+ await domContentLoadedPromise;
+ expect(bothFired).toBe(false);
+ response.end();
+ await loadFiredPromise;
+ await navigationPromise;
+ expect(error).toBeUndefined();
+ });
+ it('should work with clicking on anchor links', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`<a href='#foobar'>foobar</a>`);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a'),
+ ]);
+ expect(response).toBe(null);
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar');
+ });
+ it('should work with history.pushState()', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`
+ <a onclick='javascript:pushState()'>SPA</a>
+ <script>
+ function pushState() { history.pushState({}, '', 'wow.html') }
+ </script>
+ `);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a'),
+ ]);
+ expect(response).toBe(null);
+ expect(page.url()).toBe(server.PREFIX + '/wow.html');
+ });
+ it('should work with history.replaceState()', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`
+ <a onclick='javascript:replaceState()'>SPA</a>
+ <script>
+ function replaceState() { history.replaceState({}, '', '/replaced.html') }
+ </script>
+ `);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a'),
+ ]);
+ expect(response).toBe(null);
+ expect(page.url()).toBe(server.PREFIX + '/replaced.html');
+ });
+ it('should work with DOM history.back()/history.forward()', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`
+ <a id=back onclick='javascript:goBack()'>back</a>
+ <a id=forward onclick='javascript:goForward()'>forward</a>
+ <script>
+ function goBack() { history.back(); }
+ function goForward() { history.forward(); }
+ history.pushState({}, '', '/first.html');
+ history.pushState({}, '', '/second.html');
+ </script>
+ `);
+ expect(page.url()).toBe(server.PREFIX + '/second.html');
+ const [backResponse] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a#back'),
+ ]);
+ expect(backResponse).toBe(null);
+ expect(page.url()).toBe(server.PREFIX + '/first.html');
+ const [forwardResponse] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a#forward'),
+ ]);
+ expect(forwardResponse).toBe(null);
+ expect(page.url()).toBe(server.PREFIX + '/second.html');
+ });
+ it('should work when subframe issues window.stop()', async function () {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/frames/style.css', () => {});
+ let frame: Frame | undefined;
+ const eventPromises = Deferred.race([
+ Promise.all([
+ waitEvent(page, 'frameattached').then(_frame => {
+ return (frame = _frame);
+ }),
+ waitEvent(page, 'framenavigated', f => {
+ return f === frame;
+ }),
+ ]),
+ Deferred.create({
+ message: `should work when subframe issues window.stop()`,
+ timeout: this.timeout() - 1000,
+ }),
+ ]);
+ const navigationPromise = page.goto(
+ server.PREFIX + '/frames/one-frame.html'
+ );
+ try {
+ await eventPromises;
+ } catch (error) {
+ navigationPromise.catch(() => {});
+ throw error;
+ }
+ await Promise.all([
+ frame!.evaluate(() => {
+ return window.stop();
+ }),
+ navigationPromise,
+ ]);
+ });
+ });
+
+ describe('Page.goBack', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.goto(server.PREFIX + '/grid.html');
+
+ let response = (await page.goBack())!;
+ expect(response.ok()).toBe(true);
+ expect(response.url()).toContain(server.EMPTY_PAGE);
+
+ response = (await page.goForward())!;
+ expect(response.ok()).toBe(true);
+ expect(response.url()).toContain('/grid.html');
+
+ response = (await page.goForward())!;
+ expect(response).toBe(null);
+ });
+ it('should work with HistoryAPI', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ history.pushState({}, '', '/first.html');
+ history.pushState({}, '', '/second.html');
+ });
+ expect(page.url()).toBe(server.PREFIX + '/second.html');
+
+ await page.goBack();
+ expect(page.url()).toBe(server.PREFIX + '/first.html');
+ await page.goBack();
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ await page.goForward();
+ expect(page.url()).toBe(server.PREFIX + '/first.html');
+ });
+ });
+
+ describe('Frame.goto', function () {
+ it('should navigate subframes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ expect(page.frames()[0]!.url()).toContain('/frames/one-frame.html');
+ expect(page.frames()[1]!.url()).toContain('/frames/frame.html');
+
+ const response = (await page.frames()[1]!.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ expect(response.frame()).toBe(page.frames()[1]);
+ });
+ it('should reject when frame detaches', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+
+ server.setRoute('/empty.html', () => {});
+ const navigationPromise = page
+ .frames()[1]!
+ .goto(server.EMPTY_PAGE)
+ .catch(error_ => {
+ return error_;
+ });
+ await server.waitForRequest('/empty.html').catch(() => {});
+
+ await page.$eval('iframe', frame => {
+ return frame.remove();
+ });
+ const error = await navigationPromise;
+ expect(error.message).atLeastOneToContain([
+ 'Navigating frame was detached',
+ 'Frame detached',
+ 'Error: NS_BINDING_ABORTED',
+ 'net::ERR_ABORTED',
+ ]);
+ });
+ it('should return matching responses', async () => {
+ const {page, server} = await getTestState();
+
+ // Disable cache: otherwise, the browser will cache similar requests.
+ await page.setCacheEnabled(false);
+ await page.goto(server.EMPTY_PAGE);
+ // Attach three frames.
+ const frames = await Promise.all([
+ attachFrame(page, 'frame1', server.EMPTY_PAGE),
+ attachFrame(page, 'frame2', server.EMPTY_PAGE),
+ attachFrame(page, 'frame3', server.EMPTY_PAGE),
+ ]);
+ // Navigate all frames to the same URL.
+ const serverResponses: ServerResponse[] = [];
+ server.setRoute('/one-style.html', (_req, res) => {
+ return serverResponses.push(res);
+ });
+ const navigations: Array<Promise<HTTPResponse | null>> = [];
+ for (let i = 0; i < 3; ++i) {
+ navigations.push(frames[i]!.goto(server.PREFIX + '/one-style.html'));
+ await server.waitForRequest('/one-style.html');
+ }
+ // Respond from server out-of-order.
+ const serverResponseTexts = ['AAA', 'BBB', 'CCC'];
+ try {
+ for (const i of [1, 2, 0]) {
+ const response = await getResponse(i);
+ expect(response.frame()).toBe(frames[i]);
+ expect(await response.text()).toBe(serverResponseTexts[i]);
+ }
+ } catch (error) {
+ await Promise.all([getResponse(0), getResponse(1), getResponse(2)]);
+ throw error;
+ }
+
+ async function getResponse(index: number) {
+ serverResponses[index]!.end(serverResponseTexts[index]);
+ return (await navigations[index])!;
+ }
+ });
+ });
+
+ describe('Frame.waitForNavigation', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ const frame = page.frames()[1]!;
+ const [response] = await Promise.all([
+ frame.waitForNavigation(),
+ frame.evaluate((url: string) => {
+ return (window.location.href = url);
+ }, server.PREFIX + '/grid.html'),
+ ]);
+ expect(response!.ok()).toBe(true);
+ expect(response!.url()).toContain('grid.html');
+ expect(response!.frame()).toBe(frame);
+ expect(page.url()).toContain('/frames/one-frame.html');
+ });
+ it('should fail when frame detaches', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ const frame = page.frames()[1]!;
+
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ const navigationPromise = frame.waitForNavigation().catch(error_ => {
+ return (error = error_);
+ });
+ await Promise.all([
+ server.waitForRequest('/empty.html'),
+ frame.evaluate(() => {
+ return ((window as any).location = '/empty.html');
+ }),
+ ]);
+ await page.$eval('iframe', frame => {
+ return frame.remove();
+ });
+ await navigationPromise;
+ expect(error.message).atLeastOneToContain([
+ 'Navigating frame was detached',
+ 'Frame detached',
+ ]);
+ });
+ });
+
+ describe('Page.reload', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return ((globalThis as any)._foo = 10);
+ });
+ await page.reload();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any)._foo;
+ })
+ ).toBe(undefined);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/network.spec.ts b/remote/test/puppeteer/test/src/network.spec.ts
new file mode 100644
index 0000000000..c6f51a3412
--- /dev/null
+++ b/remote/test/puppeteer/test/src/network.spec.ts
@@ -0,0 +1,917 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import type {ServerResponse} from 'http';
+import path from 'path';
+
+import expect from 'expect';
+import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js';
+
+import {getTestState, launch, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame, isFavicon, waitEvent} from './utils.js';
+
+describe('network', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.Events.Request', function () {
+ it('should fire for navigation requests', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ });
+ it('should fire for iframes', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(requests).toHaveLength(2);
+ });
+ it('should fire for fetches', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return fetch('/empty.html');
+ });
+ expect(requests).toHaveLength(2);
+ });
+ });
+ describe('Request.frame', function () {
+ it('should work for main frame navigation request', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.frame()).toBe(page.mainFrame());
+ });
+ it('should work for subframe navigation request', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.frame()).toBe(page.frames()[1]);
+ });
+ it('should work for fetch requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.evaluate(() => {
+ return fetch('/digits/1.png');
+ });
+ requests = requests.filter(request => {
+ return !request.url().includes('favicon');
+ });
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.frame()).toBe(page.mainFrame());
+ });
+ });
+
+ describe('Request.headers', function () {
+ it('should define Browser in user agent header', async () => {
+ const {page, server, isChrome} = await getTestState();
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ const userAgent = response.request().headers()['user-agent'];
+
+ if (isChrome) {
+ expect(userAgent).toContain('Chrome');
+ } else {
+ expect(userAgent).toContain('Firefox');
+ }
+ });
+ });
+
+ describe('Response.headers', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('foo', 'bar');
+ res.end();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.headers()['foo']).toBe('bar');
+ });
+ });
+
+ describe('Request.initiator', () => {
+ it('should return the initiator', async () => {
+ const {page, server} = await getTestState();
+
+ const initiators = new Map();
+ page.on('request', request => {
+ return initiators.set(
+ request.url().split('/').pop(),
+ request.initiator()
+ );
+ });
+ await page.goto(server.PREFIX + '/initiator.html');
+
+ expect(initiators.get('initiator.html').type).toBe('other');
+ expect(initiators.get('initiator.js').type).toBe('parser');
+ expect(initiators.get('initiator.js').url).toBe(
+ server.PREFIX + '/initiator.html'
+ );
+ expect(initiators.get('frame.html').type).toBe('parser');
+ expect(initiators.get('frame.html').url).toBe(
+ server.PREFIX + '/initiator.html'
+ );
+ expect(initiators.get('script.js').type).toBe('parser');
+ expect(initiators.get('script.js').url).toBe(
+ server.PREFIX + '/frames/frame.html'
+ );
+ expect(initiators.get('style.css').type).toBe('parser');
+ expect(initiators.get('style.css').url).toBe(
+ server.PREFIX + '/frames/frame.html'
+ );
+ expect(initiators.get('initiator.js').type).toBe('parser');
+ expect(initiators.get('injectedfile.js').type).toBe('script');
+ expect(initiators.get('injectedfile.js').stack.callFrames[0]!.url).toBe(
+ server.PREFIX + '/initiator.js'
+ );
+ expect(initiators.get('injectedstyle.css').type).toBe('script');
+ expect(initiators.get('injectedstyle.css').stack.callFrames[0]!.url).toBe(
+ server.PREFIX + '/initiator.js'
+ );
+ expect(initiators.get('initiator.js').url).toBe(
+ server.PREFIX + '/initiator.html'
+ );
+ });
+ });
+
+ describe('Response.fromCache', function () {
+ it('should return |false| for non-cached content', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.fromCache()).toBe(false);
+ });
+
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const responses = new Map();
+ page.on('response', r => {
+ return (
+ !isFavicon(r.request()) && responses.set(r.url().split('/').pop(), r)
+ );
+ });
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ await page.reload();
+
+ expect(responses.size).toBe(2);
+ expect(responses.get('one-style.css').status()).toBe(200);
+ expect(responses.get('one-style.css').fromCache()).toBe(true);
+ expect(responses.get('one-style.html').status()).toBe(304);
+ expect(responses.get('one-style.html').fromCache()).toBe(false);
+ });
+ });
+
+ describe('Response.fromServiceWorker', function () {
+ it('should return |false| for non-service-worker content', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.fromServiceWorker()).toBe(false);
+ });
+
+ it('Response.fromServiceWorker', async () => {
+ const {page, server} = await getTestState();
+
+ const responses = new Map();
+ page.on('response', r => {
+ return !isFavicon(r) && responses.set(r.url().split('/').pop(), r);
+ });
+
+ // Load and re-load to make sure serviceworker is installed and running.
+ await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {
+ waitUntil: 'networkidle2',
+ });
+ await page.evaluate(async () => {
+ return (globalThis as any).activationPromise;
+ });
+ await page.reload();
+
+ expect(responses.size).toBe(2);
+ expect(responses.get('sw.html').status()).toBe(200);
+ expect(responses.get('sw.html').fromServiceWorker()).toBe(true);
+ expect(responses.get('style.css').status()).toBe(200);
+ expect(responses.get('style.css').fromServiceWorker()).toBe(true);
+ });
+ });
+
+ describe('Request.postData', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRoute('/post', (_req, res) => {
+ return res.end();
+ });
+
+ const [request] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'request', r => {
+ return !isFavicon(r);
+ }),
+ page.evaluate(() => {
+ return fetch('./post', {
+ method: 'POST',
+ body: JSON.stringify({foo: 'bar'}),
+ });
+ }),
+ ]);
+
+ expect(request).toBeTruthy();
+ expect(request.postData()).toBe('{"foo":"bar"}');
+ });
+
+ it('should be |undefined| when there is no post data', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.request().postData()).toBe(undefined);
+ });
+
+ it('should work with blobs', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRoute('/post', (_req, res) => {
+ return res.end();
+ });
+
+ const [request] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'request', r => {
+ return !isFavicon(r);
+ }),
+ page.evaluate(() => {
+ return fetch('./post', {
+ method: 'POST',
+ body: new Blob([JSON.stringify({foo: 'bar'})], {
+ type: 'application/json',
+ }),
+ });
+ }),
+ ]);
+
+ expect(request).toBeTruthy();
+ expect(request.postData()).toBe(undefined);
+ expect(request.hasPostData()).toBe(true);
+ expect(await request.fetchPostData()).toBe('{"foo":"bar"}');
+ });
+ });
+
+ describe('Response.text', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/simple.json'))!;
+ const responseText = (await response.text()).trimEnd();
+ expect(responseText).toBe('{"foo": "bar"}');
+ });
+ it('should return uncompressed text', async () => {
+ const {page, server} = await getTestState();
+
+ server.enableGzip('/simple.json');
+ const response = (await page.goto(server.PREFIX + '/simple.json'))!;
+ expect(response.headers()['content-encoding']).toBe('gzip');
+ const responseText = (await response.text()).trimEnd();
+ expect(responseText).toBe('{"foo": "bar"}');
+ });
+ it('should throw when requesting body of redirected response', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/foo.html', '/empty.html');
+ const response = (await page.goto(server.PREFIX + '/foo.html'))!;
+ const redirectChain = response.request().redirectChain();
+ expect(redirectChain).toHaveLength(1);
+ const redirected = redirectChain[0]!.response()!;
+ expect(redirected.status()).toBe(302);
+ let error!: Error;
+ await redirected.text().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain(
+ 'Response body is unavailable for redirect responses'
+ );
+ });
+ it('should wait until response completes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ // Setup server to trap request.
+ let serverResponse!: ServerResponse;
+ server.setRoute('/get', (_req, res) => {
+ serverResponse = res;
+ // In Firefox, |fetch| will be hanging until it receives |Content-Type| header
+ // from server.
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
+ res.write('hello ');
+ });
+ // Setup page to trap response.
+ let requestFinished = false;
+ page.on('requestfinished', r => {
+ return (requestFinished = requestFinished || r.url().includes('/get'));
+ });
+ // send request and wait for server response
+ const [pageResponse] = await Promise.all([
+ page.waitForResponse(r => {
+ return !isFavicon(r.request());
+ }),
+ page.evaluate(() => {
+ return fetch('./get', {method: 'GET'});
+ }),
+ server.waitForRequest('/get'),
+ ]);
+
+ expect(serverResponse).toBeTruthy();
+ expect(pageResponse).toBeTruthy();
+ expect(pageResponse.status()).toBe(200);
+ expect(requestFinished).toBe(false);
+
+ const responseText = pageResponse.text();
+ // Write part of the response and wait for it to be flushed.
+ await new Promise(x => {
+ return serverResponse.write('wor', x);
+ });
+ // Finish response.
+ await new Promise<void>(x => {
+ serverResponse.end('ld!', () => {
+ return x();
+ });
+ });
+ expect(await responseText).toBe('hello world!');
+ });
+ });
+
+ describe('Response.json', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/simple.json'))!;
+ expect(await response.json()).toEqual({foo: 'bar'});
+ });
+ });
+
+ describe('Response.buffer', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/pptr.png'))!;
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, '../assets', 'pptr.png')
+ );
+ const responseBuffer = await response.buffer();
+ expect(responseBuffer.equals(imageBuffer)).toBe(true);
+ });
+ it('should work with compression', async () => {
+ const {page, server} = await getTestState();
+
+ server.enableGzip('/pptr.png');
+ const response = (await page.goto(server.PREFIX + '/pptr.png'))!;
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, '../assets', 'pptr.png')
+ );
+ const responseBuffer = await response.buffer();
+ expect(responseBuffer.equals(imageBuffer)).toBe(true);
+ });
+ it('should throw if the response does not have a body', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/empty.html');
+
+ server.setRoute('/test.html', (_req, res) => {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Headers', 'x-ping');
+ res.end('Hello World');
+ });
+ const url = server.CROSS_PROCESS_PREFIX + '/test.html';
+ const responsePromise = waitEvent<HTTPResponse>(
+ page,
+ 'response',
+ response => {
+ // Get the preflight response.
+ return (
+ response.request().method() === 'OPTIONS' && response.url() === url
+ );
+ }
+ );
+
+ // Trigger a request with a preflight.
+ await page.evaluate(async src => {
+ const response = await fetch(src, {
+ method: 'POST',
+ headers: {'x-ping': 'pong'},
+ });
+ return response;
+ }, url);
+
+ const response = await responsePromise;
+ await expect(response.buffer()).rejects.toThrowError(
+ 'Could not load body for this request. This might happen if the request is a preflight request.'
+ );
+ });
+ });
+
+ describe('Response.statusText', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/cool', (_req, res) => {
+ res.writeHead(200, 'cool!');
+ res.end();
+ });
+ const response = (await page.goto(server.PREFIX + '/cool'))!;
+ expect(response.statusText()).toBe('cool!');
+ });
+
+ it('handles missing status text', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/nostatus', (_req, res) => {
+ res.writeHead(200, '');
+ res.end();
+ });
+ const response = (await page.goto(server.PREFIX + '/nostatus'))!;
+ expect(response.statusText()).toBe('');
+ });
+ });
+
+ describe('Response.timing', function () {
+ it('returns timing information', async () => {
+ const {page, server} = await getTestState();
+ const responses: HTTPResponse[] = [];
+ page.on('response', response => {
+ return responses.push(response);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(responses).toHaveLength(1);
+ expect(responses[0]!.timing()!.receiveHeadersEnd).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Network Events', function () {
+ it('Page.Events.Request', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
+ expect(requests[0]!.resourceType()).toBe('document');
+ expect(requests[0]!.method()).toBe('GET');
+ expect(requests[0]!.response()).toBeTruthy();
+ expect(requests[0]!.frame() === page.mainFrame()).toBe(true);
+ expect(requests[0]!.frame()!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('Page.Events.RequestServedFromCache', async () => {
+ const {page, server} = await getTestState();
+
+ const cached: string[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r.url().split('/').pop()!);
+ });
+
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ expect(cached).toEqual([]);
+
+ await page.reload();
+ expect(cached).toEqual(['one-style.css']);
+ });
+ it('Page.Events.Response', async () => {
+ const {page, server} = await getTestState();
+
+ const responses: HTTPResponse[] = [];
+ page.on('response', response => {
+ return responses.push(response);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(responses).toHaveLength(1);
+ expect(responses[0]!.url()).toBe(server.EMPTY_PAGE);
+ expect(responses[0]!.status()).toBe(200);
+ expect(responses[0]!.ok()).toBe(true);
+ expect(responses[0]!.request()).toBeTruthy();
+ const remoteAddress = responses[0]!.remoteAddress();
+ // Either IPv6 or IPv4, depending on environment.
+ expect(
+ remoteAddress.ip!.includes('::1') || remoteAddress.ip === '127.0.0.1'
+ ).toBe(true);
+ expect(remoteAddress.port).toBe(server.PORT);
+ });
+
+ it('Page.Events.RequestFailed', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.url().endsWith('css')) {
+ void request.abort();
+ } else {
+ void request.continue();
+ }
+ });
+ const failedRequests: HTTPRequest[] = [];
+ page.on('requestfailed', request => {
+ return failedRequests.push(request);
+ });
+ await page.goto(server.PREFIX + '/one-style.html');
+ expect(failedRequests).toHaveLength(1);
+ expect(failedRequests[0]!.url()).toContain('one-style.css');
+ expect(failedRequests[0]!.response()).toBe(null);
+ expect(failedRequests[0]!.resourceType()).toBe('stylesheet');
+ if (isChrome) {
+ expect(failedRequests[0]!.failure()!.errorText).toBe('net::ERR_FAILED');
+ } else {
+ expect(failedRequests[0]!.failure()!.errorText).toBe(
+ 'NS_ERROR_FAILURE'
+ );
+ }
+ expect(failedRequests[0]!.frame()).toBeTruthy();
+ });
+ it('Page.Events.RequestFinished', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('requestfinished', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ const request = requests[0]!;
+ expect(request.url()).toBe(server.EMPTY_PAGE);
+ expect(request.response()).toBeTruthy();
+ expect(request.frame() === page.mainFrame()).toBe(true);
+ expect(request.frame()!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should fire events in proper order', async () => {
+ const {page, server} = await getTestState();
+
+ const events: string[] = [];
+ page.on('request', () => {
+ return events.push('request');
+ });
+ page.on('response', () => {
+ return events.push('response');
+ });
+ page.on('requestfinished', () => {
+ return events.push('requestfinished');
+ });
+ await page.goto(server.EMPTY_PAGE);
+ // Events can sneak in after the page has navigate
+ expect(events.slice(0, 3)).toEqual([
+ 'request',
+ 'response',
+ 'requestfinished',
+ ]);
+ });
+ it('should support redirects', async () => {
+ const {page, server} = await getTestState();
+
+ const events: string[] = [];
+ page.on('request', request => {
+ return events.push(`${request.method()} ${request.url()}`);
+ });
+ page.on('response', response => {
+ return events.push(`${response.status()} ${response.url()}`);
+ });
+ page.on('requestfinished', request => {
+ return events.push(`DONE ${request.url()}`);
+ });
+ page.on('requestfailed', request => {
+ return events.push(`FAIL ${request.url()}`);
+ });
+ server.setRedirect('/foo.html', '/empty.html');
+ const FOO_URL = server.PREFIX + '/foo.html';
+ const response = (await page.goto(FOO_URL))!;
+ expect(events).toEqual([
+ `GET ${FOO_URL}`,
+ `302 ${FOO_URL}`,
+ `DONE ${FOO_URL}`,
+ `GET ${server.EMPTY_PAGE}`,
+ `200 ${server.EMPTY_PAGE}`,
+ `DONE ${server.EMPTY_PAGE}`,
+ ]);
+
+ // Check redirect chain
+ const redirectChain = response.request().redirectChain();
+ expect(redirectChain).toHaveLength(1);
+ expect(redirectChain[0]!.url()).toContain('/foo.html');
+ expect(redirectChain[0]!.response()!.remoteAddress().port).toBe(
+ server.PORT
+ );
+ });
+ });
+
+ describe('Request.isNavigationRequest', () => {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const requests = new Map();
+ page.on('request', request => {
+ return requests.set(request.url().split('/').pop(), request);
+ });
+ server.setRedirect('/rrredirect', '/frames/one-frame.html');
+ await page.goto(server.PREFIX + '/rrredirect');
+ expect(requests.get('rrredirect').isNavigationRequest()).toBe(true);
+ expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true);
+ expect(requests.get('frame.html').isNavigationRequest()).toBe(true);
+ expect(requests.get('script.js').isNavigationRequest()).toBe(false);
+ expect(requests.get('style.css').isNavigationRequest()).toBe(false);
+ });
+ it('should work with request interception', async () => {
+ const {page, server} = await getTestState();
+
+ const requests = new Map();
+ page.on('request', request => {
+ requests.set(request.url().split('/').pop(), request);
+ void request.continue();
+ });
+ await page.setRequestInterception(true);
+ server.setRedirect('/rrredirect', '/frames/one-frame.html');
+ await page.goto(server.PREFIX + '/rrredirect');
+ expect(requests.get('rrredirect').isNavigationRequest()).toBe(true);
+ expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true);
+ expect(requests.get('frame.html').isNavigationRequest()).toBe(true);
+ expect(requests.get('script.js').isNavigationRequest()).toBe(false);
+ expect(requests.get('style.css').isNavigationRequest()).toBe(false);
+ });
+ it('should work when navigating to image', async () => {
+ const {page, server} = await getTestState();
+
+ const [request] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'request'),
+ page.goto(server.PREFIX + '/pptr.png'),
+ ]);
+ expect(request.isNavigationRequest()).toBe(true);
+ });
+ });
+
+ describe('Page.setExtraHTTPHeaders', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ foo: 'bar',
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(request.headers['foo']).toBe('bar');
+ });
+ it('should throw for non-string header values', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ // @ts-expect-error purposeful bad input
+ await page.setExtraHTTPHeaders({foo: 1});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toBe(
+ 'Expected value of header "foo" to be String, but "number" is found.'
+ );
+ });
+ });
+
+ describe('Page.authenticate', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ server.setAuth('/empty.html', 'user', 'pass');
+ let response;
+ try {
+ response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(401);
+ } catch (error) {
+ // In headful, an error is thrown instead of 401.
+ if (
+ !(error as Error).message.startsWith(
+ 'net::ERR_INVALID_AUTH_CREDENTIALS'
+ )
+ ) {
+ throw error;
+ }
+ }
+ await page.authenticate({
+ username: 'user',
+ password: 'pass',
+ });
+ response = (await page.reload())!;
+ expect(response.status()).toBe(200);
+ });
+ it('should fail if wrong credentials', async () => {
+ const {page, server} = await getTestState();
+
+ // Use unique user/password since Chrome caches credentials per origin.
+ server.setAuth('/empty.html', 'user2', 'pass2');
+ await page.authenticate({
+ username: 'foo',
+ password: 'bar',
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(401);
+ });
+ it('should allow disable authentication', async () => {
+ const {page, server} = await getTestState();
+
+ // Use unique user/password since Chrome caches credentials per origin.
+ server.setAuth('/empty.html', 'user3', 'pass3');
+ await page.authenticate({
+ username: 'user3',
+ password: 'pass3',
+ });
+ let response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(200);
+ await page.authenticate({
+ username: '',
+ password: '',
+ });
+ // Navigate to a different origin to bust Chrome's credential caching.
+ try {
+ response = (await page.goto(
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ ))!;
+ expect(response.status()).toBe(401);
+ } catch (error) {
+ // In headful, an error is thrown instead of 401.
+ if (
+ !(error as Error).message.startsWith(
+ 'net::ERR_INVALID_AUTH_CREDENTIALS'
+ )
+ ) {
+ throw error;
+ }
+ }
+ });
+ it('should not disable caching', async () => {
+ const {page, server} = await getTestState();
+
+ // Use unique user/password since Chrome caches credentials per origin.
+ server.setAuth('/cached/one-style.css', 'user4', 'pass4');
+ server.setAuth('/cached/one-style.html', 'user4', 'pass4');
+ await page.authenticate({
+ username: 'user4',
+ password: 'pass4',
+ });
+
+ const responses = new Map();
+ page.on('response', r => {
+ return responses.set(r.url().split('/').pop(), r);
+ });
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ await page.reload();
+
+ expect(responses.get('one-style.css').status()).toBe(200);
+ expect(responses.get('one-style.css').fromCache()).toBe(true);
+ expect(responses.get('one-style.html').status()).toBe(304);
+ expect(responses.get('one-style.html').fromCache()).toBe(false);
+ });
+ });
+
+ describe('raw network headers', () => {
+ it('Same-origin set-cookie navigation', async () => {
+ const {page, server} = await getTestState();
+
+ const setCookieString = 'foo=bar';
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('set-cookie', setCookieString);
+ res.end('hello world');
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.headers()['set-cookie']).toBe(setCookieString);
+ });
+
+ it('Same-origin set-cookie subresource', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+
+ const setCookieString = 'foo=bar';
+ server.setRoute('/foo', (_req, res) => {
+ res.setHeader('set-cookie', setCookieString);
+ res.end('hello world');
+ });
+
+ const [response] = await Promise.all([
+ waitEvent<HTTPResponse>(page, 'response', res => {
+ return !isFavicon(res);
+ }),
+ page.evaluate(() => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', '/foo');
+ xhr.send();
+ }),
+ ]);
+ expect(response.headers()['set-cookie']).toBe(setCookieString);
+ });
+
+ it('Cross-origin set-cookie', async () => {
+ const {page, httpsServer, close} = await launch({
+ ignoreHTTPSErrors: true,
+ });
+ try {
+ await page.goto(httpsServer.PREFIX + '/empty.html');
+
+ const setCookieString = 'hello=world';
+ httpsServer.setRoute('/setcookie.html', (_req, res) => {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('set-cookie', setCookieString);
+ res.end();
+ });
+ await page.goto(httpsServer.PREFIX + '/setcookie.html');
+ const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html';
+ const [response] = await Promise.all([
+ waitEvent<HTTPResponse>(page, 'response', response => {
+ return response.url() === url;
+ }),
+ page.evaluate(src => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', src);
+ xhr.send();
+ }, url),
+ ]);
+ expect(response.headers()['set-cookie']).toBe(setCookieString);
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Page.setBypassServiceWorker', () => {
+ it('bypass for network', async () => {
+ const {page, server} = await getTestState();
+
+ const responses = new Map();
+ page.on('response', r => {
+ return !isFavicon(r) && responses.set(r.url().split('/').pop(), r);
+ });
+
+ // Load and re-load to make sure serviceworker is installed and running.
+ await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {
+ waitUntil: 'networkidle2',
+ });
+ await page.evaluate(async () => {
+ return (globalThis as any).activationPromise;
+ });
+ await page.reload({
+ waitUntil: 'networkidle2',
+ });
+
+ expect(page.isServiceWorkerBypassed()).toBe(false);
+ expect(responses.size).toBe(2);
+ expect(responses.get('sw.html').status()).toBe(200);
+ expect(responses.get('sw.html').fromServiceWorker()).toBe(true);
+ expect(responses.get('style.css').status()).toBe(200);
+ expect(responses.get('style.css').fromServiceWorker()).toBe(true);
+
+ await page.setBypassServiceWorker(true);
+ await page.reload({
+ waitUntil: 'networkidle2',
+ });
+
+ expect(page.isServiceWorkerBypassed()).toBe(true);
+ expect(responses.get('sw.html').status()).toBe(200);
+ expect(responses.get('sw.html').fromServiceWorker()).toBe(false);
+ expect(responses.get('style.css').status()).toBe(200);
+ expect(responses.get('style.css').fromServiceWorker()).toBe(false);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/oopif.spec.ts b/remote/test/puppeteer/test/src/oopif.spec.ts
new file mode 100644
index 0000000000..c024b76aba
--- /dev/null
+++ b/remote/test/puppeteer/test/src/oopif.spec.ts
@@ -0,0 +1,527 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js';
+import type {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
+import {CDPSessionEvent} from 'puppeteer-core/internal/api/CDPSession.js';
+import type {CdpTarget} from 'puppeteer-core/internal/cdp/Target.js';
+
+import {getTestState, launch} from './mocha-utils.js';
+import {attachFrame, detachFrame, navigateFrame} from './utils.js';
+
+describe('OOPIF', function () {
+ /* We use a special browser for this test as we need the --site-per-process flag */
+ let state: Awaited<ReturnType<typeof launch>>;
+
+ before(async () => {
+ const {defaultBrowserOptions} = await getTestState({skipLaunch: true});
+
+ state = await launch(
+ Object.assign({}, defaultBrowserOptions, {
+ args: (defaultBrowserOptions.args || []).concat([
+ '--site-per-process',
+ '--remote-debugging-port=21222',
+ '--host-rules=MAP * 127.0.0.1',
+ ]),
+ }),
+ {after: 'all'}
+ );
+ });
+
+ beforeEach(async () => {
+ state.context = await state.browser.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ });
+
+ afterEach(async () => {
+ await state.context.close();
+ });
+
+ after(async () => {
+ await state.close();
+ });
+
+ it('should treat OOP iframes and normal iframes the same', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return frame.url().endsWith('/empty.html');
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(
+ page,
+ 'frame2',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ await framePromise;
+ expect(page.mainFrame().childFrames()).toHaveLength(2);
+ });
+ it('should track navigations within OOP iframes', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ const frame = await framePromise;
+ expect(frame.url()).toContain('/empty.html');
+ await navigateFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/assets/frame.html'
+ );
+ expect(frame.url()).toContain('/assets/frame.html');
+ });
+ it('should support OOP iframes becoming normal iframes again', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+
+ const frame = await framePromise;
+ expect(frame.isOOPFrame()).toBe(false);
+ await navigateFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ expect(frame.isOOPFrame()).toBe(true);
+ await navigateFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(frame.isOOPFrame()).toBe(false);
+ expect(page.frames()).toHaveLength(2);
+ });
+ it('should support frames within OOP frames', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const frame1Promise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ const frame2Promise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 2;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/frames/one-frame.html'
+ );
+
+ const [frame1, frame2] = await Promise.all([frame1Promise, frame2Promise]);
+
+ expect(
+ await frame1.evaluate(() => {
+ return document.location.href;
+ })
+ ).toMatch(/one-frame\.html$/);
+ expect(
+ await frame2.evaluate(() => {
+ return document.location.href;
+ })
+ ).toMatch(/frames\/frame\.html$/);
+ });
+ it('should support OOP iframes getting detached', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+
+ const frame = await framePromise;
+ expect(frame.isOOPFrame()).toBe(false);
+ await navigateFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ expect(frame.isOOPFrame()).toBe(true);
+ await detachFrame(page, 'frame1');
+ expect(page.frames()).toHaveLength(1);
+ });
+
+ it('should support wait for navigation for transitions from local to OOPIF', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+
+ const frame = await framePromise;
+ expect(frame.isOOPFrame()).toBe(false);
+ const nav = frame.waitForNavigation();
+ await navigateFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ await nav;
+ expect(frame.isOOPFrame()).toBe(true);
+ await detachFrame(page, 'frame1');
+ expect(page.frames()).toHaveLength(1);
+ });
+
+ it('should keep track of a frames OOP state', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ const frame = await framePromise;
+ expect(frame.url()).toContain('/empty.html');
+ await navigateFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(frame.url()).toBe(server.EMPTY_PAGE);
+ });
+
+ it('should support evaluating in oop iframes', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ const frame = await framePromise;
+ await frame.evaluate(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ _test = 'Test 123!';
+ });
+ const result = await frame.evaluate(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ return window._test;
+ });
+ expect(result).toBe('Test 123!');
+ });
+ it('should provide access to elements', async () => {
+ const {server, isHeadless, headless, page} = state;
+
+ if (!isHeadless || headless === 'new') {
+ // TODO: this test is partially blocked on crbug.com/1334119. Enable test once
+ // the upstream is fixed.
+ // TLDR: when we dispatch events to the frame the compositor might
+ // not be up-to-date yet resulting in a misclick (the iframe element
+ // becomes the event target instead of the content inside the iframe).
+ // The solution is to use InsertVisualCallback on the backend but that causes
+ // another issue that events cannot be dispatched to inactive tabs as the
+ // visual callback is never invoked.
+ // The old headless mode does not have this issue since it operates with
+ // special scheduling settings that keep even inactive tabs updating.
+ return;
+ }
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+
+ const frame = await framePromise;
+ await frame.evaluate(() => {
+ const button = document.createElement('button');
+ button.id = 'test-button';
+ button.innerText = 'click';
+ button.onclick = () => {
+ button.id = 'clicked';
+ };
+ document.body.appendChild(button);
+ });
+ await page.evaluate(() => {
+ document.body.style.border = '150px solid black';
+ document.body.style.margin = '250px';
+ document.body.style.padding = '50px';
+ });
+ await frame.waitForSelector('#test-button', {visible: true});
+ await frame.click('#test-button');
+ await frame.waitForSelector('#clicked');
+ });
+ it('should report oopif frames', async () => {
+ const {server, page, context} = state;
+
+ const frame = page.waitForFrame(frame => {
+ return frame.url().endsWith('/oopif.html');
+ });
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ await frame;
+ expect(oopifs(context)).toHaveLength(1);
+ expect(page.frames()).toHaveLength(2);
+ });
+
+ it('should wait for inner OOPIFs', async () => {
+ const {server, page, context} = state;
+ await page.goto(`http://mainframe:${server.PORT}/main-frame.html`);
+ const frame2 = await page.waitForFrame(frame => {
+ return frame.url().endsWith('inner-frame2.html');
+ });
+ expect(oopifs(context)).toHaveLength(2);
+ expect(
+ page.frames().filter(frame => {
+ return frame.isOOPFrame();
+ })
+ ).toHaveLength(2);
+ expect(
+ await frame2.evaluate(() => {
+ return document.querySelectorAll('button').length;
+ })
+ ).toStrictEqual(1);
+ });
+
+ it('should load oopif iframes with subresources and request interception', async () => {
+ const {server, page, context} = state;
+
+ const framePromise = page.waitForFrame(frame => {
+ return frame.url().endsWith('/oopif.html');
+ });
+ page.on('request', request => {
+ void request.continue();
+ });
+ await page.setRequestInterception(true);
+ const requestPromise = page.waitForRequest(request => {
+ return request.url().includes('requestFromOOPIF');
+ });
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ const frame = await framePromise;
+ const request = await requestPromise;
+ expect(oopifs(context)).toHaveLength(1);
+ expect(request.frame()).toBe(frame);
+ });
+
+ it('should support frames within OOP iframes', async () => {
+ const {server, page} = state;
+
+ const oopIframePromise = page.waitForFrame(frame => {
+ return frame.url().endsWith('/oopif.html');
+ });
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ const oopIframe = await oopIframePromise;
+ await attachFrame(
+ oopIframe,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+
+ const frame1 = oopIframe.childFrames()[0]!;
+ expect(frame1.url()).toMatch(/empty.html$/);
+ await navigateFrame(
+ oopIframe,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/oopif.html'
+ );
+ expect(frame1.url()).toMatch(/oopif.html$/);
+ await frame1.goto(
+ server.CROSS_PROCESS_PREFIX + '/oopif.html#navigate-within-document',
+ {waitUntil: 'load'}
+ );
+ expect(frame1.url()).toMatch(/oopif.html#navigate-within-document$/);
+ await detachFrame(oopIframe, 'frame1');
+ expect(oopIframe.childFrames()).toHaveLength(0);
+ });
+
+ it('clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs', async () => {
+ const {server, page} = state;
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ const frame = await framePromise;
+ await page.evaluate(() => {
+ document.body.style.border = '50px solid black';
+ document.body.style.margin = '50px';
+ document.body.style.padding = '50px';
+ });
+ await frame.evaluate(() => {
+ const button = document.createElement('button');
+ button.id = 'test-button';
+ button.innerText = 'click';
+ document.body.appendChild(button);
+ });
+ using button = (await frame.waitForSelector('#test-button', {
+ visible: true,
+ }))!;
+ const result = await button.clickablePoint();
+ expect(result.x).toBeGreaterThan(150); // padding + margin + border left
+ expect(result.y).toBeGreaterThan(150); // padding + margin + border top
+ const resultBoxModel = (await button.boxModel())!;
+ for (const quad of [
+ resultBoxModel.content,
+ resultBoxModel.border,
+ resultBoxModel.margin,
+ resultBoxModel.padding,
+ ]) {
+ for (const part of quad) {
+ expect(part.x).toBeGreaterThan(150); // padding + margin + border left
+ expect(part.y).toBeGreaterThan(150); // padding + margin + border top
+ }
+ }
+ const resultBoundingBox = (await button.boundingBox())!;
+ expect(resultBoundingBox.x).toBeGreaterThan(150); // padding + margin + border left
+ expect(resultBoundingBox.y).toBeGreaterThan(150); // padding + margin + border top
+ });
+
+ it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => {
+ const {server, puppeteer, page, context} = state;
+
+ const frame = page.waitForFrame(frame => {
+ return frame.url().endsWith('/oopif.html');
+ });
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ await frame;
+ expect(oopifs(context)).toHaveLength(1);
+ expect(page.frames()).toHaveLength(2);
+
+ const browserURL = 'http://127.0.0.1:21222';
+ const browser1 = await puppeteer.connect({browserURL});
+ const target = await browser1.waitForTarget(target => {
+ return target.url().endsWith('dynamic-oopif.html');
+ });
+ await target.page();
+ await browser1.disconnect();
+ });
+
+ it('should support lazy OOP frames', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.PREFIX + '/lazy-oopif-frame.html');
+ await page.setViewport({width: 1000, height: 1000});
+
+ expect(
+ page.frames().map(frame => {
+ return frame._hasStartedLoading;
+ })
+ ).toEqual([true, true, false]);
+ });
+
+ describe('waitForFrame', () => {
+ it('should resolve immediately if the frame already exists', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(
+ page,
+ 'frame2',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+
+ await page.waitForFrame(frame => {
+ return frame.url().endsWith('/empty.html');
+ });
+ });
+ });
+
+ it('should report google.com frame', async () => {
+ const {server, page} = state;
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ page.on('request', r => {
+ return r.respond({body: 'YO, GOOGLE.COM'});
+ });
+ await page.evaluate(() => {
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', 'https://google.com/');
+ document.body.appendChild(frame);
+ return new Promise(x => {
+ return (frame.onload = x);
+ });
+ });
+ await page.waitForSelector('iframe[src="https://google.com/"]');
+ const urls = page
+ .frames()
+ .map(frame => {
+ return frame.url();
+ })
+ .sort();
+ expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']);
+ });
+
+ it('should expose events within OOPIFs', async () => {
+ const {server, page} = state;
+
+ // Setup our session listeners to observe OOPIF activity.
+ const session = await page.target().createCDPSession();
+ const networkEvents: string[] = [];
+ const otherSessions: CDPSession[] = [];
+ await session.send('Target.setAutoAttach', {
+ autoAttach: true,
+ flatten: true,
+ waitForDebuggerOnStart: true,
+ });
+ session.on(CDPSessionEvent.SessionAttached, async session => {
+ otherSessions.push(session);
+
+ session.on('Network.requestWillBeSent', params => {
+ return networkEvents.push(params.request.url);
+ });
+ await session.send('Network.enable');
+ await session.send('Runtime.runIfWaitingForDebugger');
+ });
+
+ // Navigate to the empty page and add an OOPIF iframe with at least one request.
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(
+ (frameUrl: string) => {
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', frameUrl);
+ document.body.appendChild(frame);
+ return new Promise((x, y) => {
+ frame.onload = x;
+ frame.onerror = y;
+ });
+ },
+ server.PREFIX.replace('localhost', 'oopifdomain') + '/one-style.html'
+ );
+ await page.waitForSelector('iframe');
+
+ // Ensure we found the iframe session.
+ expect(otherSessions).toHaveLength(1);
+
+ // Resume the iframe and trigger another request.
+ const iframeSession = otherSessions[0]!;
+ await iframeSession.send('Runtime.evaluate', {
+ expression: `fetch('/fetch')`,
+ awaitPromise: true,
+ });
+
+ expect(networkEvents).toContain(`http://oopifdomain:${server.PORT}/fetch`);
+ });
+});
+
+function oopifs(context: BrowserContext) {
+ return context.targets().filter(target => {
+ return (target as CdpTarget)._getTargetInfo().type === 'iframe';
+ });
+}
diff --git a/remote/test/puppeteer/test/src/page.spec.ts b/remote/test/puppeteer/test/src/page.spec.ts
new file mode 100644
index 0000000000..79fc69ebbc
--- /dev/null
+++ b/remote/test/puppeteer/test/src/page.spec.ts
@@ -0,0 +1,2287 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert';
+import fs from 'fs';
+import type {ServerResponse} from 'http';
+import path from 'path';
+
+import expect from 'expect';
+import {KnownDevices, TimeoutError} from 'puppeteer';
+import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
+import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {Metrics, Page} from 'puppeteer-core/internal/api/Page.js';
+import type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js';
+import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js';
+import sinon from 'sinon';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame, detachFrame, isFavicon, waitEvent} from './utils.js';
+
+describe('Page', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.close', function () {
+ it('should reject all promises when page is closed', async () => {
+ const {context} = await getTestState();
+
+ const newPage = await context.newPage();
+ let error!: Error;
+ await Promise.all([
+ newPage
+ .evaluate(() => {
+ return new Promise(() => {});
+ })
+ .catch(error_ => {
+ return (error = error_);
+ }),
+ newPage.close(),
+ ]);
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should not be visible in browser.pages', async () => {
+ const {browser} = await getTestState();
+
+ const newPage = await browser.newPage();
+ expect(await browser.pages()).toContain(newPage);
+ await newPage.close();
+ expect(await browser.pages()).not.toContain(newPage);
+ });
+ it('should run beforeunload if asked for', async () => {
+ const {context, server, isChrome} = await getTestState();
+
+ const newPage = await context.newPage();
+ await newPage.goto(server.PREFIX + '/beforeunload.html');
+ // We have to interact with a page so that 'beforeunload' handlers
+ // fire.
+ await newPage.click('body');
+ const pageClosingPromise = newPage.close({runBeforeUnload: true});
+ const dialog = await waitEvent(newPage, 'dialog');
+ expect(dialog.type()).toBe('beforeunload');
+ expect(dialog.defaultValue()).toBe('');
+ if (isChrome) {
+ expect(dialog.message()).toBe('');
+ } else {
+ expect(dialog.message()).toBeTruthy();
+ }
+ await dialog.accept();
+ await pageClosingPromise;
+ });
+ it('should *not* run beforeunload by default', async () => {
+ const {context, server} = await getTestState();
+
+ const newPage = await context.newPage();
+ await newPage.goto(server.PREFIX + '/beforeunload.html');
+ // We have to interact with a page so that 'beforeunload' handlers
+ // fire.
+ await newPage.click('body');
+ await newPage.close();
+ });
+ it('should set the page close state', async () => {
+ const {context} = await getTestState();
+
+ const newPage = await context.newPage();
+ expect(newPage.isClosed()).toBe(false);
+ await newPage.close();
+ expect(newPage.isClosed()).toBe(true);
+ });
+ it('should terminate network waiters', async () => {
+ const {context, server} = await getTestState();
+
+ const newPage = await context.newPage();
+ const results = await Promise.all([
+ newPage.waitForRequest(server.EMPTY_PAGE).catch(error => {
+ return error;
+ }),
+ newPage.waitForResponse(server.EMPTY_PAGE).catch(error => {
+ return error;
+ }),
+ newPage.close(),
+ ]);
+ for (let i = 0; i < 2; i++) {
+ const message = results[i].message;
+ expect(message).atLeastOneToContain(['Target closed', 'Page closed!']);
+ expect(message).not.toContain('Timeout');
+ }
+ });
+ });
+
+ describe('Page.Events.Load', function () {
+ it('should fire when expected', async () => {
+ const {page} = await getTestState();
+
+ await Promise.all([waitEvent(page, 'load'), page.goto('about:blank')]);
+ });
+ });
+
+ describe('removing and adding event handlers', () => {
+ it('should correctly fire event handlers as they are added and then removed', async () => {
+ const {page, server} = await getTestState();
+
+ const handler = sinon.spy();
+ const onResponse = (response: {url: () => string}) => {
+ // Ignore default favicon requests.
+ if (!isFavicon(response)) {
+ handler();
+ }
+ };
+ page.on('response', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ expect(handler.callCount).toBe(1);
+ page.off('response', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ // Still one because we removed the handler.
+ expect(handler.callCount).toBe(1);
+ page.on('response', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ // Two now because we added the handler back.
+ expect(handler.callCount).toBe(2);
+ });
+
+ it('should correctly added and removed request events', async () => {
+ const {page, server} = await getTestState();
+
+ const handler = sinon.spy();
+ const onResponse = (response: {url: () => string}) => {
+ // Ignore default favicon requests.
+ if (!isFavicon(response)) {
+ handler();
+ }
+ };
+
+ page.on('request', onResponse);
+ page.on('request', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ expect(handler.callCount).toBe(2);
+ page.off('request', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ // Still one because we removed the handler.
+ expect(handler.callCount).toBe(3);
+ page.off('request', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ expect(handler.callCount).toBe(3);
+ page.on('request', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ // Two now because we added the handler back.
+ expect(handler.callCount).toBe(4);
+ });
+ });
+
+ describe('Page.Events.error', function () {
+ it('should throw when page crashes', async () => {
+ const {page, isChrome} = await getTestState();
+
+ let navigate: Promise<unknown>;
+ if (isChrome) {
+ navigate = page.goto('chrome://crash').catch(() => {});
+ } else {
+ navigate = page.goto('about:crashcontent').catch(() => {});
+ }
+ const [error] = await Promise.all([
+ waitEvent<Error>(page, 'error'),
+ navigate,
+ ]);
+ expect(error.message).toBe('Page crashed!');
+ });
+ });
+
+ describe('Page.Events.Popup', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.evaluate(() => {
+ return window.open('about:blank');
+ }),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(true);
+ });
+ it('should work with noopener', async () => {
+ const {page} = await getTestState();
+
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.evaluate(() => {
+ return window.open('about:blank', undefined, 'noopener');
+ }),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ });
+ it('should work with clicking target=_blank and without rel=opener', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent('<a target=_blank href="/one-style.html">yo</a>');
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.click('a'),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ });
+ it('should work with clicking target=_blank and with rel=opener', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(
+ '<a target=_blank rel=opener href="/one-style.html">yo</a>'
+ );
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.click('a'),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(true);
+ });
+ it('should work with fake-clicking target=_blank and rel=noopener', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(
+ '<a target=_blank rel=noopener href="/one-style.html">yo</a>'
+ );
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.$eval('a', a => {
+ return (a as HTMLAnchorElement).click();
+ }),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ });
+ it('should work with clicking target=_blank and rel=noopener', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(
+ '<a target=_blank rel=noopener href="/one-style.html">yo</a>'
+ );
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.click('a'),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ });
+ });
+
+ describe('Page.setGeolocation', function () {
+ it('should work', async () => {
+ const {page, server, context} = await getTestState();
+
+ await context.overridePermissions(server.PREFIX, ['geolocation']);
+ await page.goto(server.EMPTY_PAGE);
+ await page.setGeolocation({longitude: 10, latitude: 10});
+ const geolocation = await page.evaluate(() => {
+ return new Promise(resolve => {
+ return navigator.geolocation.getCurrentPosition(position => {
+ resolve({
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude,
+ });
+ });
+ });
+ });
+ expect(geolocation).toEqual({
+ latitude: 10,
+ longitude: 10,
+ });
+ });
+ it('should throw when invalid longitude', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ await page.setGeolocation({longitude: 200, latitude: 10});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain('Invalid longitude "200"');
+ });
+ });
+
+ describe('Page.setOfflineMode', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setOfflineMode(true);
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ await page.setOfflineMode(false);
+ const response = (await page.reload())!;
+ expect(response.status()).toBe(200);
+ });
+ it('should emulate navigator.onLine', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return window.navigator.onLine;
+ })
+ ).toBe(true);
+ await page.setOfflineMode(true);
+ expect(
+ await page.evaluate(() => {
+ return window.navigator.onLine;
+ })
+ ).toBe(false);
+ await page.setOfflineMode(false);
+ expect(
+ await page.evaluate(() => {
+ return window.navigator.onLine;
+ })
+ ).toBe(true);
+ });
+ });
+
+ describe('Page.Events.Console', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.evaluate(() => {
+ return console.log('hello', 5, {foo: 'bar'});
+ }),
+ ]);
+ expect(message.text()).toEqual('hello 5 JSHandle@object');
+ expect(message.type()).toEqual('log');
+ expect(message.args()).toHaveLength(3);
+ expect(message.location()).toEqual({
+ url: expect.any(String),
+ lineNumber: expect.any(Number),
+ columnNumber: expect.any(Number),
+ });
+
+ expect(await message.args()[0]!.jsonValue()).toEqual('hello');
+ expect(await message.args()[1]!.jsonValue()).toEqual(5);
+ expect(await message.args()[2]!.jsonValue()).toEqual({foo: 'bar'});
+ });
+ it('should work on script call right after navigation', async () => {
+ const {page} = await getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.goto(
+ // Firefox prints warn if <!DOCTYPE html> is not present
+ `data:text/html,<!DOCTYPE html><script>console.log('SOME_LOG_MESSAGE');</script>`
+ ),
+ ]);
+
+ expect(message.text()).toEqual('SOME_LOG_MESSAGE');
+ });
+ it('should work for different console API calls with logging functions', async () => {
+ const {page} = await getTestState();
+
+ const messages: ConsoleMessage[] = [];
+ page.on('console', msg => {
+ return messages.push(msg);
+ });
+ // All console events will be reported before `page.evaluate` is finished.
+ await page.evaluate(() => {
+ console.trace('calling console.trace');
+ console.dir('calling console.dir');
+ console.warn('calling console.warn');
+ console.error('calling console.error');
+ console.log(Promise.resolve('should not wait until resolved!'));
+ });
+ expect(
+ messages.map(msg => {
+ return msg.type();
+ })
+ ).toEqual(['trace', 'dir', 'warning', 'error', 'log']);
+ expect(
+ messages.map(msg => {
+ return msg.text();
+ })
+ ).toEqual([
+ 'calling console.trace',
+ 'calling console.dir',
+ 'calling console.warn',
+ 'calling console.error',
+ 'JSHandle@promise',
+ ]);
+ });
+ it('should work for different console API calls with timing functions', async () => {
+ const {page} = await getTestState();
+
+ const messages: any[] = [];
+ page.on('console', msg => {
+ return messages.push(msg);
+ });
+ // All console events will be reported before `page.evaluate` is finished.
+ await page.evaluate(() => {
+ // A pair of time/timeEnd generates only one Console API call.
+ console.time('calling console.time');
+ console.timeEnd('calling console.time');
+ });
+ expect(
+ messages.map(msg => {
+ return msg.type();
+ })
+ ).toEqual(['timeEnd']);
+ expect(messages[0]!.text()).toContain('calling console.time');
+ });
+ it('should not fail for window object', async () => {
+ const {page} = await getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.evaluate(() => {
+ return console.error(window);
+ }),
+ ]);
+ expect(message.text()).atLeastOneToContain([
+ 'JSHandle@object',
+ 'JSHandle@window',
+ ]);
+ });
+ it('should trigger correct Log', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.goto('about:blank');
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.evaluate(async (url: string) => {
+ return await fetch(url).catch(() => {});
+ }, server.EMPTY_PAGE),
+ ]);
+ expect(message.text()).toContain('Access-Control-Allow-Origin');
+ if (isChrome) {
+ expect(message.type()).toEqual('error');
+ } else {
+ expect(message.type()).toEqual('warn');
+ }
+ });
+ it('should have location when fetch fails', async () => {
+ const {page, server} = await getTestState();
+
+ // The point of this test is to make sure that we report console messages from
+ // Log domain: https://vanilla.aslushnikov.com/?Log.entryAdded
+ await page.goto(server.EMPTY_PAGE);
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.setContent(`<script>fetch('http://wat');</script>`),
+ ]);
+ expect(message.text()).toContain(`ERR_NAME_NOT_RESOLVED`);
+ expect(message.type()).toEqual('error');
+ expect(message.location()).toEqual({
+ url: 'http://wat/',
+ lineNumber: undefined,
+ });
+ });
+ it('should have location and stack trace for console API calls', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.goto(server.PREFIX + '/consolelog.html'),
+ ]);
+ expect(message.text()).toBe('yellow');
+ expect(message.type()).toBe('log');
+ expect(message.location()).toEqual({
+ url: server.PREFIX + '/consolelog.html',
+ lineNumber: 8,
+ columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log
+ });
+ expect(message.stackTrace()).toEqual([
+ {
+ url: server.PREFIX + '/consolelog.html',
+ lineNumber: 8,
+ columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log
+ },
+ {
+ url: server.PREFIX + '/consolelog.html',
+ lineNumber: 11,
+ columnNumber: 8,
+ },
+ {
+ url: server.PREFIX + '/consolelog.html',
+ lineNumber: 13,
+ columnNumber: 6,
+ },
+ ]);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3865
+ it('should not throw when there are console messages in detached iframes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(async () => {
+ // 1. Create a popup that Puppeteer is not connected to.
+ const win = window.open(
+ window.location.href,
+ 'Title',
+ 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=200,top=0,left=0'
+ )!;
+ await new Promise(x => {
+ return (win.onload = x);
+ });
+ // 2. In this popup, create an iframe that console.logs a message.
+ win.document.body.innerHTML = `<iframe src='/consolelog.html'></iframe>`;
+ const frame = win.document.querySelector('iframe')!;
+ await new Promise(x => {
+ return (frame.onload = x);
+ });
+ // 3. After that, remove the iframe.
+ frame.remove();
+ });
+ const popupTarget = page
+ .browserContext()
+ .targets()
+ .find(target => {
+ return target !== page.target();
+ })!;
+ // 4. Connect to the popup and make sure it doesn't throw.
+ await popupTarget.page();
+ });
+ });
+
+ describe('Page.Events.DOMContentLoaded', function () {
+ it('should fire when expected', async () => {
+ const {page} = await getTestState();
+
+ const navigate = page.goto('about:blank');
+ await Promise.all([waitEvent(page, 'domcontentloaded'), navigate]);
+ });
+ });
+
+ describe('Page.metrics', function () {
+ it('should get metrics from a page', async () => {
+ const {page} = await getTestState();
+
+ await page.goto('about:blank');
+ const metrics = await page.metrics();
+ checkMetrics(metrics);
+ });
+ it('metrics event fired on console.timeStamp', async () => {
+ const {page} = await getTestState();
+
+ const metricsPromise = waitEvent<{metrics: Metrics; title: string}>(
+ page,
+ 'metrics'
+ );
+
+ await page.evaluate(() => {
+ return console.timeStamp('test42');
+ });
+ const metrics = await metricsPromise;
+ expect(metrics.title).toBe('test42');
+ checkMetrics(metrics.metrics);
+ });
+ function checkMetrics(metrics: Metrics) {
+ const metricsToCheck = new Set([
+ 'Timestamp',
+ 'Documents',
+ 'Frames',
+ 'JSEventListeners',
+ 'Nodes',
+ 'LayoutCount',
+ 'RecalcStyleCount',
+ 'LayoutDuration',
+ 'RecalcStyleDuration',
+ 'ScriptDuration',
+ 'TaskDuration',
+ 'JSHeapUsedSize',
+ 'JSHeapTotalSize',
+ ]);
+ for (const name in metrics) {
+ expect(metricsToCheck.has(name)).toBeTruthy();
+ expect(metrics[name as keyof Metrics]).toBeGreaterThanOrEqual(0);
+ metricsToCheck.delete(name);
+ }
+ expect(metricsToCheck.size).toBe(0);
+ }
+ });
+
+ describe('Page.waitForRequest', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(server.PREFIX + '/digits/2.png'),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with predicate', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(request => {
+ return request.url() === server.PREFIX + '/digits/2.png';
+ }),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with async predicate', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(async request => {
+ return request.url() === server.PREFIX + '/digits/2.png';
+ }),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .waitForRequest(
+ () => {
+ return false;
+ },
+ {timeout: 1}
+ )
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect default timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ page.setDefaultTimeout(1);
+ await page
+ .waitForRequest(() => {
+ return false;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should work with no timeout', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(server.PREFIX + '/digits/2.png', {timeout: 0}),
+ page.evaluate(() => {
+ return setTimeout(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }, 50);
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ });
+
+ describe('Page.waitForResponse', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(server.PREFIX + '/digits/2.png'),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .waitForResponse(
+ () => {
+ return false;
+ },
+ {timeout: 1}
+ )
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect default timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ page.setDefaultTimeout(1);
+ await page
+ .waitForResponse(() => {
+ return false;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should work with predicate', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(response => {
+ return response.url() === server.PREFIX + '/digits/2.png';
+ }),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with async predicate', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(async response => {
+ return response.url() === server.PREFIX + '/digits/2.png';
+ }),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with no timeout', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(server.PREFIX + '/digits/2.png', {timeout: 0}),
+ page.evaluate(() => {
+ return setTimeout(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }, 50);
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ });
+
+ describe('Page.waitForNetworkIdle', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ let res;
+ const [t1, t2] = await Promise.all([
+ page.waitForNetworkIdle().then(r => {
+ res = r;
+ return Date.now();
+ }),
+ page
+ .evaluate(async () => {
+ await Promise.all([fetch('/digits/1.png'), fetch('/digits/2.png')]);
+ await new Promise(resolve => {
+ return setTimeout(resolve, 200);
+ });
+ await fetch('/digits/3.png');
+ await new Promise(resolve => {
+ return setTimeout(resolve, 200);
+ });
+ await fetch('/digits/4.png');
+ })
+ .then(() => {
+ return Date.now();
+ }),
+ ]);
+ expect(res).toBe(undefined);
+ expect(t1).toBeGreaterThan(t2);
+ expect(t1 - t2).toBeGreaterThanOrEqual(400);
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+ let error!: Error;
+ await page.waitForNetworkIdle({timeout: 1}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect idleTime', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const [t1, t2] = await Promise.all([
+ page.waitForNetworkIdle({idleTime: 10}).then(() => {
+ return Date.now();
+ }),
+ page
+ .evaluate(() => {
+ return (async () => {
+ await Promise.all([
+ fetch('/digits/1.png'),
+ fetch('/digits/2.png'),
+ ]);
+ await new Promise(resolve => {
+ return setTimeout(resolve, 250);
+ });
+ })();
+ })
+ .then(() => {
+ return Date.now();
+ }),
+ ]);
+ expect(t2).toBeGreaterThan(t1);
+ });
+ it('should work with no timeout', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const [result] = await Promise.all([
+ page.waitForNetworkIdle({timeout: 0}),
+ page.evaluate(() => {
+ return setTimeout(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }, 50);
+ }),
+ ]);
+ expect(result).toBe(undefined);
+ });
+ it('should work with aborted requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/abort-request.html');
+
+ using element = await page.$(`#abort`);
+ await element!.click();
+
+ let error = false;
+ await page.waitForNetworkIdle().catch(() => {
+ return (error = true);
+ });
+
+ expect(error).toBe(false);
+ });
+ it('should work with delayed response', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ let response!: ServerResponse;
+ server.setRoute('/fetch-request-b.js', (_req, res) => {
+ response = res;
+ });
+ const t0 = Date.now();
+ const [t1, t2] = await Promise.all([
+ page.waitForNetworkIdle({idleTime: 100}).then(() => {
+ return Date.now();
+ }),
+ new Promise<number>(res => {
+ setTimeout(() => {
+ response.end();
+ res(Date.now());
+ }, 300);
+ }),
+ page.evaluate(async () => {
+ await fetch('/fetch-request-b.js');
+ }),
+ ]);
+ expect(t1).toBeGreaterThan(t2);
+ // request finished + idle time.
+ expect(t1 - t0).toBeGreaterThan(400);
+ // request finished + idle time - request finished.
+ expect(t1 - t2).toBeGreaterThanOrEqual(100);
+ });
+ });
+
+ describe('Page.exposeFunction', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return a * b;
+ });
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ });
+ expect(result).toBe(36);
+ });
+ it('should throw exception in page context', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('woof', () => {
+ throw new Error('WOOF WOOF');
+ });
+ const {message, stack} = await page.evaluate(async () => {
+ try {
+ return await (
+ globalThis as unknown as {woof(): Promise<never>}
+ ).woof();
+ } catch (error) {
+ return {
+ message: (error as Error).message,
+ stack: (error as Error).stack,
+ };
+ }
+ });
+ expect(message).toBe('WOOF WOOF');
+ expect(stack).toContain('page.spec.ts');
+ });
+ it('should support throwing "null"', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('woof', function () {
+ throw null;
+ });
+ const thrown = await page.evaluate(async () => {
+ try {
+ await (globalThis as any).woof();
+ return;
+ } catch (error) {
+ return error;
+ }
+ });
+ expect(thrown).toBe(null);
+ });
+ it('should be callable from-inside evaluateOnNewDocument', async () => {
+ const {page} = await getTestState();
+
+ let called = false;
+ await page.exposeFunction('woof', function () {
+ called = true;
+ });
+ await page.evaluateOnNewDocument(() => {
+ return (globalThis as any).woof();
+ });
+ await page.reload();
+ expect(called).toBe(true);
+ });
+ it('should survive navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return a * b;
+ });
+
+ await page.goto(server.EMPTY_PAGE);
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ });
+ expect(result).toBe(36);
+ });
+ it('should await returned promise', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should await returned if called from function', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+
+ const result = await page.evaluate(async function () {
+ const result = await (globalThis as any).compute(3, 5);
+ return result;
+ });
+ expect(result).toBe(15);
+ });
+ it('should work on frames', async () => {
+ const {page, server} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ const frame = page.frames()[1]!;
+ const result = await frame.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should work with loading frames', async () => {
+ // Tries to reproduce the scenario from
+ // https://github.com/puppeteer/puppeteer/issues/8106
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ let saveRequest: (value: HTTPRequest | PromiseLike<HTTPRequest>) => void;
+ const iframeRequest = new Promise<HTTPRequest>(resolve => {
+ saveRequest = resolve;
+ });
+ page.on('request', async req => {
+ if (req.url().endsWith('/frames/frame.html')) {
+ saveRequest(req);
+ } else {
+ await req.continue();
+ }
+ });
+
+ let error: Error | undefined;
+ const navPromise = page
+ .goto(server.PREFIX + '/frames/one-frame.html', {
+ waitUntil: 'networkidle0',
+ })
+ .catch(err => {
+ error = err;
+ });
+ const req = await iframeRequest;
+ // Expose function while the frame is being loaded. Loading process is
+ // controlled by interception.
+ const exposePromise = page.exposeFunction(
+ 'compute',
+ function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ }
+ );
+ await Promise.all([req.continue(), exposePromise]);
+ await navPromise;
+ expect(error).toBeUndefined();
+ const frame = page.frames()[1]!;
+ const result = await frame.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should work on frames before navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+
+ const frame = page.frames()[1]!;
+ const result = await frame.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should not throw when frames detach', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+ await detachFrame(page, 'frame1');
+
+ await expect(
+ page.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ })
+ ).resolves.toEqual(15);
+ });
+ it('should work with complex objects', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction(
+ 'complexObject',
+ function (a: {x: number}, b: {x: number}) {
+ return {x: a.x + b.x};
+ }
+ );
+ const result = await page.evaluate(async () => {
+ return (globalThis as any).complexObject({x: 5}, {x: 2});
+ });
+ expect(result.x).toBe(7);
+ });
+ it('should fallback to default export when passed a module object', async () => {
+ const {page, server} = await getTestState();
+ const moduleObject = {
+ default: function (a: number, b: number) {
+ return a * b;
+ },
+ };
+ await page.goto(server.EMPTY_PAGE);
+ await page.exposeFunction('compute', moduleObject);
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ });
+ expect(result).toBe(36);
+ });
+ });
+
+ describe('Page.removeExposedFunction', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return a * b;
+ });
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ });
+ expect(result).toBe(36);
+ await page.removeExposedFunction('compute');
+
+ let error: Error | null = null;
+ await page
+ .evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ })
+ .catch(_error => {
+ return (error = _error);
+ });
+ expect(error).toBeTruthy();
+ });
+ });
+
+ describe('Page.Events.PageError', function () {
+ it('should fire', async () => {
+ const {page, server} = await getTestState();
+
+ const [error] = await Promise.all([
+ waitEvent<Error>(page, 'pageerror', err => {
+ return err.message.includes('Fancy');
+ }),
+ page.goto(server.PREFIX + '/error.html'),
+ ]);
+ expect(error.message).toContain('Fancy');
+ expect(error.stack?.split('\n')[1]).toContain('error.html:13');
+ });
+ });
+
+ describe('Page.setUserAgent', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).toContain('Mozilla');
+ await page.setUserAgent('foobar');
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(request.headers['user-agent']).toBe('foobar');
+ });
+ it('should work for subframes', async () => {
+ const {page, server} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).toContain('Mozilla');
+ await page.setUserAgent('foobar');
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ attachFrame(page, 'frame1', server.EMPTY_PAGE),
+ ]);
+ expect(request.headers['user-agent']).toBe('foobar');
+ });
+ it('should emulate device user-agent', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).not.toContain('iPhone');
+ await page.setUserAgent(KnownDevices['iPhone 6'].userAgent);
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).toContain('iPhone');
+ });
+ it('should work with additional userAgentMetdata', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setUserAgent('MockBrowser', {
+ architecture: 'Mock1',
+ mobile: false,
+ model: 'Mockbook',
+ platform: 'MockOS',
+ platformVersion: '3.1',
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error: userAgentData not yet in TypeScript DOM API
+ return navigator.userAgentData.mobile;
+ })
+ ).toBe(false);
+
+ const uaData = await page.evaluate(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error: userAgentData not yet in TypeScript DOM API
+ return navigator.userAgentData.getHighEntropyValues([
+ 'architecture',
+ 'model',
+ 'platform',
+ 'platformVersion',
+ ]);
+ });
+ expect(uaData['architecture']).toBe('Mock1');
+ expect(uaData['model']).toBe('Mockbook');
+ expect(uaData['platform']).toBe('MockOS');
+ expect(uaData['platformVersion']).toBe('3.1');
+ expect(request.headers['user-agent']).toBe('MockBrowser');
+ });
+ });
+
+ describe('Page.setContent', function () {
+ const expectedOutput =
+ '<html><head></head><body><div>hello</div></body></html>';
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>hello</div>');
+ const result = await page.content();
+ expect(result).toBe(expectedOutput);
+ });
+ it('should work with doctype', async () => {
+ const {page} = await getTestState();
+
+ const doctype = '<!DOCTYPE html>';
+ await page.setContent(`${doctype}<div>hello</div>`);
+ const result = await page.content();
+ expect(result).toBe(`${doctype}${expectedOutput}`);
+ });
+ it('should work with HTML 4 doctype', async () => {
+ const {page} = await getTestState();
+
+ const doctype =
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" ' +
+ '"http://www.w3.org/TR/html4/strict.dtd">';
+ await page.setContent(`${doctype}<div>hello</div>`);
+ const result = await page.content();
+ expect(result).toBe(`${doctype}${expectedOutput}`);
+ });
+ it('should respect timeout', async () => {
+ const {page, server} = await getTestState();
+
+ const imgPath = '/img.png';
+ // stall for image
+ server.setRoute(imgPath, () => {});
+ let error!: Error;
+ await page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`, {
+ timeout: 1,
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect default navigation timeout', async () => {
+ const {page, server} = await getTestState();
+
+ page.setDefaultNavigationTimeout(1);
+ const imgPath = '/img.png';
+ // stall for image
+ server.setRoute(imgPath, () => {});
+ let error!: Error;
+ await page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`)
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should await resources to load', async () => {
+ const {page, server} = await getTestState();
+
+ const imgPath = '/img.png';
+ let imgResponse!: ServerResponse;
+ server.setRoute(imgPath, (_req, res) => {
+ return (imgResponse = res);
+ });
+ let loaded = false;
+ const contentPromise = page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`)
+ .then(() => {
+ return (loaded = true);
+ });
+ await server.waitForRequest(imgPath);
+ expect(loaded).toBe(false);
+ imgResponse.end();
+ await contentPromise;
+ });
+ it('should work fast enough', async () => {
+ const {page} = await getTestState();
+
+ for (let i = 0; i < 20; ++i) {
+ await page.setContent('<div>yo</div>');
+ }
+ });
+ it('should work with tricky content', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>hello world</div>' + '\x7F');
+ expect(
+ await page.$eval('div', div => {
+ return div.textContent;
+ })
+ ).toBe('hello world');
+ });
+ it('should work with accents', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>aberración</div>');
+ expect(
+ await page.$eval('div', div => {
+ return div.textContent;
+ })
+ ).toBe('aberración');
+ });
+ it('should work with emojis', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>🐥</div>');
+ expect(
+ await page.$eval('div', div => {
+ return div.textContent;
+ })
+ ).toBe('🐥');
+ });
+ it('should work with newline', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>\n</div>');
+ expect(
+ await page.$eval('div', div => {
+ return div.textContent;
+ })
+ ).toBe('\n');
+ });
+ it('should work with comments outside HTML tag', async () => {
+ const {page} = await getTestState();
+
+ const comment = '<!-- Comment -->';
+ await page.setContent(`${comment}<div>hello</div>`);
+ const result = await page.content();
+ expect(result).toBe(`${comment}${expectedOutput}`);
+ });
+ });
+
+ describe('Page.setBypassCSP', function () {
+ it('should bypass CSP meta tag', async () => {
+ const {page, server} = await getTestState();
+
+ // Make sure CSP prohibits addScriptTag.
+ await page.goto(server.PREFIX + '/csp.html');
+ await page
+ .addScriptTag({content: 'window.__injected = 42;'})
+ .catch(error => {
+ return void error;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(undefined);
+
+ // By-pass CSP and try one more time.
+ await page.setBypassCSP(true);
+ await page.reload();
+ await page.addScriptTag({content: 'window.__injected = 42;'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should bypass CSP header', async () => {
+ const {page, server} = await getTestState();
+
+ // Make sure CSP prohibits addScriptTag.
+ server.setCSP('/empty.html', 'default-src "self"');
+ await page.goto(server.EMPTY_PAGE);
+ await page
+ .addScriptTag({content: 'window.__injected = 42;'})
+ .catch(error => {
+ return void error;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(undefined);
+
+ // By-pass CSP and try one more time.
+ await page.setBypassCSP(true);
+ await page.reload();
+ await page.addScriptTag({content: 'window.__injected = 42;'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should bypass after cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setBypassCSP(true);
+ await page.goto(server.PREFIX + '/csp.html');
+ await page.addScriptTag({content: 'window.__injected = 42;'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html');
+ await page.addScriptTag({content: 'window.__injected = 42;'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+ it('should bypass CSP in iframes as well', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ {
+ // Make sure CSP prohibits addScriptTag in an iframe.
+ const frame = (await attachFrame(
+ page,
+ 'frame1',
+ server.PREFIX + '/csp.html'
+ ))!;
+ await frame
+ .addScriptTag({content: 'window.__injected = 42;'})
+ .catch(error => {
+ return void error;
+ });
+ expect(
+ await frame.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(undefined);
+ }
+
+ // By-pass CSP and try one more time.
+ await page.setBypassCSP(true);
+ await page.reload();
+
+ {
+ const frame = (await attachFrame(
+ page,
+ 'frame1',
+ server.PREFIX + '/csp.html'
+ ))!;
+ await frame
+ .addScriptTag({content: 'window.__injected = 42;'})
+ .catch(error => {
+ return void error;
+ });
+ expect(
+ await frame.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ }
+ });
+ });
+
+ describe('Page.addScriptTag', function () {
+ it('should throw an error if no options are provided', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ // @ts-expect-error purposefully passing bad options
+ await page.addScriptTag('/injectedfile.js');
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toBe(
+ 'Exactly one of `url`, `path`, or `content` must be specified.'
+ );
+ });
+
+ it('should work with a url', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using scriptHandle = await page.addScriptTag({url: '/injectedfile.js'});
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should work with a url and type=module', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({url: '/es6/es6import.js', type: 'module'});
+ expect(
+ await page.evaluate(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should work with a path and type=module', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({
+ path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
+ type: 'module',
+ });
+ await page.waitForFunction(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should work with a content and type=module', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({
+ content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
+ type: 'module',
+ });
+ await page.waitForFunction(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should throw an error if loading from url fail', async () => {
+ const {page, server, isFirefox} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error!: Error;
+ try {
+ await page.addScriptTag({url: '/nonexistfile.js'});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ if (isFirefox) {
+ expect(error.message).toBeTruthy();
+ } else {
+ expect(error.message).toContain('Could not load script');
+ }
+ });
+
+ it('should work with a path', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using scriptHandle = await page.addScriptTag({
+ path: path.join(__dirname, '../assets/injectedfile.js'),
+ });
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should include sourcemap when path is provided', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({
+ path: path.join(__dirname, '../assets/injectedfile.js'),
+ });
+ const result = await page.evaluate(() => {
+ return (globalThis as any).__injectedError.stack;
+ });
+ expect(result).toContain(path.join('assets', 'injectedfile.js'));
+ });
+
+ it('should work with content', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using scriptHandle = await page.addScriptTag({
+ content: 'window.__injected = 35;',
+ });
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(35);
+ });
+
+ it('should add id when provided', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({content: 'window.__injected = 1;', id: 'one'});
+ await page.addScriptTag({url: '/injectedfile.js', id: 'two'});
+ expect(await page.$('#one')).not.toBeNull();
+ expect(await page.$('#two')).not.toBeNull();
+ });
+
+ // @see https://github.com/puppeteer/puppeteer/issues/4840
+ it('should throw when added with content to the CSP page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error!: Error;
+ await page
+ .addScriptTag({content: 'window.__injected = 35;'})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ });
+
+ it('should throw when added with URL to the CSP page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error!: Error;
+ await page
+ .addScriptTag({url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js'})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ });
+ });
+
+ describe('Page.addStyleTag', function () {
+ it('should throw an error if no options are provided', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ // @ts-expect-error purposefully passing bad input
+ await page.addStyleTag('/injectedstyle.css');
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toBe(
+ 'Exactly one of `url`, `path`, or `content` must be specified.'
+ );
+ });
+
+ it('should work with a url', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using styleHandle = await page.addStyleTag({url: '/injectedstyle.css'});
+ expect(styleHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(
+ `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`
+ )
+ ).toBe('rgb(255, 0, 0)');
+ });
+
+ it('should throw an error if loading from url fail', async () => {
+ const {page, server, isFirefox} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error!: Error;
+ try {
+ await page.addStyleTag({url: '/nonexistfile.js'});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ if (isFirefox) {
+ expect(error.message).toBeTruthy();
+ } else {
+ expect(error.message).toContain('Could not load style');
+ }
+ });
+
+ it('should work with a path', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using styleHandle = await page.addStyleTag({
+ path: path.join(__dirname, '../assets/injectedstyle.css'),
+ });
+ expect(styleHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(
+ `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`
+ )
+ ).toBe('rgb(255, 0, 0)');
+ });
+
+ it('should include sourcemap when path is provided', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addStyleTag({
+ path: path.join(__dirname, '../assets/injectedstyle.css'),
+ });
+ using styleHandle = (await page.$('style'))!;
+ const styleContent = await page.evaluate((style: HTMLStyleElement) => {
+ return style.innerHTML;
+ }, styleHandle);
+ expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
+ });
+
+ it('should work with content', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using styleHandle = await page.addStyleTag({
+ content: 'body { background-color: green; }',
+ });
+ expect(styleHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(
+ `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`
+ )
+ ).toBe('rgb(0, 128, 0)');
+ });
+
+ it('should throw when added with content to the CSP page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error!: Error;
+ await page
+ .addStyleTag({content: 'body { background-color: green; }'})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ });
+
+ it('should throw when added with URL to the CSP page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error!: Error;
+ await page
+ .addStyleTag({
+ url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css',
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ });
+ });
+
+ describe('Page.url', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ expect(page.url()).toBe('about:blank');
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ });
+ });
+
+ describe('Page.setJavaScriptEnabled', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setJavaScriptEnabled(false);
+ await page.goto(
+ 'data:text/html, <script>var something = "forbidden"</script>'
+ );
+ let error!: Error;
+ await page.evaluate('something').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('something is not defined');
+
+ await page.setJavaScriptEnabled(true);
+ await page.goto(
+ 'data:text/html, <script>var something = "forbidden"</script>'
+ );
+ expect(await page.evaluate('something')).toBe('forbidden');
+ });
+ });
+
+ describe('Page.setCacheEnabled', function () {
+ it('should enable or disable the cache based on the state passed', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ const [cachedRequest] = await Promise.all([
+ server.waitForRequest('/cached/one-style.html'),
+ page.reload(),
+ ]);
+ // Rely on "if-modified-since" caching in our test server.
+ expect(cachedRequest.headers['if-modified-since']).not.toBe(undefined);
+
+ await page.setCacheEnabled(false);
+ const [nonCachedRequest] = await Promise.all([
+ server.waitForRequest('/cached/one-style.html'),
+ page.reload(),
+ ]);
+ expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined);
+ });
+ it('should stay disabled when toggling request interception on/off', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setCacheEnabled(false);
+ await page.setRequestInterception(true);
+ await page.setRequestInterception(false);
+
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ const [nonCachedRequest] = await Promise.all([
+ server.waitForRequest('/cached/one-style.html'),
+ page.reload(),
+ ]);
+ expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined);
+ });
+ });
+
+ describe('Page.pdf', function () {
+ it('can print to PDF and save to file', async () => {
+ const {page, server} = await getTestState();
+
+ const outputFile = __dirname + '/../assets/output.pdf';
+ await page.goto(server.PREFIX + '/pdf.html');
+ await page.pdf({path: outputFile});
+ try {
+ expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0);
+ } finally {
+ fs.unlinkSync(outputFile);
+ }
+ });
+
+ it('can print to PDF with accessible', async () => {
+ const {page, server} = await getTestState();
+
+ const outputFile = __dirname + '/../assets/output.pdf';
+ const outputFileAccessible =
+ __dirname + '/../assets/output-accessible.pdf';
+ await page.goto(server.PREFIX + '/pdf.html');
+ await page.pdf({path: outputFile});
+ await page.pdf({path: outputFileAccessible, tagged: true});
+ try {
+ expect(
+ fs.readFileSync(outputFileAccessible).byteLength
+ ).toBeGreaterThan(fs.readFileSync(outputFile).byteLength);
+ } finally {
+ fs.unlinkSync(outputFileAccessible);
+ fs.unlinkSync(outputFile);
+ }
+ });
+
+ it('can print to PDF and stream the result', async () => {
+ const {page} = await getTestState();
+
+ const stream = await page.createPDFStream();
+ let size = 0;
+ for await (const chunk of stream) {
+ size += chunk.length;
+ }
+ expect(size).toBeGreaterThan(0);
+ });
+
+ it('should respect timeout', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/pdf.html');
+
+ const error = await page.pdf({timeout: 1}).catch(err => {
+ return err;
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ });
+
+ describe('Page.title', function () {
+ it('should return the page title', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/title.html');
+ expect(await page.title()).toBe('Woof-Woof');
+ });
+ });
+
+ describe('Page.select', function () {
+ it('should select single option', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onInput;
+ })
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onChange;
+ })
+ ).toEqual(['blue']);
+ });
+ it('should select only first option', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue', 'green', 'red');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onInput;
+ })
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onChange;
+ })
+ ).toEqual(['blue']);
+ });
+ it('should not throw when select causes navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.$eval('select', select => {
+ return select.addEventListener('input', () => {
+ return ((window as any).location = '/empty.html');
+ });
+ });
+ await Promise.all([
+ page.select('select', 'blue'),
+ page.waitForNavigation(),
+ ]);
+ expect(page.url()).toContain('empty.html');
+ });
+ it('should select multiple options', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => {
+ return (globalThis as any).makeMultiple();
+ });
+ await page.select('select', 'blue', 'green', 'red');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onInput;
+ })
+ ).toEqual(['blue', 'green', 'red']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onChange;
+ })
+ ).toEqual(['blue', 'green', 'red']);
+ });
+ it('should respect event bubbling', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onBubblingInput;
+ })
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onBubblingChange;
+ })
+ ).toEqual(['blue']);
+ });
+ it('should throw when element is not a <select>', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('body', '').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Element is not a <select> element.');
+ });
+ it('should return [] on no matched values', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ const result = await page.select('select', '42', 'abc');
+ expect(result).toEqual([]);
+ });
+ it('should return an array of matched values', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => {
+ return (globalThis as any).makeMultiple();
+ });
+ const result = await page.select('select', 'blue', 'black', 'magenta');
+ expect(
+ result.reduce((accumulator, current) => {
+ return ['blue', 'black', 'magenta'].includes(current) && accumulator;
+ }, true)
+ ).toEqual(true);
+ });
+ it('should return an array of one element when multiple is not set', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ const result = await page.select(
+ 'select',
+ '42',
+ 'blue',
+ 'black',
+ 'magenta'
+ );
+ expect(result).toHaveLength(1);
+ });
+ it('should return [] on no values', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ const result = await page.select('select');
+ expect(result).toEqual([]);
+ });
+ it('should deselect all options when passed no values for a multiple select', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => {
+ return (globalThis as any).makeMultiple();
+ });
+ await page.select('select', 'blue', 'black', 'magenta');
+ await page.select('select');
+ expect(
+ await page.$eval('select', select => {
+ return Array.from((select as HTMLSelectElement).options).every(
+ option => {
+ return !option.selected;
+ }
+ );
+ })
+ ).toEqual(true);
+ });
+ it('should deselect all options when passed no values for a select without multiple', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue', 'black', 'magenta');
+ await page.select('select');
+ expect(
+ await page.$eval('select', select => {
+ return Array.from((select as HTMLSelectElement).options).filter(
+ option => {
+ return option.selected;
+ }
+ )[0]!.value;
+ })
+ ).toEqual('');
+ });
+ it('should throw if passed in non-strings', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<select><option value="12"/></select>');
+ let error!: Error;
+ try {
+ // @ts-expect-error purposefully passing bad input
+ await page.select('select', 12);
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain('Values must be strings');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3327
+ it('should work when re-defining top-level Event class', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => {
+ // @ts-expect-error Expected.
+ return (window.Event = undefined);
+ });
+ await page.select('select', 'blue');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onInput;
+ })
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onChange;
+ })
+ ).toEqual(['blue']);
+ });
+ });
+
+ describe('Page.Events.Close', function () {
+ it('should work with window.close', async () => {
+ const {page, context} = await getTestState();
+
+ const newPagePromise = new Promise<Page | null>(fulfill => {
+ return context.once('targetcreated', target => {
+ return fulfill(target.page());
+ });
+ });
+ assert(page);
+ await page.evaluate(() => {
+ return ((window as any)['newPage'] = window.open('about:blank'));
+ });
+ const newPage = await newPagePromise;
+ assert(newPage);
+ const closedPromise = waitEvent(newPage, 'close');
+ await page.evaluate(() => {
+ return (window as any)['newPage'].close();
+ });
+ await closedPromise;
+ });
+ it('should work with page.close', async () => {
+ const {context} = await getTestState();
+
+ const newPage = await context.newPage();
+ const closedPromise = waitEvent(newPage, 'close');
+ await newPage.close();
+ await closedPromise;
+ });
+ });
+
+ describe('Page.browser', function () {
+ it('should return the correct browser instance', async () => {
+ const {page, browser} = await getTestState();
+
+ expect(page.browser()).toBe(browser);
+ });
+ });
+
+ describe('Page.browserContext', function () {
+ it('should return the correct browser context instance', async () => {
+ const {page, context} = await getTestState();
+
+ expect(page.browserContext()).toBe(context);
+ });
+ });
+
+ describe('Page.client', function () {
+ it('should return the client instance', async () => {
+ const {page} = await getTestState();
+ expect((page as CdpPage)._client()).toBeInstanceOf(CDPSession);
+ });
+ });
+
+ describe('Page.bringToFront', function () {
+ it('should work', async () => {
+ const {browser} = await getTestState();
+ const page1 = await browser.newPage();
+ const page2 = await browser.newPage();
+
+ await page1.bringToFront();
+ expect(
+ await page1.evaluate(() => {
+ return document.visibilityState;
+ })
+ ).toBe('visible');
+ expect(
+ await page2.evaluate(() => {
+ return document.visibilityState;
+ })
+ ).toBe('hidden');
+
+ await page2.bringToFront();
+ expect(
+ await page1.evaluate(() => {
+ return document.visibilityState;
+ })
+ ).toBe('hidden');
+ expect(
+ await page2.evaluate(() => {
+ return document.visibilityState;
+ })
+ ).toBe('visible');
+
+ await page1.close();
+ await page2.close();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/proxy.spec.ts b/remote/test/puppeteer/test/src/proxy.spec.ts
new file mode 100644
index 0000000000..07b73cdd0d
--- /dev/null
+++ b/remote/test/puppeteer/test/src/proxy.spec.ts
@@ -0,0 +1,236 @@
+/**
+ * @license
+ * Copyright 2021 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {IncomingMessage, Server, ServerResponse} from 'http';
+import http from 'http';
+import type {AddressInfo} from 'net';
+import os from 'os';
+
+import type {TestServer} from '@pptr/testserver';
+import expect from 'expect';
+
+import {getTestState, launch} from './mocha-utils.js';
+
+let HOSTNAME = os.hostname();
+
+// Hostname might not be always accessible in environments other than GitHub
+// Actions. Therefore, we try to find an external IPv4 address to be used as a
+// hostname in these tests.
+const networkInterfaces = os.networkInterfaces();
+for (const key of Object.keys(networkInterfaces)) {
+ const interfaces = networkInterfaces[key];
+ for (const net of interfaces || []) {
+ if (net.family === 'IPv4' && !net.internal) {
+ HOSTNAME = net.address;
+ break;
+ }
+ }
+}
+
+/**
+ * Requests to localhost do not get proxied by default. Create a URL using the hostname
+ * instead.
+ */
+function getEmptyPageUrl(server: TestServer): string {
+ const emptyPagePath = new URL(server.EMPTY_PAGE).pathname;
+
+ return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`;
+}
+
+describe('request proxy', () => {
+ let proxiedRequestUrls: string[];
+ let proxyServer: Server;
+ let proxyServerUrl: string;
+ const defaultArgs = [
+ // We disable this in tests so that proxy-related tests
+ // don't intercept queries from this service in headful.
+ '--disable-features=NetworkTimeServiceQuerying',
+ ];
+
+ beforeEach(() => {
+ proxiedRequestUrls = [];
+
+ proxyServer = http
+ .createServer(
+ (
+ originalRequest: IncomingMessage,
+ originalResponse: ServerResponse
+ ) => {
+ proxiedRequestUrls.push(originalRequest.url as string);
+
+ const proxyRequest = http.request(
+ originalRequest.url as string,
+ {
+ method: originalRequest.method,
+ headers: originalRequest.headers,
+ },
+ proxyResponse => {
+ originalResponse.writeHead(
+ proxyResponse.statusCode as number,
+ proxyResponse.headers
+ );
+ proxyResponse.pipe(originalResponse, {end: true});
+ }
+ );
+
+ originalRequest.pipe(proxyRequest, {end: true});
+ }
+ )
+ .listen();
+
+ proxyServerUrl = `http://${HOSTNAME}:${
+ (proxyServer.address() as AddressInfo).port
+ }`;
+ });
+
+ afterEach(async () => {
+ await new Promise((resolve, reject) => {
+ proxyServer.close(error => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(undefined);
+ }
+ });
+ });
+ });
+
+ it('should proxy requests when configured', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`],
+ });
+ try {
+ const page = await browser.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
+ } finally {
+ await close();
+ }
+ });
+
+ it('should respect proxy bypass list', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: [
+ ...defaultArgs,
+ `--proxy-server=${proxyServerUrl}`,
+ `--proxy-bypass-list=${new URL(emptyPageUrl).host}`,
+ ],
+ });
+ try {
+ const page = await browser.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([]);
+ } finally {
+ await close();
+ }
+ });
+
+ describe('in incognito browser context', () => {
+ it('should proxy requests when configured at browser level', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`],
+ });
+ try {
+ const context = await browser.createIncognitoBrowserContext();
+ const page = await context.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
+ } finally {
+ await close();
+ }
+ });
+
+ it('should respect proxy bypass list when configured at browser level', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: [
+ ...defaultArgs,
+ `--proxy-server=${proxyServerUrl}`,
+ `--proxy-bypass-list=${new URL(emptyPageUrl).host}`,
+ ],
+ });
+ try {
+ const context = await browser.createIncognitoBrowserContext();
+ const page = await context.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([]);
+ } finally {
+ await close();
+ }
+ });
+
+ /**
+ * See issues #7873, #7719, and #7698.
+ */
+ it('should proxy requests when configured at context level', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: defaultArgs,
+ });
+ try {
+ const context = await browser.createIncognitoBrowserContext({
+ proxyServer: proxyServerUrl,
+ });
+ const page = await context.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
+ } finally {
+ await close();
+ }
+ });
+
+ it('should respect proxy bypass list when configured at context level', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: defaultArgs,
+ });
+ try {
+ const context = await browser.createIncognitoBrowserContext({
+ proxyServer: proxyServerUrl,
+ proxyBypassList: [new URL(emptyPageUrl).host],
+ });
+ const page = await context.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([]);
+ } finally {
+ await close();
+ }
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/queryhandler.spec.ts b/remote/test/puppeteer/test/src/queryhandler.spec.ts
new file mode 100644
index 0000000000..05f201a9be
--- /dev/null
+++ b/remote/test/puppeteer/test/src/queryhandler.spec.ts
@@ -0,0 +1,653 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert';
+
+import expect from 'expect';
+import {Puppeteer} from 'puppeteer-core';
+import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Query handler tests', function () {
+ setupTestBrowserHooks();
+
+ describe('Pierce selectors', function () {
+ async function setUpPage(): ReturnType<typeof getTestState> {
+ const state = await getTestState();
+ await state.page.setContent(
+ `<script>
+ const div = document.createElement('div');
+ const shadowRoot = div.attachShadow({mode: 'open'});
+ const div1 = document.createElement('div');
+ div1.textContent = 'Hello';
+ div1.className = 'foo';
+ const div2 = document.createElement('div');
+ div2.textContent = 'World';
+ div2.className = 'foo';
+ shadowRoot.appendChild(div1);
+ shadowRoot.appendChild(div2);
+ document.documentElement.appendChild(div);
+ </script>`
+ );
+ return state;
+ }
+ it('should find first element in shadow', async () => {
+ const {page} = await setUpPage();
+ using div = (await page.$('pierce/.foo')) as ElementHandle<HTMLElement>;
+ const text = await div.evaluate(element => {
+ return element.textContent;
+ });
+ expect(text).toBe('Hello');
+ });
+ it('should find all elements in shadow', async () => {
+ const {page} = await setUpPage();
+ const divs = (await page.$$('pierce/.foo')) as Array<
+ ElementHandle<HTMLElement>
+ >;
+ const text = await Promise.all(
+ divs.map(div => {
+ return div.evaluate(element => {
+ return element.textContent;
+ });
+ })
+ );
+ expect(text.join(' ')).toBe('Hello World');
+ });
+ it('should find first child element', async () => {
+ const {page} = await setUpPage();
+ using parentElement = (await page.$('html > div'))!;
+ using childElement = (await parentElement.$(
+ 'pierce/div'
+ )) as ElementHandle<HTMLElement>;
+ const text = await childElement.evaluate(element => {
+ return element.textContent;
+ });
+ expect(text).toBe('Hello');
+ });
+ it('should find all child elements', async () => {
+ const {page} = await setUpPage();
+ using parentElement = (await page.$('html > div'))!;
+ const childElements = (await parentElement.$$('pierce/div')) as Array<
+ ElementHandle<HTMLElement>
+ >;
+ const text = await Promise.all(
+ childElements.map(div => {
+ return div.evaluate(element => {
+ return element.textContent;
+ });
+ })
+ );
+ expect(text.join(' ')).toBe('Hello World');
+ });
+ });
+
+ describe('Text selectors', function () {
+ describe('in Page', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>test</section>');
+
+ expect(await page.$('text/test')).toBeTruthy();
+ expect(await page.$$('text/test')).toHaveLength(1);
+ });
+ it('should return empty array for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ expect(await page.$('text/test')).toBeFalsy();
+ expect(await page.$$('text/test')).toHaveLength(0);
+ });
+ it('should return first element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div id="1">a</div><div>a</div>');
+
+ using element = await page.$('text/a');
+ expect(
+ await element?.evaluate(e => {
+ return e.id;
+ })
+ ).toBe('1');
+ });
+ it('should return multiple elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>a</div><div>a</div>');
+
+ const elements = await page.$$('text/a');
+ expect(elements).toHaveLength(2);
+ });
+ it('should pierce shadow DOM', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ const div = document.createElement('div');
+ const shadow = div.attachShadow({mode: 'open'});
+ const diva = document.createElement('div');
+ shadow.append(diva);
+ const divb = document.createElement('div');
+ shadow.append(divb);
+ diva.innerHTML = 'a';
+ divb.innerHTML = 'b';
+ document.body.append(div);
+ });
+
+ using element = await page.$('text/a');
+ expect(
+ await element?.evaluate(e => {
+ return e.textContent;
+ })
+ ).toBe('a');
+ });
+ it('should query deeply nested text', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div><div>a</div><div>b</div></div>');
+
+ using element = await page.$('text/a');
+ expect(
+ await element?.evaluate(e => {
+ return e.textContent;
+ })
+ ).toBe('a');
+ });
+ it('should query inputs', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<input value="a">');
+
+ using element = (await page.$(
+ 'text/a'
+ )) as ElementHandle<HTMLInputElement>;
+ expect(
+ await element?.evaluate(e => {
+ return e.value;
+ })
+ ).toBe('a');
+ });
+ it('should not query radio', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<radio value="a">');
+
+ expect(await page.$('text/a')).toBeNull();
+ });
+ it('should query text spanning multiple elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div><span>a</span> <span>b</span><div>');
+
+ using element = await page.$('text/a b');
+ expect(
+ await element?.evaluate(e => {
+ return e.textContent;
+ })
+ ).toBe('a b');
+ });
+ it('should clear caches', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div id=target1>text</div><input id=target2 value=text><div id=target3>text</div>'
+ );
+ using div = (await page.$('#target1')) as ElementHandle<HTMLDivElement>;
+ using input = (await page.$(
+ '#target2'
+ )) as ElementHandle<HTMLInputElement>;
+
+ await div.evaluate(div => {
+ div.textContent = 'text';
+ });
+ expect(
+ await page.$eval(`text/text`, e => {
+ return e.id;
+ })
+ ).toBe('target1');
+ await div.evaluate(div => {
+ div.textContent = 'foo';
+ });
+ expect(
+ await page.$eval(`text/text`, e => {
+ return e.id;
+ })
+ ).toBe('target2');
+ await input.evaluate(input => {
+ input.value = '';
+ });
+ await input.type('foo');
+ expect(
+ await page.$eval(`text/text`, e => {
+ return e.id;
+ })
+ ).toBe('target3');
+
+ await div.evaluate(div => {
+ div.textContent = 'text';
+ });
+ await input.evaluate(input => {
+ input.value = '';
+ });
+ await input.type('text');
+ expect(
+ await page.$$eval(`text/text`, es => {
+ return es.length;
+ })
+ ).toBe(3);
+ await div.evaluate(div => {
+ div.textContent = 'foo';
+ });
+ expect(
+ await page.$$eval(`text/text`, es => {
+ return es.length;
+ })
+ ).toBe(2);
+ await input.evaluate(input => {
+ input.value = '';
+ });
+ await input.type('foo');
+ expect(
+ await page.$$eval(`text/text`, es => {
+ return es.length;
+ })
+ ).toBe(1);
+ });
+ });
+ describe('in ElementHandles', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div class="a"><span>a</span></div>');
+
+ using elementHandle = (await page.$('div'))!;
+ expect(await elementHandle.$(`text/a`)).toBeTruthy();
+ expect(await elementHandle.$$(`text/a`)).toHaveLength(1);
+ });
+
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div class="a"></div>');
+
+ using elementHandle = (await page.$('div'))!;
+ expect(await elementHandle.$(`text/a`)).toBeFalsy();
+ expect(await elementHandle.$$(`text/a`)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('XPath selectors', function () {
+ describe('in Page', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>test</section>');
+
+ expect(await page.$('xpath/html/body/section')).toBeTruthy();
+ expect(await page.$$('xpath/html/body/section')).toHaveLength(1);
+ });
+ it('should return empty array for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.$('xpath/html/body/non-existing-element')
+ ).toBeFalsy();
+ expect(
+ await page.$$('xpath/html/body/non-existing-element')
+ ).toHaveLength(0);
+ });
+ it('should return first element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>a</div><div></div>');
+
+ using element = await page.$('xpath/html/body/div');
+ expect(
+ await element?.evaluate(e => {
+ return e.textContent === 'a';
+ })
+ ).toBeTruthy();
+ });
+ it('should return multiple elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div></div><div></div>');
+
+ const elements = await page.$$('xpath/html/body/div');
+ expect(elements).toHaveLength(2);
+ });
+ });
+ describe('in ElementHandles', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div class="a">a<span></span></div>');
+
+ using elementHandle = (await page.$('div'))!;
+ expect(await elementHandle.$(`xpath/span`)).toBeTruthy();
+ expect(await elementHandle.$$(`xpath/span`)).toHaveLength(1);
+ });
+
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div class="a">a</div>');
+
+ using elementHandle = (await page.$('div'))!;
+ expect(await elementHandle.$(`xpath/span`)).toBeFalsy();
+ expect(await elementHandle.$$(`xpath/span`)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('P selectors', () => {
+ beforeEach(async () => {
+ Puppeteer.clearCustomQueryHandlers();
+ });
+
+ it('should work with CSS selectors', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('div > button');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+
+ // Should parse more complex CSS selectors. Listing a few problematic
+ // cases from bug reports.
+ for (const selector of [
+ '.user_row[data-user-id="\\38 "]:not(.deactivated_user)',
+ `input[value='Search']:not([class='hidden'])`,
+ `[data-test-id^="test-"]:not([data-test-id^="test-foo"])`,
+ ]) {
+ await page.$$(selector);
+ }
+ });
+
+ it('should work with deep combinators', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ {
+ using element = await page.$('div >>>> div');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'c';
+ })
+ ).toBeTruthy();
+ }
+ {
+ const elements = await page.$$('div >>> div');
+ assert(elements[1], 'Could not find element');
+ expect(
+ await elements[1]?.evaluate(element => {
+ return element.id === 'd';
+ })
+ ).toBeTruthy();
+ }
+ {
+ const elements = await page.$$('#c >>>> div');
+ assert(elements[0], 'Could not find element');
+ expect(
+ await elements[0]?.evaluate(element => {
+ return element.id === 'd';
+ })
+ ).toBeTruthy();
+ }
+ {
+ const elements = await page.$$('#c >>> div');
+ assert(elements[0], 'Could not find element');
+ expect(
+ await elements[0]?.evaluate(element => {
+ return element.id === 'd';
+ })
+ ).toBeTruthy();
+ }
+ });
+
+ it('should work with text selectors', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('div ::-p-text(world)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work ARIA selectors', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('div ::-p-aria(world)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work for ARIA selectors in multiple isolated worlds', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.waitForSelector('::-p-aria(world)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ // $ would add ARIA query handler to the main world.
+ await element.$('::-p-aria(world)');
+ using element2 = await page.waitForSelector('::-p-aria(world)');
+ assert(element2, 'Could not find element');
+ expect(
+ await element2.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work ARIA selectors with role', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('::-p-aria(world[role="button"])');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work ARIA selectors with name and role', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('::-p-aria([name="world"][role="button"])');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work XPath selectors', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('div ::-p-xpath(//button)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work with custom selectors', async () => {
+ Puppeteer.registerCustomQueryHandler('div', {
+ queryOne() {
+ return document.querySelector('div');
+ },
+ });
+
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('::-p-div');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'a';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work with custom selectors with args', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ Puppeteer.registerCustomQueryHandler('div', {
+ queryOne(_, selector) {
+ if (selector === 'true') {
+ return document.querySelector('div');
+ } else {
+ return document.querySelector('button');
+ }
+ },
+ });
+
+ {
+ using element = await page.$('::-p-div(true)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'a';
+ })
+ ).toBeTruthy();
+ }
+ {
+ using element = await page.$('::-p-div("true")');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'a';
+ })
+ ).toBeTruthy();
+ }
+ {
+ using element = await page.$("::-p-div('true')");
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'a';
+ })
+ ).toBeTruthy();
+ }
+ {
+ using element = await page.$('::-p-div');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ }
+ });
+
+ it('should work with :hover', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using button = await page.$('div ::-p-text(world)');
+ assert(button, 'Could not find element');
+ await button.hover();
+
+ using button2 = await page.$('div ::-p-text(world):hover');
+ assert(button2, 'Could not find element');
+ const value = await button2.evaluate(span => {
+ return {textContent: span.textContent, tagName: span.tagName};
+ });
+ expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'});
+ });
+
+ it('should work with selector lists', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ const elements = await page.$$('div, ::-p-text(world)');
+ expect(elements).toHaveLength(3);
+ });
+
+ const permute = <T>(inputs: T[]): T[][] => {
+ const results: T[][] = [];
+ for (let i = 0; i < inputs.length; ++i) {
+ const permutation = permute(
+ inputs.slice(0, i).concat(inputs.slice(i + 1))
+ );
+ const value = inputs[i] as T;
+ if (permutation.length === 0) {
+ results.push([value]);
+ continue;
+ }
+ for (const part of permutation) {
+ results.push([value].concat(part));
+ }
+ }
+ return results;
+ };
+
+ it('should match querySelector* ordering', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ for (const list of permute(['div', 'button', 'span'])) {
+ const elements = await page.$$(
+ list
+ .map(selector => {
+ return selector === 'button' ? '::-p-text(world)' : selector;
+ })
+ .join(',')
+ );
+ const actual = await Promise.all(
+ elements.map(element => {
+ return element.evaluate(element => {
+ return element.id;
+ });
+ })
+ );
+ expect(actual.join()).toStrictEqual('a,b,f,c');
+ }
+ });
+
+ it('should not have duplicate elements from selector lists', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ const elements = await page.$$('::-p-text(world), button');
+ expect(elements).toHaveLength(1);
+ });
+
+ it('should handle escapes', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$(
+ ':scope >>> ::-p-text(My name is Jun \\(pronounced like "June"\\))'
+ );
+ expect(element).toBeTruthy();
+ using element2 = await page.$(
+ ':scope >>> ::-p-text("My name is Jun (pronounced like \\"June\\")")'
+ );
+ expect(element2).toBeTruthy();
+ using element3 = await page.$(
+ ':scope >>> ::-p-text(My name is Jun \\(pronounced like "June"\\)")'
+ );
+ expect(element3).toBeFalsy();
+ using element4 = await page.$(
+ ':scope >>> ::-p-text("My name is Jun \\(pronounced like "June"\\))'
+ );
+ expect(element4).toBeFalsy();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/queryselector.spec.ts b/remote/test/puppeteer/test/src/queryselector.spec.ts
new file mode 100644
index 0000000000..7fd27f914f
--- /dev/null
+++ b/remote/test/puppeteer/test/src/queryselector.spec.ts
@@ -0,0 +1,491 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+import {Puppeteer} from 'puppeteer';
+import type {CustomQueryHandler} from 'puppeteer-core/internal/common/CustomQueryHandler.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('querySelector', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.$eval', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section id="testAttribute">43543</section>');
+ const idAttribute = await page.$eval('section', e => {
+ return e.id;
+ });
+ expect(idAttribute).toBe('testAttribute');
+ });
+ it('should accept arguments', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>hello</section>');
+ const text = await page.$eval(
+ 'section',
+ (e, suffix) => {
+ return e.textContent! + suffix;
+ },
+ ' world!'
+ );
+ expect(text).toBe('hello world!');
+ });
+ it('should accept ElementHandles as arguments', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>hello</section><div> world</div>');
+ using divHandle = (await page.$('div'))!;
+ const text = await page.$eval(
+ 'section',
+ (e, div) => {
+ return e.textContent! + (div as HTMLElement).textContent!;
+ },
+ divHandle
+ );
+ expect(text).toBe('hello world');
+ });
+ it('should throw error if no element is found', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .$eval('section', e => {
+ return e.id;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain(
+ 'failed to find element matching selector "section"'
+ );
+ });
+ });
+
+ // The tests for $$eval are repeated later in this file in the test group 'QueryAll'.
+ // This is done to also test a query handler where QueryAll returns an Element[]
+ // as opposed to NodeListOf<Element>.
+ describe('Page.$$eval', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCount = await page.$$eval('div', divs => {
+ return divs.length;
+ });
+ expect(divsCount).toBe(3);
+ });
+ it('should accept extra arguments', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCountPlus5 = await page.$$eval(
+ 'div',
+ (divs, two, three) => {
+ return divs.length + (two as number) + (three as number);
+ },
+ 2,
+ 3
+ );
+ expect(divsCountPlus5).toBe(8);
+ });
+ it('should accept ElementHandles as arguments', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<section>2</section><section>2</section><section>1</section><div>3</div>'
+ );
+ using divHandle = (await page.$('div'))!;
+ const sum = await page.$$eval(
+ 'section',
+ (sections, div) => {
+ return (
+ sections.reduce((acc, section) => {
+ return acc + Number(section.textContent);
+ }, 0) + Number((div as HTMLElement).textContent)
+ );
+ },
+ divHandle
+ );
+ expect(sum).toBe(8);
+ });
+ it('should handle many elements', async function () {
+ this.timeout(25_000);
+
+ const {page} = await getTestState();
+ await page.evaluate(
+ `
+ for (var i = 0; i <= 1000; i++) {
+ const section = document.createElement('section');
+ section.textContent = i;
+ document.body.appendChild(section);
+ }
+ `
+ );
+ const sum = await page.$$eval('section', sections => {
+ return sections.reduce((acc, section) => {
+ return acc + Number(section.textContent);
+ }, 0);
+ });
+ expect(sum).toBe(500500);
+ });
+ });
+
+ describe('Page.$', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>test</section>');
+ using element = (await page.$('section'))!;
+ expect(element).toBeTruthy();
+ });
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ using element = (await page.$('non-existing-element'))!;
+ expect(element).toBe(null);
+ });
+ });
+
+ describe('Page.$$', function () {
+ it('should query existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>A</div><br/><div>B</div>');
+ const elements = await page.$$('div');
+ expect(elements).toHaveLength(2);
+ const promises = elements.map(element => {
+ return page.evaluate((e: HTMLElement) => {
+ return e.textContent;
+ }, element);
+ });
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+ it('should return empty array if nothing is found', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const elements = await page.$$('div');
+ expect(elements).toHaveLength(0);
+ });
+ });
+
+ describe('Page.$x', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>test</section>');
+ const elements = await page.$x('/html/body/section');
+ expect(elements[0]).toBeTruthy();
+ expect(elements).toHaveLength(1);
+ });
+ it('should return empty array for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ const element = await page.$x('/html/body/non-existing-element');
+ expect(element).toEqual([]);
+ });
+ it('should return multiple elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div></div><div></div>');
+ const elements = await page.$x('/html/body/div');
+ expect(elements).toHaveLength(2);
+ });
+ });
+
+ describe('ElementHandle.$', function () {
+ it('should query existing element', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/playground.html');
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">A</div></div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ using second = (await html.$('.second'))!;
+ using inner = await second.$('.inner');
+ const content = await page.evaluate(e => {
+ return e?.textContent;
+ }, inner);
+ expect(content).toBe('A');
+ });
+
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">B</div></div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ using second = await html.$('.third');
+ expect(second).toBe(null);
+ });
+ });
+ describe('ElementHandle.$eval', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>'
+ );
+ using tweet = (await page.$('.tweet'))!;
+ const content = await tweet.$eval('.like', node => {
+ return (node as HTMLElement).innerText;
+ });
+ expect(content).toBe('100');
+ });
+
+ it('should retrieve content from subtree', async () => {
+ const {page} = await getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>';
+ await page.setContent(htmlContent);
+ using elementHandle = (await page.$('#myId'))!;
+ const content = await elementHandle.$eval('.a', node => {
+ return (node as HTMLElement).innerText;
+ });
+ expect(content).toBe('a-child-div');
+ });
+
+ it('should throw in case of missing selector', async () => {
+ const {page} = await getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"></div>';
+ await page.setContent(htmlContent);
+ using elementHandle = (await page.$('#myId'))!;
+ const errorMessage = await elementHandle
+ .$eval('.a', node => {
+ return (node as HTMLElement).innerText;
+ })
+ .catch(error => {
+ return error.message;
+ });
+ expect(errorMessage).toBe(
+ `Error: failed to find element matching selector ".a"`
+ );
+ });
+ });
+ describe('ElementHandle.$$eval', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>'
+ );
+ using tweet = (await page.$('.tweet'))!;
+ const content = await tweet.$$eval('.like', nodes => {
+ return (nodes as HTMLElement[]).map(n => {
+ return n.innerText;
+ });
+ });
+ expect(content).toEqual(['100', '10']);
+ });
+
+ it('should retrieve content from subtree', async () => {
+ const {page} = await getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>';
+ await page.setContent(htmlContent);
+ using elementHandle = (await page.$('#myId'))!;
+ const content = await elementHandle.$$eval('.a', nodes => {
+ return (nodes as HTMLElement[]).map(n => {
+ return n.innerText;
+ });
+ });
+ expect(content).toEqual(['a1-child-div', 'a2-child-div']);
+ });
+
+ it('should not throw in case of missing selector', async () => {
+ const {page} = await getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"></div>';
+ await page.setContent(htmlContent);
+ using elementHandle = (await page.$('#myId'))!;
+ const nodesLength = await elementHandle.$$eval('.a', nodes => {
+ return nodes.length;
+ });
+ expect(nodesLength).toBe(0);
+ });
+ });
+
+ describe('ElementHandle.$$', function () {
+ it('should query existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div>A</div><br/><div>B</div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const elements = await html.$$('div');
+ expect(elements).toHaveLength(2);
+ const promises = elements.map(element => {
+ return page.evaluate((e: HTMLElement) => {
+ return e.textContent;
+ }, element);
+ });
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+
+ it('should return empty array for non-existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><span>A</span><br/><span>B</span></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const elements = await html.$$('div');
+ expect(elements).toHaveLength(0);
+ });
+ });
+
+ describe('ElementHandle.$x', function () {
+ it('should query existing element', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/playground.html');
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">A</div></div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const second = await html.$x(`./body/div[contains(@class, 'second')]`);
+ const inner = await second[0]!.$x(`./div[contains(@class, 'inner')]`);
+ const content = await page.evaluate(e => {
+ return e.textContent;
+ }, inner[0]!);
+ expect(content).toBe('A');
+ });
+
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">B</div></div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const second = await html.$x(`/div[contains(@class, 'third')]`);
+ expect(second).toEqual([]);
+ });
+ });
+
+ // This is the same tests for `$$eval` and `$$` as above, but with a queryAll
+ // handler that returns an array instead of a list of nodes.
+ describe('QueryAll', function () {
+ const handler: CustomQueryHandler = {
+ queryAll: (element, selector) => {
+ return [...(element as Element).querySelectorAll(selector)];
+ },
+ };
+ before(() => {
+ Puppeteer.registerCustomQueryHandler('allArray', handler);
+ });
+
+ it('should have registered handler', async () => {
+ expect(
+ Puppeteer.customQueryHandlerNames().includes('allArray')
+ ).toBeTruthy();
+ });
+ it('$$ should query existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div>A</div><br/><div>B</div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const elements = await html.$$('allArray/div');
+ expect(elements).toHaveLength(2);
+ const promises = elements.map(element => {
+ return page.evaluate(e => {
+ return e.textContent;
+ }, element);
+ });
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+
+ it('$$ should return empty array for non-existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><span>A</span><br/><span>B</span></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const elements = await html.$$('allArray/div');
+ expect(elements).toHaveLength(0);
+ });
+ it('$$eval should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCount = await page.$$eval('allArray/div', divs => {
+ return divs.length;
+ });
+ expect(divsCount).toBe(3);
+ });
+ it('$$eval should accept extra arguments', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCountPlus5 = await page.$$eval(
+ 'allArray/div',
+ (divs, two, three) => {
+ return divs.length + (two as number) + (three as number);
+ },
+ 2,
+ 3
+ );
+ expect(divsCountPlus5).toBe(8);
+ });
+ it('$$eval should accept ElementHandles as arguments', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<section>2</section><section>2</section><section>1</section><div>3</div>'
+ );
+ using divHandle = (await page.$('div'))!;
+ const sum = await page.$$eval(
+ 'allArray/section',
+ (sections, div) => {
+ return (
+ sections.reduce((acc, section) => {
+ return acc + Number(section.textContent);
+ }, 0) + Number((div as HTMLElement).textContent)
+ );
+ },
+ divHandle
+ );
+ expect(sum).toBe(8);
+ });
+ it('$$eval should handle many elements', async function () {
+ this.timeout(25_000);
+
+ const {page} = await getTestState();
+ await page.evaluate(
+ `
+ for (var i = 0; i <= 1000; i++) {
+ const section = document.createElement('section');
+ section.textContent = i;
+ document.body.appendChild(section);
+ }
+ `
+ );
+ const sum = await page.$$eval('allArray/section', sections => {
+ return sections.reduce((acc, section) => {
+ return acc + Number(section.textContent);
+ }, 0);
+ });
+ expect(sum).toBe(500500);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts
new file mode 100644
index 0000000000..966554fd5d
--- /dev/null
+++ b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts
@@ -0,0 +1,969 @@
+/**
+ * @license
+ * Copyright 2021 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import expect from 'expect';
+import {
+ type ActionResult,
+ type HTTPRequest,
+ InterceptResolutionAction,
+} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {isFavicon, waitEvent} from './utils.js';
+
+describe('cooperative request interception', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.setRequestInterception', function () {
+ const expectedActions: ActionResult[] = ['abort', 'continue', 'respond'];
+ while (expectedActions.length > 0) {
+ const expectedAction = expectedActions.pop();
+ it(`should cooperatively ${expectedAction} by priority`, async () => {
+ const {page, server} = await getTestState();
+
+ const actionResults: ActionResult[] = [];
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.continue(
+ {headers: {...request.headers(), xaction: 'continue'}},
+ expectedAction === 'continue' ? 1 : 0
+ );
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.respond(
+ {headers: {xaction: 'respond'}},
+ expectedAction === 'respond' ? 1 : 0
+ );
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.abort('aborted', expectedAction === 'abort' ? 1 : 0);
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ page.on('response', response => {
+ const {xaction} = response!.headers();
+ if (response!.url().endsWith('.css') && !!xaction) {
+ actionResults.push(xaction as ActionResult);
+ }
+ });
+ page.on('requestfailed', request => {
+ if (request.url().endsWith('.css')) {
+ actionResults.push('abort');
+ }
+ });
+
+ const response = (await (async () => {
+ if (expectedAction === 'continue') {
+ const [serverRequest, response] = await Promise.all([
+ server.waitForRequest('/one-style.css'),
+ page.goto(server.PREFIX + '/one-style.html'),
+ ]);
+ actionResults.push(
+ serverRequest.headers['xaction'] as ActionResult
+ );
+ return response;
+ } else {
+ return await page.goto(server.PREFIX + '/one-style.html');
+ }
+ })())!;
+
+ expect(actionResults).toHaveLength(1);
+ expect(actionResults[0]).toBe(expectedAction);
+ expect(response!.ok()).toBe(true);
+ });
+ }
+
+ it('should intercept', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (isFavicon(request)) {
+ void request.continue({}, 0);
+ return;
+ }
+ expect(request.url()).toContain('empty.html');
+ expect(request.headers()['user-agent']).toBeTruthy();
+ expect(request.method()).toBe('GET');
+ expect(request.postData()).toBe(undefined);
+ expect(request.isNavigationRequest()).toBe(true);
+ expect(request.resourceType()).toBe('document');
+ expect(request.frame() === page.mainFrame()).toBe(true);
+ expect(request.frame()!.url()).toBe('about:blank');
+ void request.continue({}, 0);
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response!.ok()).toBe(true);
+ expect(response!.remoteAddress().port).toBe(server.PORT);
+ });
+ // @see https://github.com/puppeteer/puppeteer/pull/3105
+ it('should work when POST is redirected with 302', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/rredirect', '/empty.html');
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ await page.setContent(`
+ <form action='/rredirect' method='post'>
+ <input type="hidden" id="foo" name="foo" value="FOOBAR">
+ </form>
+ `);
+ await Promise.all([
+ page.$eval('form', form => {
+ return (form as HTMLFormElement).submit();
+ }),
+ page.waitForNavigation(),
+ ]);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3973
+ it('should work when header manipulation headers with redirect', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/rrredirect', '/empty.html');
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ });
+ void request.continue({headers}, 0);
+
+ expect(request.continueRequestOverrides()).toEqual({headers});
+ });
+ // Make sure that the goto does not time out.
+ await page.goto(server.PREFIX + '/rrredirect');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4743
+ it('should be able to remove headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ origin: undefined, // remove "origin" header
+ });
+ void request.continue({headers}, 0);
+ });
+
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.PREFIX + '/empty.html'),
+ ]);
+
+ expect(serverRequest.headers.origin).toBe(undefined);
+ });
+ it('should contain referer header', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ if (!isFavicon(request)) {
+ requests.push(request);
+ }
+ void request.continue({}, 0);
+ });
+ await page.goto(server.PREFIX + '/one-style.html');
+ expect(requests[1]!.url()).toContain('/one-style.css');
+ expect(requests[1]!.headers()['referer']).toContain('/one-style.html');
+ });
+ it('should properly return navigation response when URL has cookies', async () => {
+ const {page, server} = await getTestState();
+
+ // Setup cookie.
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({name: 'foo', value: 'bar'});
+
+ // Setup request interception.
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const response = await page.reload();
+ expect(response!.status()).toBe(200);
+ });
+ it('should stop intercepting', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.once('request', request => {
+ return request.continue({}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(false);
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should show custom HTTP headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ foo: 'bar',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ expect(request.headers()['foo']).toBe('bar');
+ void request.continue({}, 0);
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.ok()).toBe(true);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4337
+ it('should work with redirect inside sync XHR', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRedirect('/logo.png', '/pptr.png');
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const status = await page.evaluate(async () => {
+ const request = new XMLHttpRequest();
+ request.open('GET', '/logo.png', false); // `false` makes the request synchronous
+ request.send(null);
+ return request.status;
+ });
+ expect(status).toBe(200);
+ });
+ it('should work with custom referer headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE});
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
+ void request.continue({}, 0);
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.ok()).toBe(true);
+ });
+ it('should be abortable', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.abort('failed', 0);
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ let failedRequests = 0;
+ page.on('requestfailed', () => {
+ return ++failedRequests;
+ });
+ const response = await page.goto(server.PREFIX + '/one-style.html');
+ expect(response!.ok()).toBe(true);
+ expect(response!.request().failure()).toBe(null);
+ expect(failedRequests).toBe(1);
+ });
+ it('should be able to access the error reason', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.abort('failed', 0);
+ });
+ let abortReason = null;
+ page.on('request', request => {
+ abortReason = request.abortErrorReason();
+ void request.continue({}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE).catch(() => {});
+ expect(abortReason).toBe('Failed');
+ });
+ it('should be abortable with custom error codes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.abort('internetdisconnected', 0);
+ });
+
+ const [failedRequest] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'requestfailed'),
+ page.goto(server.EMPTY_PAGE).catch(() => {}),
+ ]);
+ expect(failedRequest).toBeTruthy();
+ expect(failedRequest.failure()!.errorText).toBe(
+ 'net::ERR_INTERNET_DISCONNECTED'
+ );
+ });
+ it('should send referer', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ referer: 'http://google.com/',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/grid.html'),
+ page.goto(server.PREFIX + '/grid.html'),
+ ]);
+ expect(request.headers['referer']).toBe('http://google.com/');
+ });
+ it('should fail navigation when aborting main resource', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.abort('failed', 0);
+ });
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ if (isChrome) {
+ expect(error.message).toContain('net::ERR_FAILED');
+ } else {
+ expect(error.message).toContain('NS_ERROR_FAILURE');
+ }
+ });
+ it('should work with redirects', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue({}, 0);
+ requests.push(request);
+ });
+ server.setRedirect(
+ '/non-existing-page.html',
+ '/non-existing-page-2.html'
+ );
+ server.setRedirect(
+ '/non-existing-page-2.html',
+ '/non-existing-page-3.html'
+ );
+ server.setRedirect(
+ '/non-existing-page-3.html',
+ '/non-existing-page-4.html'
+ );
+ server.setRedirect('/non-existing-page-4.html', '/empty.html');
+ const response = await page.goto(
+ server.PREFIX + '/non-existing-page.html'
+ );
+ expect(response!.status()).toBe(200);
+ expect(response!.url()).toContain('empty.html');
+ expect(requests).toHaveLength(5);
+ expect(requests[2]!.resourceType()).toBe('document');
+ // Check redirect chain
+ const redirectChain = response!.request().redirectChain();
+ expect(redirectChain).toHaveLength(4);
+ expect(redirectChain[0]!.url()).toContain('/non-existing-page.html');
+ expect(redirectChain[2]!.url()).toContain('/non-existing-page-3.html');
+ for (let i = 0; i < redirectChain.length; ++i) {
+ const request = redirectChain[i]!;
+ expect(request.isNavigationRequest()).toBe(true);
+ expect(request.redirectChain().indexOf(request)).toBe(i);
+ }
+ });
+ it('should work with redirects for subresources', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue({}, 0);
+ if (!isFavicon(request)) {
+ requests.push(request);
+ }
+ });
+ server.setRedirect('/one-style.css', '/two-style.css');
+ server.setRedirect('/two-style.css', '/three-style.css');
+ server.setRedirect('/three-style.css', '/four-style.css');
+ server.setRoute('/four-style.css', (_req, res) => {
+ return res.end('body {box-sizing: border-box; }');
+ });
+
+ const response = await page.goto(server.PREFIX + '/one-style.html');
+ expect(response!.status()).toBe(200);
+ expect(response!.url()).toContain('one-style.html');
+ expect(requests).toHaveLength(5);
+ expect(requests[0]!.resourceType()).toBe('document');
+ expect(requests[1]!.resourceType()).toBe('stylesheet');
+ // Check redirect chain
+ const redirectChain = requests[1]!.redirectChain();
+ expect(redirectChain).toHaveLength(3);
+ expect(redirectChain[0]!.url()).toContain('/one-style.css');
+ expect(redirectChain[2]!.url()).toContain('/three-style.css');
+ });
+ it('should be able to abort redirects', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRedirect('/non-existing.json', '/non-existing-2.json');
+ server.setRedirect('/non-existing-2.json', '/simple.html');
+ page.on('request', request => {
+ if (request.url().includes('non-existing-2')) {
+ void request.abort('failed', 0);
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const result = await page.evaluate(async () => {
+ try {
+ return await fetch('/non-existing.json');
+ } catch (error) {
+ return (error as Error).message;
+ }
+ });
+ if (isChrome) {
+ expect(result).toContain('Failed to fetch');
+ } else {
+ expect(result).toContain('NetworkError');
+ }
+ });
+ it('should work with equal requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let responseCount = 1;
+ server.setRoute('/zzz', (_req, res) => {
+ return res.end(responseCount++ * 11 + '');
+ });
+ await page.setRequestInterception(true);
+
+ let spinner = false;
+ // Cancel 2nd request.
+ page.on('request', request => {
+ if (isFavicon(request)) {
+ void request.continue({}, 0);
+ return;
+ }
+ void (spinner ? request.abort('failed', 0) : request.continue({}, 0));
+ spinner = !spinner;
+ });
+ const results = await page.evaluate(() => {
+ return Promise.all([
+ fetch('/zzz')
+ .then(response => {
+ return response!.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ fetch('/zzz')
+ .then(response => {
+ return response!.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ fetch('/zzz')
+ .then(response => {
+ return response!.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ ]);
+ });
+ expect(results).toEqual(['11', 'FAILED', '22']);
+ });
+ it('should navigate to dataURL and fire dataURL requests', async () => {
+ const {page} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ requests.push(request);
+ void request.continue({}, 0);
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const response = await page.goto(dataURL);
+ expect(response!.status()).toBe(200);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should be able to fetch dataURL and fire dataURL requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ !isFavicon(request) && requests.push(request);
+ void request.continue({}, 0);
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const text = await page.evaluate((url: string) => {
+ return fetch(url).then(r => {
+ return r.text();
+ });
+ }, dataURL);
+ expect(text).toBe('<div>yo</div>');
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should navigate to URL with hash and fire requests without hash', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ requests.push(request);
+ void request.continue({}, 0);
+ });
+ const response = await page.goto(server.EMPTY_PAGE + '#hash');
+ expect(response!.status()).toBe(200);
+ expect(response!.url()).toBe(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with encoded server', async () => {
+ const {page, server} = await getTestState();
+
+ // The requestWillBeSent will report encoded URL, whereas interception will
+ // report URL as-is. @see crbug.com/759388
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const response = await page.goto(
+ server.PREFIX + '/some nonexisting page'
+ );
+ expect(response!.status()).toBe(404);
+ });
+ it('should work with badly encoded server', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRoute('/malformed?rnd=%911', (_req, res) => {
+ return res.end();
+ });
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const response = await page.goto(server.PREFIX + '/malformed?rnd=%911');
+ expect(response!.status()).toBe(200);
+ });
+ it('should work with encoded server - 2', async () => {
+ const {page, server} = await getTestState();
+
+ // The requestWillBeSent will report URL as-is, whereas interception will
+ // report encoded URL for stylesheet. @see crbug.com/759388
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue({}, 0);
+ requests.push(request);
+ });
+ const response = await page.goto(
+ `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`
+ );
+ expect(response!.status()).toBe(200);
+ expect(requests).toHaveLength(2);
+ expect(requests[1]!.response()!.status()).toBe(404);
+ });
+ it('should not throw "Invalid Interception Id" if the request was cancelled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setContent('<iframe></iframe>');
+ await page.setRequestInterception(true);
+ let request!: HTTPRequest;
+ page.on('request', async r => {
+ return (request = r);
+ });
+ void (page.$eval(
+ 'iframe',
+ (frame, url) => {
+ return ((frame as HTMLIFrameElement).src = url as string);
+ },
+ server.EMPTY_PAGE
+ ),
+ // Wait for request interception.
+ await waitEvent(page, 'request'));
+ // Delete frame to cause request to be canceled.
+ await page.$eval('iframe', frame => {
+ return frame.remove();
+ });
+ let error!: Error;
+ await request.continue({}, 0).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ });
+ it('should throw if interception is not enabled', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ page.on('request', async request => {
+ try {
+ await request.continue({}, 0);
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(error.message).toContain('Request Interception is not enabled');
+ });
+ it('should work with file URLs', async () => {
+ const {page} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const urls = new Set();
+ page.on('request', request => {
+ urls.add(request.url().split('/').pop());
+ void request.continue({}, 0);
+ });
+ await page.goto(
+ pathToFileURL(path.join(__dirname, '../assets', 'one-style.html'))
+ );
+ expect(urls.size).toBe(2);
+ expect(urls.has('one-style.html')).toBe(true);
+ expect(urls.has('one-style.css')).toBe(true);
+ });
+ it('should not cache if cache disabled', async () => {
+ const {page, server} = await getTestState();
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(false);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+
+ const cached: HTTPRequest[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r);
+ });
+
+ await page.reload();
+ expect(cached).toHaveLength(0);
+ });
+ it('should cache if cache enabled', async () => {
+ const {page, server} = await getTestState();
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+
+ const cached: HTTPRequest[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r);
+ });
+
+ await page.reload();
+ expect(cached).toHaveLength(1);
+ });
+ it('should load fonts if cache enabled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+
+ await page.goto(server.PREFIX + '/cached/one-style-font.html');
+ await page.waitForResponse(r => {
+ return r.url().endsWith('/one-style.woff');
+ });
+ });
+ });
+
+ describe('Request.continue', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should amend HTTP headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers());
+ headers['FOO'] = 'bar';
+ void request.continue({headers}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz');
+ }),
+ ]);
+ expect(request.headers['foo']).toBe('bar');
+ });
+ it('should redirect in a way non-observable to page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const redirectURL = request.url().includes('/empty.html')
+ ? server.PREFIX + '/consolelog.html'
+ : undefined;
+ void request.continue({url: redirectURL}, 0);
+ });
+
+ const [consoleMessage] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ expect(consoleMessage.text()).toBe('yellow');
+ });
+ it('should amend method', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({method: 'POST'}, 0);
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz');
+ }),
+ ]);
+ expect(request.method).toBe('POST');
+ });
+ it('should amend post data', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({postData: 'doggo'}, 0);
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz', {method: 'POST', body: 'birdy'});
+ }),
+ ]);
+ expect(await serverRequest.postBody).toBe('doggo');
+ });
+ it('should amend both post data and method on navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({method: 'POST', postData: 'doggo'}, 0);
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(serverRequest.method).toBe('POST');
+ expect(await serverRequest.postBody).toBe('doggo');
+ });
+ });
+
+ describe('Request.respond', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond(
+ {
+ status: 201,
+ headers: {
+ foo: 'bar',
+ },
+ body: 'Yo, page!',
+ },
+ 0
+ );
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.status()).toBe(201);
+ expect(response!.headers()['foo']).toBe('bar');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should be able to access the response', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond(
+ {
+ status: 200,
+ body: 'Yo, page!',
+ },
+ 0
+ );
+ });
+ let response = null;
+ page.on('request', request => {
+ response = request.responseForRequest();
+ void request.continue({}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(response).toEqual({status: 200, body: 'Yo, page!'});
+ });
+ it('should work with status code 422', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond(
+ {
+ status: 422,
+ body: 'Yo, page!',
+ },
+ 0
+ );
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.status()).toBe(422);
+ expect(response!.statusText()).toBe('Unprocessable Entity');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should redirect', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (!request.url().includes('rrredirect')) {
+ void request.continue({}, 0);
+ return;
+ }
+ void request.respond(
+ {
+ status: 302,
+ headers: {
+ location: server.EMPTY_PAGE,
+ },
+ },
+ 0
+ );
+ });
+ const response = await page.goto(server.PREFIX + '/rrredirect');
+ expect(response!.request().redirectChain()).toHaveLength(1);
+ expect(response!.request().redirectChain()[0]!.url()).toBe(
+ server.PREFIX + '/rrredirect'
+ );
+ expect(response!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should allow mocking binary responses', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, '../assets', 'pptr.png')
+ );
+ void request.respond(
+ {
+ contentType: 'image/png',
+ body: imageBuffer,
+ },
+ 0
+ );
+ });
+ await page.evaluate(PREFIX => {
+ const img = document.createElement('img');
+ img.src = PREFIX + '/does-not-exist.png';
+ document.body.appendChild(img);
+ return new Promise(fulfill => {
+ return (img.onload = fulfill);
+ });
+ }, server.PREFIX);
+ using img = (await page.$('img'))!;
+ expect(await img.screenshot()).toBeGolden('mock-binary-response.png');
+ });
+ it('should stringify intercepted request response headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond(
+ {
+ status: 200,
+ headers: {
+ foo: true,
+ },
+ body: 'Yo, page!',
+ },
+ 0
+ );
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.status()).toBe(200);
+ const headers = response!.headers();
+ expect(headers['foo']).toBe('true');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should indicate already-handled if an intercept has been handled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue();
+ });
+ page.on('request', request => {
+ expect(request.isInterceptResolutionHandled()).toBeTruthy();
+ });
+ page.on('request', request => {
+ const {action} = request.interceptResolutionState();
+ expect(action).toBe(InterceptResolutionAction.AlreadyHandled);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ });
+ });
+});
+
+function pathToFileURL(path: string): string {
+ let pathName = path.replace(/\\/g, '/');
+ // Windows drive letter must be prefixed with a slash.
+ if (!pathName.startsWith('/')) {
+ pathName = '/' + pathName;
+ }
+ return 'file://' + pathName;
+}
diff --git a/remote/test/puppeteer/test/src/requestinterception.spec.ts b/remote/test/puppeteer/test/src/requestinterception.spec.ts
new file mode 100644
index 0000000000..45827bb3cf
--- /dev/null
+++ b/remote/test/puppeteer/test/src/requestinterception.spec.ts
@@ -0,0 +1,920 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import expect from 'expect';
+import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {isFavicon, waitEvent} from './utils.js';
+
+describe('request interception', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.setRequestInterception', function () {
+ it('should intercept', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (isFavicon(request)) {
+ void request.continue();
+ return;
+ }
+ expect(request.url()).toContain('empty.html');
+ expect(request.headers()['user-agent']).toBeTruthy();
+ expect(request.headers()['accept']).toBeTruthy();
+ expect(request.method()).toBe('GET');
+ expect(request.postData()).toBe(undefined);
+ expect(request.isNavigationRequest()).toBe(true);
+ expect(request.resourceType()).toBe('document');
+ expect(request.frame() === page.mainFrame()).toBe(true);
+ expect(request.frame()!.url()).toBe('about:blank');
+ void request.continue();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ expect(response.remoteAddress().port).toBe(server.PORT);
+ });
+ // @see https://github.com/puppeteer/puppeteer/pull/3105
+ it('should work when POST is redirected with 302', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/rredirect', '/empty.html');
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ await page.setContent(`
+ <form action='/rredirect' method='post'>
+ <input type="hidden" id="foo" name="foo" value="FOOBAR">
+ </form>
+ `);
+ await Promise.all([
+ page.$eval('form', form => {
+ return (form as HTMLFormElement).submit();
+ }),
+ page.waitForNavigation(),
+ ]);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3973
+ it('should work when header manipulation headers with redirect', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/rrredirect', '/empty.html');
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ });
+ void request.continue({headers});
+ });
+ await page.goto(server.PREFIX + '/rrredirect');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4743
+ it('should be able to remove headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ origin: undefined, // remove "origin" header
+ });
+ void request.continue({headers});
+ });
+
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.PREFIX + '/empty.html'),
+ ]);
+
+ expect(serverRequest.headers.origin).toBe(undefined);
+ });
+ it('should contain referer header', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ if (!isFavicon(request)) {
+ requests.push(request);
+ }
+ void request.continue();
+ });
+ await page.goto(server.PREFIX + '/one-style.html');
+ expect(requests[1]!.url()).toContain('/one-style.css');
+ expect(requests[1]!.headers()['referer']).toContain('/one-style.html');
+ });
+ it('should work with requests without networkId', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+
+ const cdp = await page.target().createCDPSession();
+ await cdp.send('DOM.enable');
+ const urls: string[] = [];
+ page.on('request', request => {
+ urls.push(request.url());
+ return request.continue();
+ });
+ // This causes network requests without networkId.
+ await cdp.send('CSS.enable');
+ expect(urls).toStrictEqual([server.EMPTY_PAGE]);
+ });
+ it('should properly return navigation response when URL has cookies', async () => {
+ const {page, server} = await getTestState();
+
+ // Setup cookie.
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({name: 'foo', value: 'bar'});
+
+ // Setup request interception.
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const response = (await page.reload())!;
+ expect(response.status()).toBe(200);
+ });
+ it('should stop intercepting', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.once('request', request => {
+ return request.continue();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(false);
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should show custom HTTP headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ foo: 'bar',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ expect(request.headers()['foo']).toBe('bar');
+ void request.continue();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4337
+ it('should work with redirect inside sync XHR', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRedirect('/logo.png', '/pptr.png');
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const status = await page.evaluate(async () => {
+ const request = new XMLHttpRequest();
+ request.open('GET', '/logo.png', false); // `false` makes the request synchronous
+ request.send(null);
+ return request.status;
+ });
+ expect(status).toBe(200);
+ });
+ it('should work with custom referer headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE});
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
+ void request.continue();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ });
+ it('should be abortable', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.abort();
+ } else {
+ void request.continue();
+ }
+ });
+ let failedRequests = 0;
+ page.on('requestfailed', () => {
+ return ++failedRequests;
+ });
+ const response = (await page.goto(server.PREFIX + '/one-style.html'))!;
+ expect(response.ok()).toBe(true);
+ expect(response.request().failure()).toBe(null);
+ expect(failedRequests).toBe(1);
+ });
+ it('should be abortable with custom error codes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.abort('internetdisconnected');
+ });
+ const [failedRequest] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'requestfailed'),
+ page.goto(server.EMPTY_PAGE).catch(() => {}),
+ ]);
+
+ expect(failedRequest).toBeTruthy();
+ expect(failedRequest.failure()!.errorText).toBe(
+ 'net::ERR_INTERNET_DISCONNECTED'
+ );
+ });
+ it('should send referer', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ referer: 'http://google.com/',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/grid.html'),
+ page.goto(server.PREFIX + '/grid.html'),
+ ]);
+ expect(request.headers['referer']).toBe('http://google.com/');
+ });
+ it('should fail navigation when aborting main resource', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.abort();
+ });
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ if (isChrome) {
+ expect(error.message).toContain('net::ERR_FAILED');
+ } else {
+ expect(error.message).toContain('NS_ERROR_FAILURE');
+ }
+ });
+ it('should work with redirects', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue();
+ requests.push(request);
+ });
+ server.setRedirect(
+ '/non-existing-page.html',
+ '/non-existing-page-2.html'
+ );
+ server.setRedirect(
+ '/non-existing-page-2.html',
+ '/non-existing-page-3.html'
+ );
+ server.setRedirect(
+ '/non-existing-page-3.html',
+ '/non-existing-page-4.html'
+ );
+ server.setRedirect('/non-existing-page-4.html', '/empty.html');
+ const response = (await page.goto(
+ server.PREFIX + '/non-existing-page.html'
+ ))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toContain('empty.html');
+ expect(requests).toHaveLength(5);
+ expect(requests[2]!.resourceType()).toBe('document');
+ // Check redirect chain
+ const redirectChain = response.request().redirectChain();
+ expect(redirectChain).toHaveLength(4);
+ expect(redirectChain[0]!.url()).toContain('/non-existing-page.html');
+ expect(redirectChain[2]!.url()).toContain('/non-existing-page-3.html');
+ for (let i = 0; i < redirectChain.length; ++i) {
+ const request = redirectChain[i]!;
+ expect(request.isNavigationRequest()).toBe(true);
+ expect(request.redirectChain().indexOf(request)).toBe(i);
+ }
+ });
+ it('should work with redirects for subresources', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue();
+ if (!isFavicon(request)) {
+ requests.push(request);
+ }
+ });
+ server.setRedirect('/one-style.css', '/two-style.css');
+ server.setRedirect('/two-style.css', '/three-style.css');
+ server.setRedirect('/three-style.css', '/four-style.css');
+ server.setRoute('/four-style.css', (_req, res) => {
+ return res.end('body {box-sizing: border-box; }');
+ });
+
+ const response = (await page.goto(server.PREFIX + '/one-style.html'))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toContain('one-style.html');
+ expect(requests).toHaveLength(5);
+ expect(requests[0]!.resourceType()).toBe('document');
+ expect(requests[1]!.resourceType()).toBe('stylesheet');
+ // Check redirect chain
+ const redirectChain = requests[1]!.redirectChain();
+ expect(redirectChain).toHaveLength(3);
+ expect(redirectChain[0]!.url()).toContain('/one-style.css');
+ expect(redirectChain[2]!.url()).toContain('/three-style.css');
+ });
+ it('should be able to abort redirects', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRedirect('/non-existing.json', '/non-existing-2.json');
+ server.setRedirect('/non-existing-2.json', '/simple.html');
+ page.on('request', request => {
+ if (request.url().includes('non-existing-2')) {
+ void request.abort();
+ } else {
+ void request.continue();
+ }
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const result = await page.evaluate(async () => {
+ try {
+ return await fetch('/non-existing.json');
+ } catch (error) {
+ return (error as Error).message;
+ }
+ });
+ if (isChrome) {
+ expect(result).toContain('Failed to fetch');
+ } else {
+ expect(result).toContain('NetworkError');
+ }
+ });
+ it('should work with equal requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let responseCount = 1;
+ server.setRoute('/zzz', (_req, res) => {
+ return res.end(responseCount++ * 11 + '');
+ });
+ await page.setRequestInterception(true);
+
+ let spinner = false;
+ // Cancel 2nd request.
+ page.on('request', request => {
+ if (isFavicon(request)) {
+ void request.continue();
+ return;
+ }
+ void (spinner ? request.abort() : request.continue());
+ spinner = !spinner;
+ });
+ const results = await page.evaluate(() => {
+ return Promise.all([
+ fetch('/zzz')
+ .then(response => {
+ return response.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ fetch('/zzz')
+ .then(response => {
+ return response.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ fetch('/zzz')
+ .then(response => {
+ return response.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ ]);
+ });
+ expect(results).toEqual(['11', 'FAILED', '22']);
+ });
+ it('should navigate to dataURL and fire dataURL requests', async () => {
+ const {page} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ requests.push(request);
+ void request.continue();
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const response = (await page.goto(dataURL))!;
+ expect(response.status()).toBe(200);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should be able to fetch dataURL and fire dataURL requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ !isFavicon(request) && requests.push(request);
+ void request.continue();
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const text = await page.evaluate((url: string) => {
+ return fetch(url).then(r => {
+ return r.text();
+ });
+ }, dataURL);
+ expect(text).toBe('<div>yo</div>');
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should navigate to URL with hash and fire requests without hash', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ requests.push(request);
+ void request.continue();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toBe(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with encoded server', async () => {
+ const {page, server} = await getTestState();
+
+ // The requestWillBeSent will report encoded URL, whereas interception will
+ // report URL as-is. @see crbug.com/759388
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const response = (await page.goto(
+ server.PREFIX + '/some nonexisting page'
+ ))!;
+ expect(response.status()).toBe(404);
+ });
+ it('should work with badly encoded server', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRoute('/malformed?rnd=%911', (_req, res) => {
+ return res.end();
+ });
+ page.on('request', request => {
+ return request.continue();
+ });
+ const response = (await page.goto(
+ server.PREFIX + '/malformed?rnd=%911'
+ ))!;
+ expect(response.status()).toBe(200);
+ });
+ it('should work wit h encoded server - 2', async () => {
+ const {page, server} = await getTestState();
+
+ // The requestWillBeSent will report URL as-is, whereas interception will
+ // report encoded URL for stylesheet. @see crbug.com/759388
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue();
+ requests.push(request);
+ });
+ const response = (await page.goto(
+ `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`
+ ))!;
+ expect(response.status()).toBe(200);
+ expect(requests).toHaveLength(2);
+ expect(requests[1]!.response()!.status()).toBe(404);
+ });
+ it('should not throw "Invalid Interception Id" if the request was cancelled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setContent('<iframe></iframe>');
+ await page.setRequestInterception(true);
+ let request!: HTTPRequest;
+ page.on('request', async r => {
+ return (request = r);
+ });
+ void (page.$eval(
+ 'iframe',
+ (frame, url) => {
+ return ((frame as HTMLIFrameElement).src = url as string);
+ },
+ server.EMPTY_PAGE
+ ),
+ // Wait for request interception.
+ await waitEvent(page, 'request'));
+ // Delete frame to cause request to be canceled.
+ await page.$eval('iframe', frame => {
+ return frame.remove();
+ });
+ let error!: Error;
+ await request.continue().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ });
+ it('should throw if interception is not enabled', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ page.on('request', async request => {
+ try {
+ await request.continue();
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(error.message).toContain('Request Interception is not enabled');
+ });
+ it('should work with file URLs', async () => {
+ const {page} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const urls = new Set();
+ page.on('request', request => {
+ urls.add(request.url().split('/').pop());
+ void request.continue();
+ });
+ await page.goto(
+ pathToFileURL(path.join(__dirname, '../assets', 'one-style.html'))
+ );
+ expect(urls.size).toBe(2);
+ expect(urls.has('one-style.html')).toBe(true);
+ expect(urls.has('one-style.css')).toBe(true);
+ });
+ it('should not cache if cache disabled', async () => {
+ const {page, server} = await getTestState();
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(false);
+ page.on('request', request => {
+ return request.continue();
+ });
+
+ const cached: HTTPRequest[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r);
+ });
+
+ await page.reload();
+ expect(cached).toHaveLength(0);
+ });
+ it('should cache if cache enabled', async () => {
+ const {page, server} = await getTestState();
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+
+ const cached: HTTPRequest[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r);
+ });
+
+ await page.reload();
+ expect(cached).toHaveLength(1);
+ });
+ it('should load fonts if cache enabled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+
+ const responsePromise = page.waitForResponse(r => {
+ return r.url().endsWith('/one-style.woff');
+ });
+ await page.goto(server.PREFIX + '/cached/one-style-font.html');
+ await responsePromise;
+ });
+ });
+
+ describe('Request.continue', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should amend HTTP headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers());
+ headers['FOO'] = 'bar';
+ void request.continue({headers});
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz');
+ }),
+ ]);
+ expect(request.headers['foo']).toBe('bar');
+ });
+ it('should redirect in a way non-observable to page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const redirectURL = request.url().includes('/empty.html')
+ ? server.PREFIX + '/consolelog.html'
+ : undefined;
+ void request.continue({url: redirectURL});
+ });
+ const [consoleMessage] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ expect(consoleMessage.text()).toBe('yellow');
+ });
+ it('should amend method', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({method: 'POST'});
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz');
+ }),
+ ]);
+ expect(request.method).toBe('POST');
+ });
+ it('should amend post data', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({postData: 'doggo'});
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz', {method: 'POST', body: 'birdy'});
+ }),
+ ]);
+ expect(await serverRequest.postBody).toBe('doggo');
+ });
+ it('should amend both post data and method on navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({method: 'POST', postData: 'doggo'});
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(serverRequest.method).toBe('POST');
+ expect(await serverRequest.postBody).toBe('doggo');
+ });
+ it('should fail if the header value is invalid', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ await page.setRequestInterception(true);
+ page.on('request', async request => {
+ await request
+ .continue({
+ headers: {
+ 'X-Invalid-Header': 'a\nb',
+ },
+ })
+ .catch(error_ => {
+ error = error_ as Error;
+ });
+ await request.continue();
+ });
+ await page.goto(server.PREFIX + '/empty.html');
+ expect(error.message).toMatch(/Invalid header/);
+ });
+ });
+
+ describe('Request.respond', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond({
+ status: 201,
+ headers: {
+ foo: 'bar',
+ },
+ body: 'Yo, page!',
+ });
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(201);
+ expect(response.headers()['foo']).toBe('bar');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should work with status code 422', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond({
+ status: 422,
+ body: 'Yo, page!',
+ });
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(422);
+ expect(response.statusText()).toBe('Unprocessable Entity');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should redirect', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (!request.url().includes('rrredirect')) {
+ void request.continue();
+ return;
+ }
+ void request.respond({
+ status: 302,
+ headers: {
+ location: server.EMPTY_PAGE,
+ },
+ });
+ });
+ const response = (await page.goto(server.PREFIX + '/rrredirect'))!;
+ expect(response.request().redirectChain()).toHaveLength(1);
+ expect(response.request().redirectChain()[0]!.url()).toBe(
+ server.PREFIX + '/rrredirect'
+ );
+ expect(response.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should allow mocking multiple headers with same key', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond({
+ status: 200,
+ headers: {
+ foo: 'bar',
+ arr: ['1', '2'],
+ 'set-cookie': ['first=1', 'second=2'],
+ },
+ body: 'Hello world',
+ });
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ const cookies = await page.cookies();
+ const firstCookie = cookies.find(cookie => {
+ return cookie.name === 'first';
+ });
+ const secondCookie = cookies.find(cookie => {
+ return cookie.name === 'second';
+ });
+ expect(response.status()).toBe(200);
+ expect(response.headers()['foo']).toBe('bar');
+ expect(response.headers()['arr']).toBe('1\n2');
+ // request.respond() will not trigger Network.responseReceivedExtraInfo
+ // fail to get 'set-cookie' header from response
+ expect(firstCookie?.value).toBe('1');
+ expect(secondCookie?.value).toBe('2');
+ });
+ it('should allow mocking binary responses', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, '../assets', 'pptr.png')
+ );
+ void request.respond({
+ contentType: 'image/png',
+ body: imageBuffer,
+ });
+ });
+ await page.evaluate(PREFIX => {
+ const img = document.createElement('img');
+ img.src = PREFIX + '/does-not-exist.png';
+ document.body.appendChild(img);
+ return new Promise(fulfill => {
+ return (img.onload = fulfill);
+ });
+ }, server.PREFIX);
+ using img = (await page.$('img'))!;
+ expect(await img.screenshot()).toBeGolden('mock-binary-response.png');
+ });
+ it('should stringify intercepted request response headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond({
+ status: 200,
+ headers: {
+ foo: true,
+ },
+ body: 'Yo, page!',
+ });
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(200);
+ const headers = response.headers();
+ expect(headers['foo']).toBe('true');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should fail if the header value is invalid', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ await page.setRequestInterception(true);
+ page.on('request', async request => {
+ await request
+ .respond({
+ headers: {
+ 'X-Invalid-Header': 'a\nb',
+ },
+ })
+ .catch(error_ => {
+ error = error_ as Error;
+ });
+ await request.respond({
+ status: 200,
+ body: 'Hello World',
+ });
+ });
+ await page.goto(server.PREFIX + '/empty.html');
+ expect(error.message).toMatch(/Invalid header/);
+ });
+ });
+});
+
+function pathToFileURL(path: string): string {
+ let pathName = path.replace(/\\/g, '/');
+ // Windows drive letter must be prefixed with a slash.
+ if (!pathName.startsWith('/')) {
+ pathName = '/' + pathName;
+ }
+ return 'file://' + pathName;
+}
diff --git a/remote/test/puppeteer/test/src/screencast.spec.ts b/remote/test/puppeteer/test/src/screencast.spec.ts
new file mode 100644
index 0000000000..b645f55da7
--- /dev/null
+++ b/remote/test/puppeteer/test/src/screencast.spec.ts
@@ -0,0 +1,99 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {statSync} from 'fs';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {getUniqueVideoFilePlaceholder} from './utils.js';
+
+describe('Screencasts', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.screencast', function () {
+ it('should work', async () => {
+ using file = getUniqueVideoFilePlaceholder();
+
+ const {page} = await getTestState();
+
+ const recorder = await page.screencast({
+ path: file.filename,
+ scale: 0.5,
+ crop: {width: 100, height: 100, x: 0, y: 0},
+ speed: 0.5,
+ });
+
+ await page.goto('data:text/html,<input>');
+ using input = await page.locator('input').waitHandle();
+ await input.type('ab', {delay: 100});
+
+ await recorder.stop();
+
+ expect(statSync(file.filename).size).toBeGreaterThan(0);
+ });
+ it('should work concurrently', async () => {
+ using file1 = getUniqueVideoFilePlaceholder();
+ using file2 = getUniqueVideoFilePlaceholder();
+
+ const {page} = await getTestState();
+
+ const recorder = await page.screencast({path: file1.filename});
+ const recorder2 = await page.screencast({path: file2.filename});
+
+ await page.goto('data:text/html,<input>');
+ using input = await page.locator('input').waitHandle();
+
+ await input.type('ab', {delay: 100});
+ await recorder.stop();
+
+ await input.type('ab', {delay: 100});
+ await recorder2.stop();
+
+ // Since file2 spent about double the time of file1 recording, so file2
+ // should be around double the size of file1.
+ const ratio =
+ statSync(file2.filename).size / statSync(file1.filename).size;
+
+ // We use a range because we cannot be precise.
+ const DELTA = 1.3;
+ expect(ratio).toBeGreaterThan(2 - DELTA);
+ expect(ratio).toBeLessThan(2 + DELTA);
+ });
+ it('should validate options', async () => {
+ const {page} = await getTestState();
+
+ await expect(page.screencast({scale: 0})).rejects.toBeDefined();
+ await expect(page.screencast({scale: -1})).rejects.toBeDefined();
+
+ await expect(page.screencast({speed: 0})).rejects.toBeDefined();
+ await expect(page.screencast({speed: -1})).rejects.toBeDefined();
+
+ await expect(
+ page.screencast({crop: {x: 0, y: 0, height: 1, width: 0}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: 0, y: 0, height: 0, width: 1}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: -1, y: 0, height: 1, width: 1}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: 0, y: -1, height: 1, width: 1}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: 0, y: 0, height: 10000, width: 1}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: 0, y: 0, height: 1, width: 10000}})
+ ).rejects.toBeDefined();
+
+ await expect(
+ page.screencast({ffmpegPath: 'non-existent-path'})
+ ).rejects.toBeDefined();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/screenshot.spec.ts b/remote/test/puppeteer/test/src/screenshot.spec.ts
new file mode 100644
index 0000000000..ad53b60e95
--- /dev/null
+++ b/remote/test/puppeteer/test/src/screenshot.spec.ts
@@ -0,0 +1,453 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+
+import {
+ getTestState,
+ isHeadless,
+ launch,
+ setupTestBrowserHooks,
+} from './mocha-utils.js';
+
+describe('Screenshots', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.screenshot', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('screenshot-sanity.png');
+ });
+ it('should clip rect', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 50,
+ y: 100,
+ width: 150,
+ height: 100,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-clip-rect.png');
+ });
+ it('should get screenshot bigger than the viewport', async () => {
+ const {page, server} = await getTestState();
+ await page.setViewport({width: 50, height: 50});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 25,
+ y: 25,
+ width: 100,
+ height: 100,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
+ });
+ it('should clip clip bigger than the viewport without "captureBeyondViewport"', async () => {
+ const {page, server} = await getTestState();
+ await page.setViewport({width: 50, height: 50});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ captureBeyondViewport: false,
+ clip: {
+ x: 25,
+ y: 25,
+ width: 100,
+ height: 100,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-offscreen-clip-2.png');
+ });
+ it('should run in parallel', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const promises = [];
+ for (let i = 0; i < 3; ++i) {
+ promises.push(
+ page.screenshot({
+ clip: {
+ x: 50 * i,
+ y: 0,
+ width: 50,
+ height: 50,
+ },
+ })
+ );
+ }
+ const screenshots = await Promise.all(promises);
+ expect(screenshots[1]).toBeGolden('grid-cell-1.png');
+ });
+ it('should take fullPage screenshots', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ });
+ expect(screenshot).toBeGolden('screenshot-grid-fullpage.png');
+ });
+ it('should take fullPage screenshots without captureBeyondViewport', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ captureBeyondViewport: false,
+ });
+ expect(screenshot).toBeGolden('screenshot-grid-fullpage-2.png');
+ expect(page.viewport()).toMatchObject({width: 500, height: 500});
+ });
+ it('should run in parallel in multiple pages', async () => {
+ const {server, context} = await getTestState();
+
+ const N = 2;
+ const pages = await Promise.all(
+ Array(N)
+ .fill(0)
+ .map(async () => {
+ const page = await context.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ return page;
+ })
+ );
+ const promises = [];
+ for (let i = 0; i < N; ++i) {
+ promises.push(
+ pages[i]!.screenshot({
+ clip: {x: 50 * i, y: 0, width: 50, height: 50},
+ })
+ );
+ }
+ const screenshots = await Promise.all(promises);
+ for (let i = 0; i < N; ++i) {
+ expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
+ }
+ await Promise.all(
+ pages.map(page => {
+ return page.close();
+ })
+ );
+ });
+ it('should work with odd clip size on Retina displays', async () => {
+ const {page} = await getTestState();
+
+ // Make sure documentElement height is at least 11px.
+ await page.setContent(`<div style="width: 11px; height: 11px;">`);
+
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: 11,
+ height: 11,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-clip-odd-size.png');
+ });
+ it('should return base64', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ encoding: 'base64',
+ });
+ expect(Buffer.from(screenshot, 'base64')).toBeGolden(
+ 'screenshot-sanity.png'
+ );
+ });
+ });
+
+ describe('ElementHandle.screenshot', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.evaluate(() => {
+ return window.scrollBy(50, 100);
+ });
+ using elementHandle = (await page.$('.box:nth-of-type(3)'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-bounding-box.png');
+ });
+ it('should work with a null viewport', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const {browser, close} = await launch({
+ defaultViewport: null,
+ });
+
+ try {
+ const page = await browser.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.evaluate(() => {
+ return window.scrollBy(50, 100);
+ });
+ using elementHandle = await page.$('.box:nth-of-type(3)');
+ assert(elementHandle);
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeTruthy();
+ } finally {
+ await close();
+ }
+ });
+ it('should take into account padding and border', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ something above
+ <style>div {
+ border: 2px solid blue;
+ background: green;
+ width: 50px;
+ height: 50px;
+ }
+ </style>
+ <div></div>
+ `);
+ using elementHandle = (await page.$('div'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
+ });
+ it('should capture full element when larger than viewport', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+
+ await page.setContent(`
+ something above
+ <style>
+ :root {
+ scrollbar-width: none;
+ }
+ div.to-screenshot {
+ border: 1px solid blue;
+ width: 600px;
+ height: 600px;
+ margin-left: 50px;
+ }
+ </style>
+ <div class="to-screenshot"></div>
+ `);
+ using elementHandle = (await page.$('div.to-screenshot'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden(
+ 'screenshot-element-larger-than-viewport.png'
+ );
+
+ expect(
+ await page.evaluate(() => {
+ return {
+ w: window.innerWidth,
+ h: window.innerHeight,
+ };
+ })
+ ).toEqual({w: 500, h: 500});
+ });
+ it('should scroll element into view', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ something above
+ <style>div.above {
+ border: 2px solid blue;
+ background: red;
+ height: 1500px;
+ }
+ div.to-screenshot {
+ border: 2px solid blue;
+ background: green;
+ width: 50px;
+ height: 50px;
+ }
+ </style>
+ <div class="above"></div>
+ <div class="to-screenshot"></div>
+ `);
+ using elementHandle = (await page.$('div.to-screenshot'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden(
+ 'screenshot-element-scrolled-into-view.png'
+ );
+ });
+ it('should work with a rotated element', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`<div style="position:absolute;
+ top: 100px;
+ left: 100px;
+ width: 100px;
+ height: 100px;
+ background: green;
+ transform: rotateZ(200deg);">&nbsp;</div>`);
+ using elementHandle = (await page.$('div'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-rotate.png');
+ });
+ it('should fail to screenshot a detached element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<h1>remove this</h1>');
+ using elementHandle = (await page.$('h1'))!;
+ await page.evaluate((element: HTMLElement) => {
+ return element.remove();
+ }, elementHandle);
+ const screenshotError = await elementHandle.screenshot().catch(error => {
+ return error;
+ });
+ expect(screenshotError.message).toBe(
+ 'Node is either not visible or not an HTMLElement'
+ );
+ });
+ it('should not hang with zero width/height element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div style="width: 50px; height: 0"></div>');
+ using div = (await page.$('div'))!;
+ const error = await div.screenshot().catch(error_ => {
+ return error_;
+ });
+ expect(error.message).toBe('Node has 0 height.');
+ });
+ it('should work for an element with fractional dimensions', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div style="width:48.51px;height:19.8px;border:1px solid black;"></div>'
+ );
+ using elementHandle = (await page.$('div'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-fractional.png');
+ });
+ it('should work for an element with an offset', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div style="position:absolute; top: 10.3px; left: 20.4px;width:50.3px;height:20.2px;border:1px solid black;"></div>'
+ );
+ using elementHandle = (await page.$('div'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png');
+ });
+ it('should work with webp', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 100, height: 100});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ type: 'webp',
+ });
+
+ expect(screenshot).toBeInstanceOf(Buffer);
+ });
+
+ it('should run in parallel in multiple pages', async () => {
+ const {browser, server} = await getTestState();
+
+ const context = await browser.createIncognitoBrowserContext();
+
+ const N = 2;
+ const pages = await Promise.all(
+ Array(N)
+ .fill(0)
+ .map(async () => {
+ const page = await context.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ return page;
+ })
+ );
+ const promises = [];
+ for (let i = 0; i < N; ++i) {
+ promises.push(
+ pages[i]!.screenshot({
+ clip: {x: 50 * i, y: 0, width: 50, height: 50},
+ })
+ );
+ }
+ const screenshots = await Promise.all(promises);
+ for (let i = 0; i < N; ++i) {
+ expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
+ }
+ await Promise.all(
+ pages.map(page => {
+ return page.close();
+ })
+ );
+
+ await context.close();
+ });
+ });
+
+ describe('Cdp', () => {
+ it('should use scale for clip', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 50,
+ y: 100,
+ width: 150,
+ height: 100,
+ scale: 2,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png');
+ });
+ it('should allow transparency', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 100, height: 100});
+ await page.goto(server.EMPTY_PAGE);
+ const screenshot = await page.screenshot({omitBackground: true});
+ expect(screenshot).toBeGolden('transparent.png');
+ });
+ it('should render white background on jpeg file', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 100, height: 100});
+ await page.goto(server.EMPTY_PAGE);
+ const screenshot = await page.screenshot({
+ omitBackground: true,
+ type: 'jpeg',
+ });
+ expect(screenshot).toBeGolden('white.jpg');
+ });
+ (!isHeadless ? it : it.skip)(
+ 'should work in "fromSurface: false" mode',
+ async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fromSurface: false,
+ });
+ expect(screenshot).toBeDefined(); // toBeGolden('screenshot-fromsurface-false.png');
+ }
+ );
+ });
+});
diff --git a/remote/test/puppeteer/test/src/stacktrace.spec.ts b/remote/test/puppeteer/test/src/stacktrace.spec.ts
new file mode 100644
index 0000000000..b36ee56661
--- /dev/null
+++ b/remote/test/puppeteer/test/src/stacktrace.spec.ts
@@ -0,0 +1,157 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+const FILENAME = __filename.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
+const parseStackTrace = (stack: string): string => {
+ stack = stack.replace(new RegExp(FILENAME, 'g'), '<filename>');
+ stack = stack.replace(/<filename>:(\d+):(\d+)/g, '<filename>:<line>:<col>');
+ stack = stack.replace(/<anonymous>:(\d+):(\d+)/g, '<anonymous>:<line>:<col>');
+ return stack;
+};
+
+describe('Stack trace', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const error = (await page
+ .evaluate(() => {
+ throw new Error('Test');
+ })
+ .catch((error: Error) => {
+ return error;
+ })) as Error;
+
+ expect(error.name).toEqual('Error');
+ expect(error.message).toEqual('Test');
+ assert(error.stack);
+ error.stack = error.stack.replace(new RegExp(FILENAME, 'g'), '<filename>');
+ expect(
+ parseStackTrace(error.stack).split('\n at ').slice(0, 2)
+ ).toMatchObject({
+ ...[
+ 'Error: Test',
+ 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ ],
+ });
+ });
+
+ it('should work with handles', async () => {
+ const {page} = await getTestState();
+
+ const error = (await page
+ .evaluateHandle(() => {
+ throw new Error('Test');
+ })
+ .catch((error: Error) => {
+ return error;
+ })) as Error;
+
+ expect(error.name).toEqual('Error');
+ expect(error.message).toEqual('Test');
+ assert(error.stack);
+ expect(
+ parseStackTrace(error.stack).split('\n at ').slice(0, 2)
+ ).toMatchObject({
+ ...[
+ 'Error: Test',
+ 'evaluateHandle (evaluateHandle at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ ],
+ });
+ });
+
+ it('should work with contiguous evaluation', async () => {
+ const {page} = await getTestState();
+
+ using thrower = await page.evaluateHandle(() => {
+ return () => {
+ throw new Error('Test');
+ };
+ });
+ const error = (await thrower
+ .evaluate(thrower => {
+ thrower();
+ })
+ .catch((error: Error) => {
+ return error;
+ })) as Error;
+
+ expect(error.name).toEqual('Error');
+ expect(error.message).toEqual('Test');
+ assert(error.stack);
+ expect(
+ parseStackTrace(error.stack).split('\n at ').slice(0, 3)
+ ).toMatchObject({
+ ...[
+ 'Error: Test',
+ 'evaluateHandle (evaluateHandle at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ ],
+ });
+ });
+
+ it('should work with nested function calls', async () => {
+ const {page} = await getTestState();
+
+ const error = (await page
+ .evaluate(() => {
+ function a() {
+ throw new Error('Test');
+ }
+ function b() {
+ a();
+ }
+ function c() {
+ b();
+ }
+ function d() {
+ c();
+ }
+ d();
+ })
+ .catch((error: Error) => {
+ return error;
+ })) as Error;
+
+ expect(error.name).toEqual('Error');
+ expect(error.message).toEqual('Test');
+ assert(error.stack);
+ expect(
+ parseStackTrace(error.stack).split('\n at ').slice(0, 6)
+ ).toMatchObject({
+ ...[
+ 'Error: Test',
+ 'a (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'b (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'c (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'd (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ ],
+ });
+ });
+
+ it('should work for none error objects', async () => {
+ const {page} = await getTestState();
+
+ const [error] = await Promise.all([
+ waitEvent<Error>(page, 'pageerror'),
+ page.evaluate(() => {
+ // This can happen when a 404 with HTML is returned
+ void Promise.reject(new Response());
+ }),
+ ]);
+
+ expect(error).toBeTruthy();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/target.spec.ts b/remote/test/puppeteer/test/src/target.spec.ts
new file mode 100644
index 0000000000..28d17a4030
--- /dev/null
+++ b/remote/test/puppeteer/test/src/target.spec.ts
@@ -0,0 +1,343 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ServerResponse} from 'http';
+
+import expect from 'expect';
+import {type Target, TimeoutError} from 'puppeteer';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+describe('Target', function () {
+ setupTestBrowserHooks();
+
+ it('Browser.targets should return all of the targets', async () => {
+ const {browser} = await getTestState();
+
+ // The pages will be the testing page and the original newtab page
+ const targets = browser.targets();
+ expect(
+ targets.some(target => {
+ return target.type() === 'page' && target.url() === 'about:blank';
+ })
+ ).toBeTruthy();
+ expect(
+ targets.some(target => {
+ return target.type() === 'browser';
+ })
+ ).toBeTruthy();
+ });
+ it('Browser.pages should return all of the pages', async () => {
+ const {page, context} = await getTestState();
+
+ // The pages will be the testing page
+ const allPages = await context.pages();
+ expect(allPages).toHaveLength(1);
+ expect(allPages).toContain(page);
+ });
+ it('should contain browser target', async () => {
+ const {browser} = await getTestState();
+
+ const targets = browser.targets();
+ const browserTarget = targets.find(target => {
+ return target.type() === 'browser';
+ });
+ expect(browserTarget).toBeTruthy();
+ });
+ it('should be able to use the default page in the browser', async () => {
+ const {page, browser} = await getTestState();
+
+ // The pages will be the testing page and the original newtab page
+ const allPages = await browser.pages();
+ const originalPage = allPages.find(p => {
+ return p !== page;
+ })!;
+ expect(
+ await originalPage.evaluate(() => {
+ return ['Hello', 'world'].join(' ');
+ })
+ ).toBe('Hello world');
+ expect(await originalPage.$('body')).toBeTruthy();
+ });
+ it('should be able to use async waitForTarget', async () => {
+ const {page, server, context} = await getTestState();
+
+ const [otherPage] = await Promise.all([
+ context
+ .waitForTarget(
+ target => {
+ return target.page().then(page => {
+ return (
+ page!.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ });
+ },
+ {timeout: 3000}
+ )
+ .then(target => {
+ return target.page();
+ }),
+ page.evaluate((url: string) => {
+ return window.open(url);
+ }, server.CROSS_PROCESS_PREFIX + '/empty.html'),
+ ]);
+ expect(otherPage!.url()).toEqual(
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ expect(page).not.toBe(otherPage);
+ });
+ it('should report when a new page is created and closed', async () => {
+ const {page, server, context} = await getTestState();
+
+ const [otherPage] = await Promise.all([
+ context
+ .waitForTarget(
+ target => {
+ return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
+ },
+ {timeout: 3000}
+ )
+ .then(target => {
+ return target.page();
+ }),
+ page.evaluate((url: string) => {
+ return window.open(url);
+ }, server.CROSS_PROCESS_PREFIX + '/empty.html'),
+ ]);
+ expect(otherPage!.url()).toContain(server.CROSS_PROCESS_PREFIX);
+ expect(
+ await otherPage!.evaluate(() => {
+ return ['Hello', 'world'].join(' ');
+ })
+ ).toBe('Hello world');
+ expect(await otherPage!.$('body')).toBeTruthy();
+
+ let allPages = await context.pages();
+ expect(allPages).toContain(page);
+ expect(allPages).toContain(otherPage);
+
+ const [closedTarget] = await Promise.all([
+ waitEvent<Target>(context, 'targetdestroyed'),
+ otherPage!.close(),
+ ]);
+ expect(await closedTarget.page()).toBe(otherPage);
+
+ allPages = (await Promise.all(
+ context.targets().map(target => {
+ return target.page();
+ })
+ )) as Page[];
+ expect(allPages).toContain(page);
+ expect(allPages).not.toContain(otherPage);
+ });
+ it('should report when a service worker is created and destroyed', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const createdTarget = waitEvent(context, 'targetcreated');
+
+ await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
+
+ expect((await createdTarget).type()).toBe('service_worker');
+ expect((await createdTarget).url()).toBe(
+ server.PREFIX + '/serviceworkers/empty/sw.js'
+ );
+
+ const destroyedTarget = waitEvent(context, 'targetdestroyed');
+ await page.evaluate(() => {
+ return (
+ globalThis as unknown as {
+ registrationPromise: Promise<{unregister: () => void}>;
+ }
+ ).registrationPromise.then((registration: any) => {
+ return registration.unregister();
+ });
+ });
+ expect(await destroyedTarget).toBe(await createdTarget);
+ });
+ it('should create a worker from a service worker', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
+
+ const target = await context.waitForTarget(
+ target => {
+ return target.type() === 'service_worker';
+ },
+ {timeout: 3000}
+ );
+ const worker = (await target.worker())!;
+
+ expect(
+ await worker.evaluate(() => {
+ return self.toString();
+ })
+ ).toBe('[object ServiceWorkerGlobalScope]');
+ });
+ it('should create a worker from a shared worker', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ new SharedWorker('data:text/javascript,console.log("hi")');
+ });
+ const target = await context.waitForTarget(
+ target => {
+ return target.type() === 'shared_worker';
+ },
+ {timeout: 3000}
+ );
+ const worker = (await target.worker())!;
+ expect(
+ await worker.evaluate(() => {
+ return self.toString();
+ })
+ ).toBe('[object SharedWorkerGlobalScope]');
+ });
+ it('should report when a target url changes', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let changedTarget = waitEvent(context, 'targetchanged');
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/');
+ expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/');
+
+ changedTarget = waitEvent(context, 'targetchanged');
+ await page.goto(server.EMPTY_PAGE);
+ expect((await changedTarget).url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should not report uninitialized pages', async () => {
+ const {context} = await getTestState();
+
+ let targetChanged = false;
+ const listener = () => {
+ targetChanged = true;
+ };
+ context.on('targetchanged', listener);
+ const targetPromise = waitEvent<Target>(context, 'targetcreated');
+ const newPagePromise = context.newPage();
+ const target = await targetPromise;
+ expect(target.url()).toBe('about:blank');
+
+ const newPage = await newPagePromise;
+ const targetPromise2 = waitEvent<Target>(context, 'targetcreated');
+ const evaluatePromise = newPage.evaluate(() => {
+ return window.open('about:blank');
+ });
+ const target2 = await targetPromise2;
+ expect(target2.url()).toBe('about:blank');
+ await evaluatePromise;
+ await newPage.close();
+ expect(targetChanged).toBe(false);
+ context.off('targetchanged', listener);
+ });
+
+ it('should not crash while redirecting if original request was missed', async () => {
+ const {page, server, context} = await getTestState();
+
+ let serverResponse!: ServerResponse;
+ server.setRoute('/one-style.css', (_req, res) => {
+ return (serverResponse = res);
+ });
+ // Open a new page. Use window.open to connect to the page later.
+ await Promise.all([
+ page.evaluate((url: string) => {
+ return window.open(url);
+ }, server.PREFIX + '/one-style.html'),
+ server.waitForRequest('/one-style.css'),
+ ]);
+ // Connect to the opened page.
+ const target = await context.waitForTarget(
+ target => {
+ return target.url().includes('one-style.html');
+ },
+ {timeout: 3000}
+ );
+ const newPage = (await target.page())!;
+ const loadEvent = waitEvent(newPage, 'load');
+ // Issue a redirect.
+ serverResponse.writeHead(302, {location: '/injectedstyle.css'});
+ serverResponse.end();
+ // Wait for the new page to load.
+ await loadEvent;
+ // Cleanup.
+ await newPage.close();
+ });
+ it('should have an opener', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [createdTarget] = await Promise.all([
+ waitEvent<Target>(context, 'targetcreated'),
+ page.goto(server.PREFIX + '/popup/window-open.html'),
+ ]);
+ expect((await createdTarget.page())!.url()).toBe(
+ server.PREFIX + '/popup/popup.html'
+ );
+ expect(createdTarget.opener()).toBe(page.target());
+ expect(page.target().opener()).toBeUndefined();
+ });
+
+ describe('Browser.waitForTarget', () => {
+ it('should wait for a target', async () => {
+ const {browser, server} = await getTestState();
+
+ let resolved = false;
+ const targetPromise = browser.waitForTarget(
+ target => {
+ return target.url() === server.EMPTY_PAGE;
+ },
+ {timeout: 3000}
+ );
+ targetPromise
+ .then(() => {
+ return (resolved = true);
+ })
+ .catch(error => {
+ resolved = true;
+ if (error instanceof TimeoutError) {
+ console.error(error);
+ } else {
+ throw error;
+ }
+ });
+ const page = await browser.newPage();
+ expect(resolved).toBe(false);
+ await page.goto(server.EMPTY_PAGE);
+ try {
+ const target = await targetPromise;
+ expect(await target.page()).toBe(page);
+ } catch (error) {
+ if (error instanceof TimeoutError) {
+ console.error(error);
+ } else {
+ throw error;
+ }
+ }
+ await page.close();
+ });
+ it('should timeout waiting for a non-existent target', async () => {
+ const {browser, server} = await getTestState();
+
+ let error!: Error;
+ await browser
+ .waitForTarget(
+ target => {
+ return target.url() === server.PREFIX + '/does-not-exist.html';
+ },
+ {
+ timeout: 1,
+ }
+ )
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/touchscreen.spec.ts b/remote/test/puppeteer/test/src/touchscreen.spec.ts
new file mode 100644
index 0000000000..28a18ec449
--- /dev/null
+++ b/remote/test/puppeteer/test/src/touchscreen.spec.ts
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+declare const allEvents: Array<{type: string}>;
+
+describe('Touchscreen', () => {
+ setupTestBrowserHooks();
+
+ describe('Touchscreen.prototype.tap', () => {
+ it('should work', async () => {
+ const {page, server, isHeadless} = await getTestState();
+ await page.goto(server.PREFIX + '/input/touchscreen.html');
+
+ await page.tap('button');
+ expect(
+ (
+ await page.evaluate(() => {
+ return allEvents;
+ })
+ ).filter(({type}) => {
+ return type !== 'pointermove' || isHeadless;
+ })
+ ).toMatchObject([
+ {height: 1, type: 'pointerdown', width: 1, x: 5, y: 5},
+ {touches: [[5, 5, 0.5, 0.5]], type: 'touchstart'},
+ {height: 1, type: 'pointerup', width: 1, x: 5, y: 5},
+ {touches: [[5, 5, 0.5, 0.5]], type: 'touchend'},
+ {height: 1, type: 'click', width: 1, x: 5, y: 5},
+ ]);
+ });
+ });
+
+ describe('Touchscreen.prototype.touchMove', () => {
+ it('should work', async () => {
+ const {page, server, isHeadless} = await getTestState();
+ await page.goto(server.PREFIX + '/input/touchscreen.html');
+
+ await page.touchscreen.touchStart(0, 0);
+ await page.touchscreen.touchMove(10, 10);
+ await page.touchscreen.touchMove(15.5, 15);
+ await page.touchscreen.touchMove(20, 20.4);
+ await page.touchscreen.touchMove(40, 30);
+ await page.touchscreen.touchEnd();
+ expect(
+ (
+ await page.evaluate(() => {
+ return allEvents;
+ })
+ ).filter(({type}) => {
+ return type !== 'pointermove' || isHeadless;
+ })
+ ).toMatchObject(
+ [
+ {type: 'pointerdown', x: 0, y: 0, width: 1, height: 1},
+ {type: 'touchstart', touches: [[0, 0, 0.5, 0.5]]},
+ {type: 'pointermove', x: 10, y: 10, width: 1, height: 1},
+ {type: 'touchmove', touches: [[10, 10, 0.5, 0.5]]},
+ {type: 'pointermove', x: 16, y: 15, width: 1, height: 1},
+ {type: 'touchmove', touches: [[16, 15, 0.5, 0.5]]},
+ {type: 'pointermove', x: 20, y: 20, width: 1, height: 1},
+ {type: 'touchmove', touches: [[20, 20, 0.5, 0.5]]},
+ {type: 'pointermove', x: 40, y: 30, width: 1, height: 1},
+ {type: 'touchmove', touches: [[40, 30, 0.5, 0.5]]},
+ {type: 'pointerup', x: 40, y: 30, width: 1, height: 1},
+ {type: 'touchend', touches: [[40, 30, 0.5, 0.5]]},
+ ].filter(({type}) => {
+ return type !== 'pointermove' || isHeadless;
+ })
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/tracing.spec.ts b/remote/test/puppeteer/test/src/tracing.spec.ts
new file mode 100644
index 0000000000..2c0a5aff19
--- /dev/null
+++ b/remote/test/puppeteer/test/src/tracing.spec.ts
@@ -0,0 +1,149 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import expect from 'expect';
+
+import {launch} from './mocha-utils.js';
+
+describe('Tracing', function () {
+ let outputFile!: string;
+ let testState: Awaited<ReturnType<typeof launch>>;
+
+ /* we manually manage the browser here as we want a new browser for each
+ * individual test, which isn't the default behaviour of getTestState()
+ */
+ beforeEach(async () => {
+ testState = await launch({});
+ outputFile = path.join(__dirname, 'trace.json');
+ });
+
+ afterEach(async () => {
+ await testState.close();
+ if (fs.existsSync(outputFile)) {
+ fs.unlinkSync(outputFile);
+ }
+ });
+
+ it('should output a trace', async () => {
+ const {server, page} = testState;
+ await page.tracing.start({screenshots: true, path: outputFile});
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.tracing.stop();
+ expect(fs.existsSync(outputFile)).toBe(true);
+ });
+
+ it('should run with custom categories if provided', async () => {
+ const {page} = testState;
+ await page.tracing.start({
+ path: outputFile,
+ categories: ['-*', 'disabled-by-default-devtools.timeline.frame'],
+ });
+ await page.tracing.stop();
+
+ const traceJson = JSON.parse(
+ fs.readFileSync(outputFile, {encoding: 'utf8'})
+ );
+ const traceConfig = JSON.parse(traceJson.metadata['trace-config']);
+ expect(traceConfig.included_categories).toEqual([
+ 'disabled-by-default-devtools.timeline.frame',
+ ]);
+ expect(traceConfig.excluded_categories).toEqual(['*']);
+ expect(traceJson.traceEvents).not.toContainEqual(
+ expect.objectContaining({
+ cat: 'toplevel',
+ })
+ );
+ });
+
+ it('should run with default categories', async () => {
+ const {page} = testState;
+ await page.tracing.start({
+ path: outputFile,
+ });
+ await page.tracing.stop();
+
+ const traceJson = JSON.parse(
+ fs.readFileSync(outputFile, {encoding: 'utf8'})
+ );
+ expect(traceJson.traceEvents).toContainEqual(
+ expect.objectContaining({
+ cat: 'toplevel',
+ })
+ );
+ });
+ it('should throw if tracing on two pages', async () => {
+ const {page, browser} = testState;
+ await page.tracing.start({path: outputFile});
+ const newPage = await browser.newPage();
+ let error!: Error;
+ await newPage.tracing.start({path: outputFile}).catch(error_ => {
+ return (error = error_);
+ });
+ await newPage.close();
+ expect(error).toBeTruthy();
+ await page.tracing.stop();
+ });
+ it('should return a buffer', async () => {
+ const {page, server} = testState;
+
+ await page.tracing.start({screenshots: true, path: outputFile});
+ await page.goto(server.PREFIX + '/grid.html');
+ const trace = (await page.tracing.stop())!;
+ const buf = fs.readFileSync(outputFile);
+ expect(trace.toString()).toEqual(buf.toString());
+ });
+ it('should work without options', async () => {
+ const {page, server} = testState;
+
+ await page.tracing.start();
+ await page.goto(server.PREFIX + '/grid.html');
+ const trace = await page.tracing.stop();
+ expect(trace).toBeTruthy();
+ });
+
+ it('should return undefined in case of Buffer error', async () => {
+ const {page, server} = testState;
+
+ await page.tracing.start({screenshots: true});
+ await page.goto(server.PREFIX + '/grid.html');
+
+ const oldBufferConcat = Buffer.concat;
+ try {
+ Buffer.concat = () => {
+ throw new Error('error');
+ };
+ const trace = await page.tracing.stop();
+ expect(trace).toEqual(undefined);
+ } finally {
+ Buffer.concat = oldBufferConcat;
+ }
+ });
+
+ it('should support a buffer without a path', async () => {
+ const {page, server} = testState;
+
+ await page.tracing.start({screenshots: true});
+ await page.goto(server.PREFIX + '/grid.html');
+ const trace = (await page.tracing.stop())!;
+ expect(trace.toString()).toContain('screenshot');
+ });
+
+ it('should properly fail if readProtocolStream errors out', async () => {
+ const {page} = testState;
+ await page.tracing.start({path: __dirname});
+
+ let error!: Error;
+ try {
+ await page.tracing.stop();
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error).toBeDefined();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/utils.ts b/remote/test/puppeteer/test/src/utils.ts
new file mode 100644
index 0000000000..d1bad65a16
--- /dev/null
+++ b/remote/test/puppeteer/test/src/utils.ts
@@ -0,0 +1,171 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {rm} from 'fs/promises';
+import {tmpdir} from 'os';
+import path from 'path';
+
+import expect from 'expect';
+import type {Frame} from 'puppeteer-core/internal/api/Frame.js';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+import type {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js';
+import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
+
+import {compare} from './golden-utils.js';
+
+const PROJECT_ROOT = path.join(__dirname, '..', '..');
+
+declare module 'expect' {
+ interface Matchers<R> {
+ toBeGolden(pathOrBuffer: string | Buffer): R;
+ }
+}
+
+export const extendExpectWithToBeGolden = (
+ goldenDir: string,
+ outputDir: string
+): void => {
+ expect.extend({
+ toBeGolden: (testScreenshot: string | Buffer, goldenFilePath: string) => {
+ const result = compare(
+ goldenDir,
+ outputDir,
+ testScreenshot,
+ goldenFilePath
+ );
+
+ if (result.pass) {
+ return {
+ pass: true,
+ message: () => {
+ return '';
+ },
+ };
+ } else {
+ return {
+ pass: false,
+ message: () => {
+ return result.message;
+ },
+ };
+ }
+ },
+ });
+};
+
+export const projectRoot = (): string => {
+ return PROJECT_ROOT;
+};
+
+export const attachFrame = async (
+ pageOrFrame: Page | Frame,
+ frameId: string,
+ url: string
+): Promise<Frame | undefined> => {
+ using handle = await pageOrFrame.evaluateHandle(attachFrame, frameId, url);
+ return (await handle.asElement()?.contentFrame()) ?? undefined;
+
+ async function attachFrame(frameId: string, url: string) {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ frame.id = frameId;
+ document.body.appendChild(frame);
+ await new Promise(x => {
+ return (frame.onload = x);
+ });
+ return frame;
+ }
+};
+
+export const isFavicon = (request: {url: () => string | string[]}): boolean => {
+ return request.url().includes('favicon.ico');
+};
+
+export async function detachFrame(
+ pageOrFrame: Page | Frame,
+ frameId: string
+): Promise<void> {
+ await pageOrFrame.evaluate(detachFrame, frameId);
+
+ function detachFrame(frameId: string) {
+ const frame = document.getElementById(frameId) as HTMLIFrameElement;
+ frame.remove();
+ }
+}
+
+export async function navigateFrame(
+ pageOrFrame: Page | Frame,
+ frameId: string,
+ url: string
+): Promise<void> {
+ await pageOrFrame.evaluate(navigateFrame, frameId, url);
+
+ function navigateFrame(frameId: string, url: string) {
+ const frame = document.getElementById(frameId) as HTMLIFrameElement;
+ frame.src = url;
+ return new Promise(x => {
+ return (frame.onload = x);
+ });
+ }
+}
+
+export const dumpFrames = (frame: Frame, indentation?: string): string[] => {
+ indentation = indentation || '';
+ let description = frame.url().replace(/:\d{4,5}\//, ':<PORT>/');
+ if (frame.name()) {
+ description += ' (' + frame.name() + ')';
+ }
+ const result = [indentation + description];
+ for (const child of frame.childFrames()) {
+ result.push(...dumpFrames(child, ' ' + indentation));
+ }
+ return result;
+};
+
+export const waitEvent = async <T = any>(
+ emitter: EventEmitter<any>,
+ eventName: string,
+ predicate: (event: T) => boolean = () => {
+ return true;
+ }
+): Promise<T> => {
+ const deferred = Deferred.create<T>({
+ timeout: 5000,
+ message: `Waiting for ${eventName} event timed out.`,
+ });
+ const handler = (event: T) => {
+ if (!predicate(event)) {
+ return;
+ }
+ deferred.resolve(event);
+ };
+ emitter.on(eventName, handler);
+ try {
+ return await deferred.valueOrThrow();
+ } finally {
+ emitter.off(eventName, handler);
+ }
+};
+
+export interface FilePlaceholder {
+ filename: `${string}.webm`;
+ [Symbol.dispose](): void;
+}
+
+export function getUniqueVideoFilePlaceholder(): FilePlaceholder {
+ return {
+ filename: `${tmpdir()}/test-video-${Math.round(
+ Math.random() * 10000
+ )}.webm`,
+ [Symbol.dispose]() {
+ void rmIfExists(this.filename);
+ },
+ };
+}
+
+export function rmIfExists(file: string): Promise<void> {
+ return rm(file).catch(() => {});
+}
diff --git a/remote/test/puppeteer/test/src/waittask.spec.ts b/remote/test/puppeteer/test/src/waittask.spec.ts
new file mode 100644
index 0000000000..8ff52db16f
--- /dev/null
+++ b/remote/test/puppeteer/test/src/waittask.spec.ts
@@ -0,0 +1,867 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {TimeoutError, ElementHandle} from 'puppeteer';
+import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js';
+
+import {
+ createTimeout,
+ getTestState,
+ setupTestBrowserHooks,
+} from './mocha-utils.js';
+import {attachFrame, detachFrame} from './utils.js';
+
+describe('waittask specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Frame.waitForFunction', function () {
+ it('should accept a string', async () => {
+ const {page} = await getTestState();
+
+ const watchdog = page.waitForFunction('self.__FOO === 1');
+ await page.evaluate(() => {
+ return ((self as unknown as {__FOO: number}).__FOO = 1);
+ });
+ await watchdog;
+ });
+ it('should work when resolved right before execution context disposal', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluateOnNewDocument(() => {
+ return ((globalThis as any).__RELOADED = true);
+ });
+ await page.waitForFunction(() => {
+ if (!(globalThis as any).__RELOADED) {
+ window.location.reload();
+ return false;
+ }
+ return true;
+ });
+ });
+ it('should poll on interval', async () => {
+ const {page} = await getTestState();
+ const startTime = Date.now();
+ const polling = 100;
+ const watchdog = page.waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {polling}
+ );
+ await page.evaluate(() => {
+ setTimeout(() => {
+ (globalThis as any).__FOO = 'hit';
+ }, 50);
+ });
+ await watchdog;
+ expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
+ });
+ it('should poll on mutation', async () => {
+ const {page} = await getTestState();
+
+ let success = false;
+ const watchdog = page
+ .waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'mutation',
+ }
+ )
+ .then(() => {
+ return (success = true);
+ });
+ await page.evaluate(() => {
+ return ((globalThis as any).__FOO = 'hit');
+ });
+ expect(success).toBe(false);
+ await page.evaluate(() => {
+ return document.body.appendChild(document.createElement('div'));
+ });
+ await watchdog;
+ });
+ it('should poll on mutation async', async () => {
+ const {page} = await getTestState();
+
+ let success = false;
+ const watchdog = page
+ .waitForFunction(
+ async () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'mutation',
+ }
+ )
+ .then(() => {
+ return (success = true);
+ });
+ await page.evaluate(async () => {
+ return ((globalThis as any).__FOO = 'hit');
+ });
+ expect(success).toBe(false);
+ await page.evaluate(async () => {
+ return document.body.appendChild(document.createElement('div'));
+ });
+ await watchdog;
+ });
+ it('should poll on raf', async () => {
+ const {page} = await getTestState();
+
+ const watchdog = page.waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'raf',
+ }
+ );
+ await page.evaluate(() => {
+ return ((globalThis as any).__FOO = 'hit');
+ });
+ await watchdog;
+ });
+ it('should poll on raf async', async () => {
+ const {page} = await getTestState();
+
+ const watchdog = page.waitForFunction(
+ async () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'raf',
+ }
+ );
+ await page.evaluate(async () => {
+ return ((globalThis as any).__FOO = 'hit');
+ });
+ await watchdog;
+ });
+ it('should work with strict CSP policy', async () => {
+ const {page, server} = await getTestState();
+
+ server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
+ await page.goto(server.EMPTY_PAGE);
+ let error!: Error;
+ await Promise.all([
+ page
+ .waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'raf',
+ }
+ )
+ .catch(error_ => {
+ return (error = error_);
+ }),
+ page.evaluate(() => {
+ return ((globalThis as any).__FOO = 'hit');
+ }),
+ ]);
+ expect(error).toBeUndefined();
+ });
+ it('should throw negative polling interval', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ await page.waitForFunction(
+ () => {
+ return !!document.body;
+ },
+ {polling: -10}
+ );
+ } catch (error_) {
+ if (isErrorLike(error_)) {
+ error = error_ as Error;
+ }
+ }
+ expect(error?.message).toContain(
+ 'Cannot poll with non-positive interval'
+ );
+ });
+ it('should return the success value as a JSHandle', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await (
+ await page.waitForFunction(() => {
+ return 5;
+ })
+ ).jsonValue()
+ ).toBe(5);
+ });
+ it('should return the window as a success value', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.waitForFunction(() => {
+ return window;
+ })
+ ).toBeTruthy();
+ });
+ it('should accept ElementHandle arguments', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div></div>');
+ using div = (await page.$('div'))!;
+ let resolved = false;
+ const waitForFunction = page
+ .waitForFunction(
+ element => {
+ return element.localName === 'div' && !element.parentElement;
+ },
+ {},
+ div
+ )
+ .then(() => {
+ return (resolved = true);
+ });
+ expect(resolved).toBe(false);
+ await page.evaluate((element: HTMLElement) => {
+ return element.remove();
+ }, div);
+ await waitForFunction;
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .waitForFunction(
+ () => {
+ return false;
+ },
+ {timeout: 10}
+ )
+ .catch(error_ => {
+ return (error = error_);
+ });
+
+ expect(error).toBeInstanceOf(TimeoutError);
+ expect(error?.message).toContain('Waiting failed: 10ms exceeded');
+ });
+ it('should respect default timeout', async () => {
+ const {page} = await getTestState();
+
+ page.setDefaultTimeout(1);
+ let error!: Error;
+ await page
+ .waitForFunction(() => {
+ return false;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ expect(error?.message).toContain('Waiting failed: 1ms exceeded');
+ });
+ it('should disable timeout when its set to 0', async () => {
+ const {page} = await getTestState();
+
+ const watchdog = page.waitForFunction(
+ () => {
+ (globalThis as any).__counter =
+ ((globalThis as any).__counter || 0) + 1;
+ return (globalThis as any).__injected;
+ },
+ {timeout: 0, polling: 10}
+ );
+ await page.waitForFunction(() => {
+ return (globalThis as any).__counter > 10;
+ });
+ await page.evaluate(() => {
+ return ((globalThis as any).__injected = true);
+ });
+ await watchdog;
+ });
+ it('should survive cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let fooFound = false;
+ const waitForFunction = page
+ .waitForFunction(() => {
+ return (globalThis as unknown as {__FOO: number}).__FOO === 1;
+ })
+ .then(() => {
+ return (fooFound = true);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(fooFound).toBe(false);
+ await page.reload();
+ expect(fooFound).toBe(false);
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
+ expect(fooFound).toBe(false);
+ await page.evaluate(() => {
+ return ((globalThis as any).__FOO = 1);
+ });
+ await waitForFunction;
+ expect(fooFound).toBe(true);
+ });
+ it('should survive navigations', async () => {
+ const {page, server} = await getTestState();
+
+ const watchdog = page.waitForFunction(() => {
+ return (globalThis as any).__done;
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await page.goto(server.PREFIX + '/consolelog.html');
+ await page.evaluate(() => {
+ return ((globalThis as any).__done = true);
+ });
+ await watchdog;
+ });
+ it('should be cancellable', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const abortController = new AbortController();
+ const task = page.waitForFunction(
+ () => {
+ return (globalThis as any).__done;
+ },
+ {
+ signal: abortController.signal,
+ }
+ );
+ abortController.abort();
+ await expect(task).rejects.toThrow(/aborted/);
+ });
+ });
+
+ describe('Page.waitForTimeout', () => {
+ it('waits for the given timeout before resolving', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const startTime = Date.now();
+ await page.waitForTimeout(1000);
+ const endTime = Date.now();
+ /* In a perfect world endTime - startTime would be exactly 1000 but we
+ * expect some fluctuations and for it to be off by a little bit. So to
+ * avoid a flaky test we'll make sure it waited for roughly 1 second.
+ */
+ expect(endTime - startTime).toBeGreaterThan(700);
+ expect(endTime - startTime).toBeLessThan(1300);
+ });
+ });
+
+ describe('Frame.waitForTimeout', () => {
+ it('waits for the given timeout before resolving', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const frame = page.mainFrame();
+ const startTime = Date.now();
+ await frame.waitForTimeout(1000);
+ const endTime = Date.now();
+ /* In a perfect world endTime - startTime would be exactly 1000 but we
+ * expect some fluctuations and for it to be off by a little bit. So to
+ * avoid a flaky test we'll make sure it waited for roughly 1 second
+ */
+ expect(endTime - startTime).toBeGreaterThan(700);
+ expect(endTime - startTime).toBeLessThan(1300);
+ });
+ });
+
+ describe('Frame.waitForSelector', function () {
+ const addElement = (tag: string) => {
+ return document.body.appendChild(document.createElement(tag));
+ };
+
+ it('should immediately resolve promise if node exists', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const frame = page.mainFrame();
+ await frame.waitForSelector('*');
+ await frame.evaluate(addElement, 'div');
+ await frame.waitForSelector('div');
+ });
+
+ it('should be cancellable', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const abortController = new AbortController();
+ const task = page.waitForSelector('wrong', {
+ signal: abortController.signal,
+ });
+ abortController.abort();
+ await expect(task).rejects.toThrow(/aborted/);
+ });
+
+ it('should work with removed MutationObserver', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ // @ts-expect-error We want to remove it for the test.
+ return delete window.MutationObserver;
+ });
+ const [handle] = await Promise.all([
+ page.waitForSelector('.zombo'),
+ page.setContent(`<div class='zombo'>anything</div>`),
+ ]);
+ expect(
+ await page.evaluate(x => {
+ return x?.textContent;
+ }, handle)
+ ).toBe('anything');
+ });
+
+ it('should resolve promise when node is added', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const frame = page.mainFrame();
+ const watchdog = frame.waitForSelector('div');
+ await frame.evaluate(addElement, 'br');
+ await frame.evaluate(addElement, 'div');
+ using eHandle = (await watchdog)!;
+ const tagName = await (await eHandle.getProperty('tagName')).jsonValue();
+ expect(tagName).toBe('DIV');
+ });
+
+ it('should work when node is added through innerHTML', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const watchdog = page.waitForSelector('h3 div');
+ await page.evaluate(addElement, 'span');
+ await page.evaluate(() => {
+ return (document.querySelector('span')!.innerHTML =
+ '<h3><div></div></h3>');
+ });
+ await watchdog;
+ });
+
+ it('Page.waitForSelector is shortcut for main frame', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const otherFrame = page.frames()[1]!;
+ const watchdog = page.waitForSelector('div');
+ await otherFrame.evaluate(addElement, 'div');
+ await page.evaluate(addElement, 'div');
+ using eHandle = await watchdog;
+ expect(eHandle?.frame).toBe(page.mainFrame());
+ });
+
+ it('should run in specified frame', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(page, 'frame2', server.EMPTY_PAGE);
+ const frame1 = page.frames()[1]!;
+ const frame2 = page.frames()[2]!;
+ const waitForSelectorPromise = frame2.waitForSelector('div');
+ await frame1.evaluate(addElement, 'div');
+ await frame2.evaluate(addElement, 'div');
+ using eHandle = await waitForSelectorPromise;
+ expect(eHandle?.frame).toBe(frame2);
+ });
+
+ it('should throw when frame is detached', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1]!;
+ let waitError: Error | undefined;
+ const waitPromise = frame.waitForSelector('.box').catch(error => {
+ return (waitError = error);
+ });
+ await detachFrame(page, 'frame1');
+ await waitPromise;
+ expect(waitError).toBeTruthy();
+ expect(waitError?.message).toContain(
+ 'waitForFunction failed: frame got detached.'
+ );
+ });
+ it('should survive cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let boxFound = false;
+ const waitForSelector = page.waitForSelector('.box').then(() => {
+ return (boxFound = true);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(boxFound).toBe(false);
+ await page.reload();
+ expect(boxFound).toBe(false);
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
+ await waitForSelector;
+ expect(boxFound).toBe(true);
+ });
+ it('should wait for element to be visible (display)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {visible: true});
+ await page.setContent('<div style="display: none">text</div>');
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.removeProperty('display');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be visible (visibility)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {visible: true});
+ await page.setContent('<div style="visibility: hidden">text</div>');
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.setProperty('visibility', 'collapse');
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.removeProperty('visibility');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be visible (bounding box)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {visible: true});
+ await page.setContent('<div style="width: 0">text</div>');
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.setProperty('height', '0');
+ e.style.removeProperty('width');
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.removeProperty('height');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be visible recursively', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div#inner', {
+ visible: true,
+ });
+ await page.setContent(
+ `<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
+ );
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ return e.style.removeProperty('display');
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ return e.style.removeProperty('visibility');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be hidden (visibility)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {hidden: true});
+ await page.setContent(`<div style='display: block;'>text</div>`);
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ return e.style.setProperty('visibility', 'hidden');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be hidden (display)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {hidden: true});
+ await page.setContent(`<div style='display: block;'>text</div>`);
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ return e.style.setProperty('display', 'none');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be hidden (bounding box)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {hidden: true});
+ await page.setContent('<div>text</div>');
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.setProperty('height', '0');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be hidden (removal)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {hidden: true});
+ await page.setContent(`<div>text</div>`);
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40, true)])
+ ).resolves.toBeTruthy();
+ await element.evaluate(e => {
+ e.remove();
+ });
+ await expect(promise).resolves.toBeFalsy();
+ });
+ it('should return null if waiting to hide non-existing element', async () => {
+ const {page} = await getTestState();
+
+ using handle = await page.waitForSelector('non-existing', {
+ hidden: true,
+ });
+ expect(handle).toBe(null);
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForSelector('div', {timeout: 10}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ expect(error?.message).toContain(
+ 'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
+ );
+ });
+ it('should have an error message specifically for awaiting an element to be hidden', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div>text</div>`);
+ let error!: Error;
+ await page
+ .waitForSelector('div', {hidden: true, timeout: 10})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ expect(error?.message).toContain(
+ 'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
+ );
+ });
+
+ it('should respond to node attribute mutation', async () => {
+ const {page} = await getTestState();
+
+ let divFound = false;
+ const waitForSelector = page.waitForSelector('.zombo').then(() => {
+ return (divFound = true);
+ });
+ await page.setContent(`<div class='notZombo'></div>`);
+ expect(divFound).toBe(false);
+ await page.evaluate(() => {
+ return (document.querySelector('div')!.className = 'zombo');
+ });
+ expect(await waitForSelector).toBe(true);
+ });
+ it('should return the element handle', async () => {
+ const {page} = await getTestState();
+
+ const waitForSelector = page.waitForSelector('.zombo');
+ await page.setContent(`<div class='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForSelector
+ )
+ ).toBe('anything');
+ });
+ it('should have correct stack trace for timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error?.stack).toContain(
+ 'Waiting for selector `.zombo` failed: Waiting failed: 10ms exceeded'
+ );
+ // The extension is ts here as Mocha maps back via sourcemaps.
+ expect(error?.stack).toContain('WaitTask.ts');
+ });
+ });
+
+ describe('Frame.waitForXPath', function () {
+ const addElement = (tag: string) => {
+ return document.body.appendChild(document.createElement(tag));
+ };
+
+ it('should support some fancy xpath', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<p>red herring</p><p>hello world </p>`);
+ const waitForXPath = page.waitForXPath(
+ '//p[normalize-space(.)="hello world"]'
+ );
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForXPath
+ )
+ ).toBe('hello world ');
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForXPath('//div', {timeout: 10}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ expect(error?.message).toContain('Waiting failed: 10ms exceeded');
+ });
+ it('should run in specified frame', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(page, 'frame2', server.EMPTY_PAGE);
+ const frame1 = page.frames()[1]!;
+ const frame2 = page.frames()[2]!;
+ const waitForXPathPromise = frame2.waitForXPath('//div');
+ await frame1.evaluate(addElement, 'div');
+ await frame2.evaluate(addElement, 'div');
+ using eHandle = await waitForXPathPromise;
+ expect(eHandle?.frame).toBe(frame2);
+ });
+ it('should throw when frame is detached', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1]!;
+ let waitError: Error | undefined;
+ const waitPromise = frame
+ .waitForXPath('//*[@class="box"]')
+ .catch(error => {
+ return (waitError = error);
+ });
+ await detachFrame(page, 'frame1');
+ await waitPromise;
+ expect(waitError).toBeTruthy();
+ expect(waitError?.message).toContain(
+ 'waitForFunction failed: frame got detached.'
+ );
+ });
+ it('hidden should wait for display: none', async () => {
+ const {page} = await getTestState();
+
+ let divHidden = false;
+ await page.setContent(`<div style='display: block;'>text</div>`);
+ const waitForXPath = page
+ .waitForXPath('//div', {hidden: true})
+ .then(() => {
+ return (divHidden = true);
+ });
+ await page.waitForXPath('//div'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')
+ ?.style.setProperty('display', 'none');
+ });
+ expect(await waitForXPath).toBe(true);
+ expect(divHidden).toBe(true);
+ });
+ it('hidden should return null if the element is not found', async () => {
+ const {page} = await getTestState();
+
+ using waitForXPath = await page.waitForXPath('//div', {hidden: true});
+
+ expect(waitForXPath).toBe(null);
+ });
+ it('hidden should return an empty element handle if the element is found', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div style='display: none;'>text</div>`);
+
+ using waitForXPath = await page.waitForXPath('//div', {hidden: true});
+
+ expect(waitForXPath).toBeInstanceOf(ElementHandle);
+ });
+ it('should return the element handle', async () => {
+ const {page} = await getTestState();
+
+ const waitForXPath = page.waitForXPath('//*[@class="zombo"]');
+ await page.setContent(`<div class='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForXPath
+ )
+ ).toBe('anything');
+ });
+ it('should allow you to select a text node', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div>some text</div>`);
+ using text = await page.waitForXPath('//div/text()');
+ expect(await (await text!.getProperty('nodeType')!).jsonValue()).toBe(
+ 3 /* Node.TEXT_NODE */
+ );
+ });
+ it('should allow you to select an element with single slash', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div>some text</div>`);
+ const waitForXPath = page.waitForXPath('/html/body/div');
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForXPath
+ )
+ ).toBe('some text');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/worker.spec.ts b/remote/test/puppeteer/test/src/worker.spec.ts
new file mode 100644
index 0000000000..254ff4a514
--- /dev/null
+++ b/remote/test/puppeteer/test/src/worker.spec.ts
@@ -0,0 +1,109 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {WebWorker} from 'puppeteer-core/internal/api/WebWorker.js';
+import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+describe('Workers', function () {
+ setupTestBrowserHooks();
+
+ it('Page.workers', async () => {
+ const {page, server} = await getTestState();
+
+ await Promise.all([
+ waitEvent(page, 'workercreated'),
+ page.goto(server.PREFIX + '/worker/worker.html'),
+ ]);
+ const worker = page.workers()[0]!;
+ expect(worker?.url()).toContain('worker.js');
+
+ expect(
+ await worker?.evaluate(() => {
+ return (globalThis as any).workerFunction();
+ })
+ ).toBe('worker function result');
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.workers()).toHaveLength(0);
+ });
+ it('should emit created and destroyed events', async () => {
+ const {page} = await getTestState();
+
+ const workerCreatedPromise = waitEvent<WebWorker>(page, 'workercreated');
+ using workerObj = await page.evaluateHandle(() => {
+ return new Worker('data:text/javascript,1');
+ });
+ const worker = await workerCreatedPromise;
+ using workerThisObj = await worker.evaluateHandle(() => {
+ return this;
+ });
+ const workerDestroyedPromise = waitEvent(page, 'workerdestroyed');
+ await page.evaluate((workerObj: Worker) => {
+ return workerObj.terminate();
+ }, workerObj);
+ expect(await workerDestroyedPromise).toBe(worker);
+ const error = await workerThisObj.getProperty('self').catch(error => {
+ return error;
+ });
+ expect(error.message).toContain('Most likely the worker has been closed.');
+ });
+ it('should report console logs', async () => {
+ const {page} = await getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.evaluate(() => {
+ return new Worker(`data:text/javascript,console.log(1)`);
+ }),
+ ]);
+ expect(message.text()).toBe('1');
+ expect(message.location()).toEqual({
+ url: '',
+ lineNumber: 0,
+ columnNumber: 8,
+ });
+ });
+ it('should have JSHandles for console logs', async () => {
+ const {page} = await getTestState();
+
+ const logPromise = waitEvent<ConsoleMessage>(page, 'console');
+ await page.evaluate(() => {
+ return new Worker(`data:text/javascript,console.log(1,2,3,this)`);
+ });
+ const log = await logPromise;
+ expect(log.text()).toBe('1 2 3 JSHandle@object');
+ expect(log.args()).toHaveLength(4);
+ expect(await (await log.args()[3]!.getProperty('origin')).jsonValue()).toBe(
+ 'null'
+ );
+ });
+ it('should have an execution context', async () => {
+ const {page} = await getTestState();
+
+ const workerCreatedPromise = waitEvent<WebWorker>(page, 'workercreated');
+ await page.evaluate(() => {
+ return new Worker(`data:text/javascript,console.log(1)`);
+ });
+ const worker = await workerCreatedPromise;
+ expect(await worker.evaluate('1+1')).toBe(2);
+ });
+ it('should report errors', async () => {
+ const {page} = await getTestState();
+
+ const errorPromise = waitEvent<Error>(page, 'pageerror');
+ await page.evaluate(() => {
+ return new Worker(
+ `data:text/javascript, throw new Error('this is my error');`
+ );
+ });
+ const errorLog = await errorPromise;
+ expect(errorLog.message).toContain('this is my error');
+ });
+});
diff --git a/remote/test/puppeteer/test/tsconfig.json b/remote/test/puppeteer/test/tsconfig.json
new file mode 100644
index 0000000000..554d034ff1
--- /dev/null
+++ b/remote/test/puppeteer/test/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "build",
+ "rootDir": "src",
+ },
+ "include": ["src"],
+}
diff --git a/remote/test/puppeteer/test/tsdoc.json b/remote/test/puppeteer/test/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/test/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/tools/analyze_issue.mjs b/remote/test/puppeteer/tools/analyze_issue.mjs
new file mode 100755
index 0000000000..9592112de0
--- /dev/null
+++ b/remote/test/puppeteer/tools/analyze_issue.mjs
@@ -0,0 +1,281 @@
+#!/usr/bin/env node
+// @ts-check
+
+'use strict';
+
+import {writeFile, mkdir, copyFile} from 'fs/promises';
+import {dirname, join} from 'path';
+import {fileURLToPath} from 'url';
+
+import core from '@actions/core';
+import semver from 'semver';
+
+import packageJson from '../packages/puppeteer-core/package.json' assert {type: 'json'};
+
+const codifyAndJoinValues = values => {
+ return values
+ .map(value => {
+ return `\`${value}\``;
+ })
+ .join(' ,');
+};
+const formatMessage = value => {
+ return value.trim();
+};
+const removeVersionPrefix = value => {
+ return value.startsWith('v') ? value.slice(1) : value;
+};
+
+const LAST_PUPPETEER_VERSION = packageJson.version;
+if (!LAST_PUPPETEER_VERSION) {
+ core.setFailed('No maintained version found.');
+}
+const LAST_SUPPORTED_NODE_VERSION = removeVersionPrefix(
+ packageJson.engines.node.slice(2).trim()
+);
+
+const SUPPORTED_OSES = ['windows', 'macos', 'linux'];
+const SUPPORTED_PACKAGE_MANAGERS = ['yarn', 'npm', 'pnpm'];
+
+const ERROR_MESSAGES = {
+ unsupportedOs(value) {
+ return formatMessage(`
+This issue has an unsupported OS: \`${value}\`. Only the following operating systems are supported: ${codifyAndJoinValues(
+ SUPPORTED_OSES
+ )}. Please verify the issue on a supported OS and update the form.
+`);
+ },
+ unsupportedPackageManager(value) {
+ return formatMessage(`
+This issue has an unsupported package manager: \`${value}\`. Only the following package managers are supported: ${codifyAndJoinValues(
+ SUPPORTED_PACKAGE_MANAGERS
+ )}. Please verify the issue using a supported package manager and update the form.
+`);
+ },
+ invalidPackageManagerVersion(value) {
+ return formatMessage(`
+This issue has an invalid package manager version: \`${value}\`. Versions must follow [SemVer](https://semver.org/) formatting. Please update the form with a valid version.
+`);
+ },
+ unsupportedNodeVersion(value) {
+ return formatMessage(`
+This issue has an unsupported Node.js version: \`${value}\`. Only versions above \`v${LAST_SUPPORTED_NODE_VERSION}\` are supported. Please verify the issue on a supported version of Node.js and update the form.
+`);
+ },
+ invalidNodeVersion(value) {
+ return formatMessage(`
+This issue has an invalid Node.js version: \`${value}\`. Versions must follow [SemVer](https://semver.org/) formatting. Please update the form with a valid version.
+`);
+ },
+ unsupportedPuppeteerVersion(value) {
+ return formatMessage(`
+This issue has an outdated Puppeteer version: \`${value}\`. Please verify your issue on the latest \`${LAST_PUPPETEER_VERSION}\` version. Then update the form accordingly.
+`);
+ },
+ invalidPuppeteerVersion(value) {
+ return formatMessage(`
+This issue has an invalid Puppeteer version: \`${value}\`. Versions must follow [SemVer](https://semver.org/) formatting. Please update the form with a valid version.
+`);
+ },
+};
+
+(async () => {
+ let input = '';
+ for await (const chunk of process.stdin.iterator({
+ destroyOnReturn: false,
+ })) {
+ input += chunk;
+ }
+ input = JSON.parse(input).trim();
+
+ let mvce = '';
+ let error = '';
+ let configuration = '';
+ let puppeteerVersion = '';
+ let nodeVersion = '';
+ let packageManagerVersion = '';
+ let packageManager = '';
+ let os = '';
+ const behavior = {};
+ const lines = input.split('\n');
+ {
+ /** @type {(value: string) => void} */
+ let set = () => {
+ return void 0;
+ };
+ let j = 1;
+ let i = 1;
+ for (; i < lines.length; ++i) {
+ if (lines[i].startsWith('### Bug behavior')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ if (value.match(/\[x\] Flaky/i)) {
+ behavior.flaky = true;
+ }
+ if (value.match(/\[x\] pdf/i)) {
+ behavior.noError = true;
+ }
+ };
+ } else if (lines[i].startsWith('### Minimal, reproducible example')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ mvce = value;
+ };
+ } else if (lines[i].startsWith('### Error string')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ if (value.match(/no error/i)) {
+ behavior.noError = true;
+ } else {
+ error = value;
+ }
+ };
+ } else if (lines[i].startsWith('### Puppeteer configuration')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ configuration = value;
+ };
+ } else if (lines[i].startsWith('### Puppeteer version')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ puppeteerVersion = removeVersionPrefix(value);
+ };
+ } else if (lines[i].startsWith('### Node version')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ nodeVersion = removeVersionPrefix(value);
+ };
+ } else if (lines[i].startsWith('### Package manager version')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ packageManagerVersion = removeVersionPrefix(value);
+ };
+ } else if (lines[i].startsWith('### Package manager')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ packageManager = value.toLowerCase();
+ };
+ } else if (lines[i].startsWith('### Operating system')) {
+ set(lines.slice(j, i).join('\n').trim());
+ j = i + 1;
+ set = value => {
+ os = value.toLowerCase();
+ };
+ }
+ }
+ set(lines.slice(j, i).join('\n').trim());
+ }
+
+ let runsOn;
+ switch (os) {
+ case 'windows':
+ runsOn = 'windows-latest';
+ break;
+ case 'macos':
+ runsOn = 'macos-latest';
+ break;
+ case 'linux':
+ runsOn = 'ubuntu-latest';
+ break;
+ default:
+ core.setOutput('errorMessage', ERROR_MESSAGES.unsupportedOs(os));
+ core.setFailed(`Unsupported OS: ${os}`);
+ }
+
+ if (!SUPPORTED_PACKAGE_MANAGERS.includes(packageManager)) {
+ core.setOutput(
+ 'errorMessage',
+ ERROR_MESSAGES.unsupportedPackageManager(packageManager)
+ );
+ core.setFailed(`Unsupported package manager: ${packageManager}`);
+ }
+
+ if (!semver.valid(nodeVersion)) {
+ core.setOutput(
+ 'errorMessage',
+ ERROR_MESSAGES.invalidNodeVersion(nodeVersion)
+ );
+ core.setFailed('Invalid Node version');
+ }
+ if (semver.lt(nodeVersion, LAST_SUPPORTED_NODE_VERSION)) {
+ core.setOutput(
+ 'errorMessage',
+ ERROR_MESSAGES.unsupportedNodeVersion(nodeVersion)
+ );
+ core.setFailed(`Unsupported node version: ${nodeVersion}`);
+ }
+
+ if (!semver.valid(puppeteerVersion)) {
+ core.setOutput(
+ 'errorMessage',
+ ERROR_MESSAGES.invalidPuppeteerVersion(puppeteerVersion)
+ );
+ core.setFailed(`Invalid puppeteer version: ${puppeteerVersion}`);
+ }
+ if (
+ !LAST_PUPPETEER_VERSION ||
+ semver.lt(puppeteerVersion, LAST_PUPPETEER_VERSION)
+ ) {
+ core.setOutput(
+ 'errorMessage',
+ ERROR_MESSAGES.unsupportedPuppeteerVersion(puppeteerVersion)
+ );
+ core.setFailed(`Unsupported puppeteer version: ${puppeteerVersion}`);
+ }
+
+ if (!semver.valid(packageManagerVersion)) {
+ core.setOutput(
+ 'errorMessage',
+ ERROR_MESSAGES.invalidPackageManagerVersion(packageManagerVersion)
+ );
+ core.setFailed(`Invalid package manager version: ${packageManagerVersion}`);
+ }
+
+ core.setOutput('errorMessage', '');
+ core.setOutput('runsOn', runsOn);
+ core.setOutput('nodeVersion', nodeVersion);
+ core.setOutput('packageManager', packageManager);
+
+ await mkdir('out');
+ Promise.all([
+ writeFile(join('out', 'main.ts'), mvce.split('\n').slice(1, -1).join('\n')),
+ writeFile(join('out', 'puppeteer-error.txt'), error),
+ writeFile(
+ join('out', 'puppeteer.config.js'),
+ configuration.split('\n').slice(1, -1).join('\n')
+ ),
+ writeFile(join('out', 'puppeteer-behavior.json'), JSON.stringify(behavior)),
+ writeFile(
+ join('out', 'package.json'),
+ JSON.stringify({
+ packageManager: `${packageManager}@${packageManagerVersion}`,
+ scripts: {
+ start: 'tsx main.ts',
+ verify: 'tsx verify_issue.ts',
+ },
+ dependencies: {
+ puppeteer: puppeteerVersion,
+ },
+ devDependencies: {
+ tsx: 'latest',
+ },
+ })
+ ),
+ copyFile(
+ join(
+ dirname(fileURLToPath(import.meta.url)),
+ 'assets',
+ 'verify_issue.ts'
+ ),
+ join('out', 'verify_issue.ts')
+ ),
+ ]);
+})();
diff --git a/remote/test/puppeteer/tools/assets/verify_issue.ts b/remote/test/puppeteer/tools/assets/verify_issue.ts
new file mode 100755
index 0000000000..5814eff66c
--- /dev/null
+++ b/remote/test/puppeteer/tools/assets/verify_issue.ts
@@ -0,0 +1,68 @@
+import {spawnSync} from 'child_process';
+import {readFile, writeFile} from 'fs/promises';
+
+(async () => {
+ const error = await readFile('puppeteer-error.txt', 'utf-8');
+ const behavior = JSON.parse(
+ await readFile('puppeteer-behavior.json', 'utf-8')
+ ) as {flaky?: boolean; noError?: boolean};
+
+ let maxRepetitions = 1;
+ if (behavior.flaky) {
+ maxRepetitions = 100;
+ }
+
+ let status: number | null = null;
+ let stderr = '';
+ let stdout = '';
+
+ const preHook = async () => {
+ console.log('Writing output and error logs...');
+ await Promise.all([
+ writeFile('output.log', stdout),
+ writeFile('error.log', stderr),
+ ]);
+ };
+
+ let checkStatusWithError: () => Promise<void>;
+ if (behavior.noError) {
+ checkStatusWithError = async () => {
+ if (status === 0) {
+ await preHook();
+ console.log('Script ran successfully; no error found.');
+ process.exit(0);
+ }
+ };
+ } else {
+ checkStatusWithError = async () => {
+ if (status !== 0) {
+ await preHook();
+ if (stderr.toLowerCase().includes(error.toLowerCase())) {
+ console.log('Script failed; error found.');
+ process.exit(0);
+ }
+ console.error('Script failed; unknown error found.');
+ process.exit(1);
+ }
+ };
+ }
+
+ for (let i = 0; i < maxRepetitions; ++i) {
+ const result = spawnSync('npm', ['start'], {
+ shell: true,
+ encoding: 'utf-8',
+ });
+ status = result.status;
+ stdout = result.stdout ?? '';
+ stderr = result.stderr ?? '';
+ await checkStatusWithError();
+ }
+
+ await preHook();
+ if (behavior.noError) {
+ console.error('Script failed; unknown error found.');
+ } else {
+ console.error('Script ran successfully; no error found.');
+ }
+ process.exit(1);
+})();
diff --git a/remote/test/puppeteer/tools/chmod.ts b/remote/test/puppeteer/tools/chmod.ts
new file mode 100644
index 0000000000..da15b64fae
--- /dev/null
+++ b/remote/test/puppeteer/tools/chmod.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+
+/**
+ * Calls chmod with the mode in argv[2] on paths in argv[3...length-1].
+ */
+const mode = process.argv[2];
+
+for (let i = 3; i < process.argv.length; i++) {
+ fs.chmodSync(process.argv[i], mode);
+}
diff --git a/remote/test/puppeteer/tools/clean.js b/remote/test/puppeteer/tools/clean.js
new file mode 100755
index 0000000000..049fdc0434
--- /dev/null
+++ b/remote/test/puppeteer/tools/clean.js
@@ -0,0 +1,12 @@
+#!/usr/bin/env node
+
+const {exec} = require('child_process');
+const {readdirSync} = require('fs');
+
+exec(
+ `git clean -Xf ${readdirSync(process.cwd())
+ .filter(file => {
+ return file !== 'node_modules';
+ })
+ .join(' ')}`
+);
diff --git a/remote/test/puppeteer/tools/cp.ts b/remote/test/puppeteer/tools/cp.ts
new file mode 100644
index 0000000000..2915389e19
--- /dev/null
+++ b/remote/test/puppeteer/tools/cp.ts
@@ -0,0 +1,12 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+
+/**
+ * Copies single file in argv[2] to argv[3]
+ */
+fs.cpSync(process.argv[2], process.argv[3]);
diff --git a/remote/test/puppeteer/tools/docgen/package.json b/remote/test/puppeteer/tools/docgen/package.json
new file mode 100644
index 0000000000..f1ca4ea127
--- /dev/null
+++ b/remote/test/puppeteer/tools/docgen/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@puppeteer/docgen",
+ "version": "0.1.0",
+ "type": "module",
+ "private": true,
+ "main": "./lib/docgen.js",
+ "description": "Documentation generator for Puppeteer",
+ "license": "Apache-2.0",
+ "scripts": {
+ "build": "wireit",
+ "clean": "../clean.js"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b",
+ "clean": "if-file-deleted",
+ "files": [
+ "src/**"
+ ],
+ "output": [
+ "lib/**",
+ "tsconfig.tsbuildinfo"
+ ]
+ }
+ },
+ "devDependencies": {
+ "@microsoft/api-extractor": "7.39.4",
+ "@microsoft/api-documenter": "7.23.20",
+ "@microsoft/api-extractor-model": "7.28.7",
+ "@microsoft/tsdoc": "0.14.2",
+ "@rushstack/node-core-library": "3.64.2"
+ }
+}
diff --git a/remote/test/puppeteer/tools/docgen/src/custom_markdown_documenter.ts b/remote/test/puppeteer/tools/docgen/src/custom_markdown_documenter.ts
new file mode 100644
index 0000000000..d63a8b96ef
--- /dev/null
+++ b/remote/test/puppeteer/tools/docgen/src/custom_markdown_documenter.ts
@@ -0,0 +1,1495 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the
+// MIT license. See LICENSE in the project root for license information.
+
+// Taken from
+// https://github.com/microsoft/rushstack/blob/main/apps/api-documenter/src/documenters/MarkdownDocumenter.ts
+// This file has been edited to morph into Docusaurus's expected inputs.
+
+import * as path from 'path';
+
+import type {DocumenterConfig} from '@microsoft/api-documenter/lib/documenters/DocumenterConfig.js';
+import {CustomMarkdownEmitter as ApiFormatterMarkdownEmitter} from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter.js';
+import {CustomDocNodes} from '@microsoft/api-documenter/lib/nodes/CustomDocNodeKind.js';
+import {DocEmphasisSpan} from '@microsoft/api-documenter/lib/nodes/DocEmphasisSpan.js';
+import {DocHeading} from '@microsoft/api-documenter/lib/nodes/DocHeading.js';
+import {DocNoteBox} from '@microsoft/api-documenter/lib/nodes/DocNoteBox.js';
+import {DocTable} from '@microsoft/api-documenter/lib/nodes/DocTable.js';
+import {DocTableCell} from '@microsoft/api-documenter/lib/nodes/DocTableCell.js';
+import {DocTableRow} from '@microsoft/api-documenter/lib/nodes/DocTableRow.js';
+import {MarkdownDocumenterAccessor} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterAccessor.js';
+import {
+ type IMarkdownDocumenterFeatureOnBeforeWritePageArgs,
+ MarkdownDocumenterFeatureContext,
+} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterFeature.js';
+import {PluginLoader} from '@microsoft/api-documenter/lib/plugin/PluginLoader.js';
+import {Utilities} from '@microsoft/api-documenter/lib/utils/Utilities.js';
+import {
+ ApiClass,
+ ApiDeclaredItem,
+ ApiDocumentedItem,
+ type ApiEnum,
+ ApiInitializerMixin,
+ ApiInterface,
+ type ApiItem,
+ ApiItemKind,
+ type ApiModel,
+ type ApiNamespace,
+ ApiOptionalMixin,
+ type ApiPackage,
+ ApiParameterListMixin,
+ ApiPropertyItem,
+ ApiProtectedMixin,
+ ApiReadonlyMixin,
+ ApiReleaseTagMixin,
+ ApiReturnTypeMixin,
+ ApiStaticMixin,
+ ApiTypeAlias,
+ type Excerpt,
+ type ExcerptToken,
+ ExcerptTokenKind,
+ type IResolveDeclarationReferenceResult,
+ ReleaseTag,
+} from '@microsoft/api-extractor-model';
+import {
+ type DocBlock,
+ DocCodeSpan,
+ type DocComment,
+ DocFencedCode,
+ DocLinkTag,
+ type DocNodeContainer,
+ DocNodeKind,
+ DocParagraph,
+ DocPlainText,
+ DocSection,
+ StandardTags,
+ StringBuilder,
+ type TSDocConfiguration,
+} from '@microsoft/tsdoc';
+import {
+ FileSystem,
+ NewlineKind,
+ PackageName,
+} from '@rushstack/node-core-library';
+
+export interface IMarkdownDocumenterOptions {
+ apiModel: ApiModel;
+ documenterConfig: DocumenterConfig | undefined;
+ outputFolder: string;
+}
+
+export class CustomMarkdownEmitter extends ApiFormatterMarkdownEmitter {
+ protected override getEscapedText(text: string): string {
+ const textWithBackslashes: string = text
+ .replace(/\\/g, '\\\\') // first replace the escape character
+ .replace(/[*#[\]_|`~]/g, x => {
+ return '\\' + x;
+ }) // then escape any special characters
+ .replace(/---/g, '\\-\\-\\-') // hyphens only if it's 3 or more
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/\{/g, '&#123;')
+ .replace(/\}/g, '&#125;');
+ return textWithBackslashes;
+ }
+
+ protected override getTableEscapedText(text: string): string {
+ return text
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/\|/g, '&#124;');
+ }
+}
+
+/**
+ * Renders API documentation in the Markdown file format.
+ * For more info: https://en.wikipedia.org/wiki/Markdown
+ */
+export class MarkdownDocumenter {
+ private readonly _apiModel: ApiModel;
+ private readonly _documenterConfig: DocumenterConfig | undefined;
+ private readonly _tsdocConfiguration: TSDocConfiguration;
+ private readonly _markdownEmitter: CustomMarkdownEmitter;
+ private readonly _outputFolder: string;
+ private readonly _pluginLoader: PluginLoader;
+
+ public constructor(options: IMarkdownDocumenterOptions) {
+ this._apiModel = options.apiModel;
+ this._documenterConfig = options.documenterConfig;
+ this._outputFolder = options.outputFolder;
+ this._tsdocConfiguration = CustomDocNodes.configuration;
+ this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel);
+
+ this._pluginLoader = new PluginLoader();
+ }
+
+ public generateFiles(): void {
+ if (this._documenterConfig) {
+ this._pluginLoader.load(this._documenterConfig, () => {
+ return new MarkdownDocumenterFeatureContext({
+ apiModel: this._apiModel,
+ outputFolder: this._outputFolder,
+ documenter: new MarkdownDocumenterAccessor({
+ getLinkForApiItem: (apiItem: ApiItem) => {
+ return this._getLinkFilenameForApiItem(apiItem);
+ },
+ }),
+ });
+ });
+ }
+
+ this._deleteOldOutputFiles();
+
+ this._writeApiItemPage(this._apiModel.members[0]!);
+
+ if (this._pluginLoader.markdownDocumenterFeature) {
+ this._pluginLoader.markdownDocumenterFeature.onFinished({});
+ }
+ }
+
+ private _writeApiItemPage(apiItem: ApiItem): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+ const output: DocSection = new DocSection({
+ configuration: this._tsdocConfiguration,
+ });
+
+ const scopedName: string = apiItem.getScopedNameWithinPackage();
+
+ switch (apiItem.kind) {
+ case ApiItemKind.Class:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} class`})
+ );
+ break;
+ case ApiItemKind.Enum:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} enum`})
+ );
+ break;
+ case ApiItemKind.Interface:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} interface`})
+ );
+ break;
+ case ApiItemKind.Constructor:
+ case ApiItemKind.ConstructSignature:
+ output.appendNode(new DocHeading({configuration, title: scopedName}));
+ break;
+ case ApiItemKind.Method:
+ case ApiItemKind.MethodSignature:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} method`})
+ );
+ break;
+ case ApiItemKind.Function:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} function`})
+ );
+ break;
+ case ApiItemKind.Model:
+ output.appendNode(
+ new DocHeading({configuration, title: `API Reference`})
+ );
+ break;
+ case ApiItemKind.Namespace:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} namespace`})
+ );
+ break;
+ case ApiItemKind.Package:
+ console.log(`Writing ${apiItem.displayName} package`);
+ output.appendNode(
+ new DocHeading({
+ configuration,
+ title: `API Reference`,
+ })
+ );
+ break;
+ case ApiItemKind.Property:
+ case ApiItemKind.PropertySignature:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} property`})
+ );
+ break;
+ case ApiItemKind.TypeAlias:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} type`})
+ );
+ break;
+ case ApiItemKind.Variable:
+ output.appendNode(
+ new DocHeading({configuration, title: `${scopedName} variable`})
+ );
+ break;
+ default:
+ throw new Error('Unsupported API item kind: ' + apiItem.kind);
+ }
+
+ if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) {
+ if (apiItem.releaseTag === ReleaseTag.Beta) {
+ this._writeBetaWarning(output);
+ }
+ }
+
+ const decoratorBlocks: DocBlock[] = [];
+
+ if (apiItem instanceof ApiDocumentedItem) {
+ const tsdocComment: DocComment | undefined = apiItem.tsdocComment;
+
+ if (tsdocComment) {
+ decoratorBlocks.push(
+ ...tsdocComment.customBlocks.filter(block => {
+ return (
+ block.blockTag.tagNameWithUpperCase ===
+ StandardTags.decorator.tagNameWithUpperCase
+ );
+ })
+ );
+
+ if (tsdocComment.deprecatedBlock) {
+ output.appendNode(
+ new DocNoteBox({configuration: this._tsdocConfiguration}, [
+ new DocParagraph({configuration: this._tsdocConfiguration}, [
+ new DocPlainText({
+ configuration: this._tsdocConfiguration,
+ text: 'Warning: This API is now obsolete. ',
+ }),
+ ]),
+ ...tsdocComment.deprecatedBlock.content.nodes,
+ ])
+ );
+ }
+
+ this._appendSection(output, tsdocComment.summarySection);
+ }
+ }
+
+ if (apiItem instanceof ApiDeclaredItem) {
+ if (apiItem.excerpt.text.length > 0) {
+ output.appendNode(
+ new DocHeading({configuration, title: 'Signature:', level: 4})
+ );
+
+ let code: string;
+ switch (apiItem.parent?.kind) {
+ case ApiItemKind.Class:
+ code = `class ${
+ apiItem.parent.displayName
+ } {${apiItem.getExcerptWithModifiers()}}`;
+ break;
+ case ApiItemKind.Interface:
+ code = `interface ${
+ apiItem.parent.displayName
+ } {${apiItem.getExcerptWithModifiers()}}`;
+ break;
+ default:
+ code = apiItem.getExcerptWithModifiers();
+ }
+ output.appendNode(
+ new DocFencedCode({
+ configuration,
+ code: code,
+ language: 'typescript',
+ })
+ );
+ }
+
+ this._writeHeritageTypes(output, apiItem);
+ }
+
+ if (decoratorBlocks.length > 0) {
+ output.appendNode(
+ new DocHeading({configuration, title: 'Decorators:', level: 4})
+ );
+ for (const decoratorBlock of decoratorBlocks) {
+ output.appendNodes(decoratorBlock.content.nodes);
+ }
+ }
+
+ let appendRemarks = true;
+ switch (apiItem.kind) {
+ case ApiItemKind.Class:
+ case ApiItemKind.Interface:
+ case ApiItemKind.Namespace:
+ case ApiItemKind.Package:
+ this._writeRemarksSection(output, apiItem);
+ appendRemarks = false;
+ break;
+ }
+
+ switch (apiItem.kind) {
+ case ApiItemKind.Class:
+ this._writeClassTables(output, apiItem as ApiClass);
+ break;
+ case ApiItemKind.Enum:
+ this._writeEnumTables(output, apiItem as ApiEnum);
+ break;
+ case ApiItemKind.Interface:
+ this._writeInterfaceTables(output, apiItem as ApiInterface);
+ break;
+ case ApiItemKind.Constructor:
+ case ApiItemKind.ConstructSignature:
+ case ApiItemKind.Method:
+ case ApiItemKind.MethodSignature:
+ case ApiItemKind.Function:
+ this._writeParameterTables(output, apiItem as ApiParameterListMixin);
+ this._writeThrowsSection(output, apiItem);
+ break;
+ case ApiItemKind.Namespace:
+ this._writePackageOrNamespaceTables(output, apiItem as ApiNamespace);
+ break;
+ case ApiItemKind.Model:
+ this._writeModelTable(output, apiItem as ApiModel);
+ break;
+ case ApiItemKind.Package:
+ this._writePackageOrNamespaceTables(output, apiItem as ApiPackage);
+ break;
+ case ApiItemKind.Property:
+ case ApiItemKind.PropertySignature:
+ break;
+ case ApiItemKind.TypeAlias:
+ break;
+ case ApiItemKind.Variable:
+ break;
+ default:
+ throw new Error('Unsupported API item kind: ' + apiItem.kind);
+ }
+
+ this._writeDefaultValueSection(output, apiItem);
+
+ if (appendRemarks) {
+ this._writeRemarksSection(output, apiItem);
+ }
+
+ const filename: string = path.join(
+ this._outputFolder,
+ this._getFilenameForApiItem(apiItem)
+ );
+ const stringBuilder: StringBuilder = new StringBuilder();
+
+ this._markdownEmitter.emit(stringBuilder, output, {
+ contextApiItem: apiItem,
+ onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => {
+ return this._getLinkFilenameForApiItem(apiItemForFilename);
+ },
+ });
+
+ let pageContent: string = stringBuilder.toString();
+
+ if (this._pluginLoader.markdownDocumenterFeature) {
+ // Allow the plugin to customize the pageContent
+ const eventArgs: IMarkdownDocumenterFeatureOnBeforeWritePageArgs = {
+ apiItem: apiItem,
+ outputFilename: filename,
+ pageContent: pageContent,
+ };
+ this._pluginLoader.markdownDocumenterFeature.onBeforeWritePage(eventArgs);
+ pageContent = eventArgs.pageContent;
+ }
+
+ pageContent =
+ `---\nsidebar_label: ${this._getSidebarLabelForApiItem(apiItem)}\n---` +
+ pageContent;
+ pageContent = pageContent.replace('##', '#');
+ pageContent = pageContent.replace(/<!-- -->/g, '');
+ pageContent = pageContent.replace(/\\\*\\\*/g, '**');
+ pageContent = pageContent.replace(/<b>|<\/b>/g, '**');
+ FileSystem.writeFile(filename, pageContent, {
+ convertLineEndings: this._documenterConfig
+ ? this._documenterConfig.newlineKind
+ : NewlineKind.CrLf,
+ });
+ }
+
+ private _writeHeritageTypes(
+ output: DocSection,
+ apiItem: ApiDeclaredItem
+ ): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ if (apiItem instanceof ApiClass) {
+ if (apiItem.extendsType) {
+ const extendsParagraph: DocParagraph = new DocParagraph(
+ {configuration},
+ [
+ new DocEmphasisSpan({configuration, bold: true}, [
+ new DocPlainText({configuration, text: 'Extends: '}),
+ ]),
+ ]
+ );
+ this._appendExcerptWithHyperlinks(
+ extendsParagraph,
+ apiItem.extendsType.excerpt
+ );
+ output.appendNode(extendsParagraph);
+ }
+ if (apiItem.implementsTypes.length > 0) {
+ const extendsParagraph: DocParagraph = new DocParagraph(
+ {configuration},
+ [
+ new DocEmphasisSpan({configuration, bold: true}, [
+ new DocPlainText({configuration, text: 'Implements: '}),
+ ]),
+ ]
+ );
+ let needsComma = false;
+ for (const implementsType of apiItem.implementsTypes) {
+ if (needsComma) {
+ extendsParagraph.appendNode(
+ new DocPlainText({configuration, text: ', '})
+ );
+ }
+ this._appendExcerptWithHyperlinks(
+ extendsParagraph,
+ implementsType.excerpt
+ );
+ needsComma = true;
+ }
+ output.appendNode(extendsParagraph);
+ }
+ }
+
+ if (apiItem instanceof ApiInterface) {
+ if (apiItem.extendsTypes.length > 0) {
+ const extendsParagraph: DocParagraph = new DocParagraph(
+ {configuration},
+ [
+ new DocEmphasisSpan({configuration, bold: true}, [
+ new DocPlainText({configuration, text: 'Extends: '}),
+ ]),
+ ]
+ );
+ let needsComma = false;
+ for (const extendsType of apiItem.extendsTypes) {
+ if (needsComma) {
+ extendsParagraph.appendNode(
+ new DocPlainText({configuration, text: ', '})
+ );
+ }
+ this._appendExcerptWithHyperlinks(
+ extendsParagraph,
+ extendsType.excerpt
+ );
+ needsComma = true;
+ }
+ output.appendNode(extendsParagraph);
+ }
+ }
+
+ if (apiItem instanceof ApiTypeAlias) {
+ const refs: ExcerptToken[] = apiItem.excerptTokens.filter(token => {
+ return (
+ token.kind === ExcerptTokenKind.Reference &&
+ token.canonicalReference &&
+ this._apiModel.resolveDeclarationReference(
+ token.canonicalReference,
+ undefined
+ ).resolvedApiItem
+ );
+ });
+ if (refs.length > 0) {
+ const referencesParagraph: DocParagraph = new DocParagraph(
+ {configuration},
+ [
+ new DocEmphasisSpan({configuration, bold: true}, [
+ new DocPlainText({configuration, text: 'References: '}),
+ ]),
+ ]
+ );
+ let needsComma = false;
+ const visited = new Set<string>();
+ for (const ref of refs) {
+ if (visited.has(ref.text)) {
+ continue;
+ }
+ visited.add(ref.text);
+
+ if (needsComma) {
+ referencesParagraph.appendNode(
+ new DocPlainText({configuration, text: ', '})
+ );
+ }
+
+ this._appendExcerptTokenWithHyperlinks(referencesParagraph, ref);
+ needsComma = true;
+ }
+ output.appendNode(referencesParagraph);
+ }
+ }
+ }
+
+ private _writeDefaultValueSection(output: DocSection, apiItem: ApiItem) {
+ if (apiItem instanceof ApiDocumentedItem) {
+ const block = apiItem.tsdocComment?.customBlocks.find(block => {
+ return (
+ block.blockTag.tagNameWithUpperCase ===
+ StandardTags.defaultValue.tagNameWithUpperCase
+ );
+ });
+ if (block) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Default value:',
+ level: 4,
+ })
+ );
+ this._appendSection(output, block.content);
+ }
+ }
+ }
+
+ private _writeRemarksSection(output: DocSection, apiItem: ApiItem): void {
+ if (apiItem instanceof ApiDocumentedItem) {
+ const tsdocComment: DocComment | undefined = apiItem.tsdocComment;
+
+ if (tsdocComment) {
+ // Write the @remarks block
+ if (tsdocComment.remarksBlock) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Remarks',
+ })
+ );
+ this._appendSection(output, tsdocComment.remarksBlock.content);
+ }
+
+ // Write the @example blocks
+ const exampleBlocks: DocBlock[] = tsdocComment.customBlocks.filter(
+ x => {
+ return (
+ x.blockTag.tagNameWithUpperCase ===
+ StandardTags.example.tagNameWithUpperCase
+ );
+ }
+ );
+
+ let exampleNumber = 1;
+ for (const exampleBlock of exampleBlocks) {
+ const heading: string =
+ exampleBlocks.length > 1 ? `Example ${exampleNumber}` : 'Example';
+
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: heading,
+ })
+ );
+
+ this._appendSection(output, exampleBlock.content);
+
+ ++exampleNumber;
+ }
+ }
+ }
+ }
+
+ private _writeThrowsSection(output: DocSection, apiItem: ApiItem): void {
+ if (apiItem instanceof ApiDocumentedItem) {
+ const tsdocComment: DocComment | undefined = apiItem.tsdocComment;
+
+ if (tsdocComment) {
+ // Write the @throws blocks
+ const throwsBlocks: DocBlock[] = tsdocComment.customBlocks.filter(x => {
+ return (
+ x.blockTag.tagNameWithUpperCase ===
+ StandardTags.throws.tagNameWithUpperCase
+ );
+ });
+
+ if (throwsBlocks.length > 0) {
+ const heading = 'Exceptions';
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: heading,
+ })
+ );
+
+ for (const throwsBlock of throwsBlocks) {
+ this._appendSection(output, throwsBlock.content);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * GENERATE PAGE: MODEL
+ */
+ private _writeModelTable(output: DocSection, apiModel: ApiModel): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const packagesTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Package', 'Description'],
+ });
+
+ for (const apiMember of apiModel.members) {
+ const row: DocTableRow = new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ ]);
+
+ switch (apiMember.kind) {
+ case ApiItemKind.Package:
+ packagesTable.addRow(row);
+ this._writeApiItemPage(apiMember);
+ break;
+ }
+ }
+
+ if (packagesTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Packages',
+ })
+ );
+ output.appendNode(packagesTable);
+ }
+ }
+
+ /**
+ * GENERATE PAGE: PACKAGE or NAMESPACE
+ */
+ private _writePackageOrNamespaceTables(
+ output: DocSection,
+ apiContainer: ApiPackage | ApiNamespace
+ ): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const classesTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Class', 'Description'],
+ });
+
+ const enumerationsTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Enumeration', 'Description'],
+ });
+
+ const functionsTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Function', 'Description'],
+ });
+
+ const interfacesTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Interface', 'Description'],
+ });
+
+ const namespacesTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Namespace', 'Description'],
+ });
+
+ const variablesTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Variable', 'Description'],
+ });
+
+ const typeAliasesTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Type Alias', 'Description'],
+ });
+
+ const apiMembers: readonly ApiItem[] =
+ apiContainer.kind === ApiItemKind.Package
+ ? (apiContainer as ApiPackage).entryPoints[0]!.members
+ : (apiContainer as ApiNamespace).members;
+
+ for (const apiMember of apiMembers) {
+ const row: DocTableRow = new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ ]);
+
+ switch (apiMember.kind) {
+ case ApiItemKind.Class:
+ classesTable.addRow(row);
+ this._writeApiItemPage(apiMember);
+ break;
+
+ case ApiItemKind.Enum:
+ enumerationsTable.addRow(row);
+ this._writeApiItemPage(apiMember);
+ break;
+
+ case ApiItemKind.Interface:
+ interfacesTable.addRow(row);
+ this._writeApiItemPage(apiMember);
+ break;
+
+ case ApiItemKind.Namespace:
+ namespacesTable.addRow(row);
+ this._writeApiItemPage(apiMember);
+ break;
+
+ case ApiItemKind.Function:
+ functionsTable.addRow(row);
+ this._writeApiItemPage(apiMember);
+ break;
+
+ case ApiItemKind.TypeAlias:
+ typeAliasesTable.addRow(row);
+ this._writeApiItemPage(apiMember);
+ break;
+
+ case ApiItemKind.Variable:
+ variablesTable.addRow(row);
+ this._writeApiItemPage(apiMember);
+ break;
+ }
+ }
+
+ if (classesTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Classes',
+ })
+ );
+ output.appendNode(classesTable);
+ }
+
+ if (enumerationsTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Enumerations',
+ })
+ );
+ output.appendNode(enumerationsTable);
+ }
+ if (functionsTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Functions',
+ })
+ );
+ output.appendNode(functionsTable);
+ }
+
+ if (interfacesTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Interfaces',
+ })
+ );
+ output.appendNode(interfacesTable);
+ }
+
+ if (namespacesTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Namespaces',
+ })
+ );
+ output.appendNode(namespacesTable);
+ }
+
+ if (variablesTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Variables',
+ })
+ );
+ output.appendNode(variablesTable);
+ }
+
+ if (typeAliasesTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Type Aliases',
+ })
+ );
+ output.appendNode(typeAliasesTable);
+ }
+ }
+
+ /**
+ * GENERATE PAGE: CLASS
+ */
+ private _writeClassTables(output: DocSection, apiClass: ApiClass): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const eventsTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Property', 'Modifiers', 'Type', 'Description'],
+ });
+
+ const constructorsTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Constructor', 'Modifiers', 'Description'],
+ });
+
+ const propertiesTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Property', 'Modifiers', 'Type', 'Description'],
+ });
+
+ const methodsTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Method', 'Modifiers', 'Description'],
+ });
+
+ for (const apiMember of apiClass.members) {
+ switch (apiMember.kind) {
+ case ApiItemKind.Constructor: {
+ constructorsTable.addRow(
+ new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember),
+ this._createModifiersCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ ])
+ );
+
+ this._writeApiItemPage(apiMember);
+ break;
+ }
+ case ApiItemKind.Method: {
+ methodsTable.addRow(
+ new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember),
+ this._createModifiersCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ ])
+ );
+
+ this._writeApiItemPage(apiMember);
+ break;
+ }
+ case ApiItemKind.Property: {
+ if ((apiMember as ApiPropertyItem).isEventProperty) {
+ eventsTable.addRow(
+ new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember, true),
+ this._createModifiersCell(apiMember),
+ this._createPropertyTypeCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ ])
+ );
+ } else {
+ propertiesTable.addRow(
+ new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember, true),
+ this._createModifiersCell(apiMember),
+ this._createPropertyTypeCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ ])
+ );
+ }
+ break;
+ }
+ }
+ }
+
+ if (eventsTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Events',
+ })
+ );
+ output.appendNode(eventsTable);
+ }
+
+ if (constructorsTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Constructors',
+ })
+ );
+ output.appendNode(constructorsTable);
+ }
+
+ if (propertiesTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Properties',
+ })
+ );
+ output.appendNode(propertiesTable);
+ }
+
+ if (methodsTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Methods',
+ })
+ );
+ output.appendNode(methodsTable);
+ }
+ }
+
+ /**
+ * GENERATE PAGE: ENUM
+ */
+ private _writeEnumTables(output: DocSection, apiEnum: ApiEnum): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const enumMembersTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Member', 'Value', 'Description'],
+ });
+
+ for (const apiEnumMember of apiEnum.members) {
+ enumMembersTable.addRow(
+ new DocTableRow({configuration}, [
+ new DocTableCell({configuration}, [
+ new DocParagraph({configuration}, [
+ new DocPlainText({
+ configuration,
+ text: Utilities.getConciseSignature(apiEnumMember),
+ }),
+ ]),
+ ]),
+ this._createInitializerCell(apiEnumMember),
+ this._createDescriptionCell(apiEnumMember),
+ ])
+ );
+ }
+
+ if (enumMembersTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Enumeration Members',
+ })
+ );
+ output.appendNode(enumMembersTable);
+ }
+ }
+
+ /**
+ * GENERATE PAGE: INTERFACE
+ */
+ private _writeInterfaceTables(
+ output: DocSection,
+ apiClass: ApiInterface
+ ): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const eventsTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Property', 'Modifiers', 'Type', 'Description'],
+ });
+
+ const propertiesTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Property', 'Modifiers', 'Type', 'Description', 'Default'],
+ });
+
+ const methodsTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Method', 'Description'],
+ });
+
+ for (const apiMember of apiClass.members) {
+ switch (apiMember.kind) {
+ case ApiItemKind.ConstructSignature:
+ case ApiItemKind.MethodSignature: {
+ methodsTable.addRow(
+ new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ ])
+ );
+
+ this._writeApiItemPage(apiMember);
+ break;
+ }
+ case ApiItemKind.PropertySignature: {
+ if ((apiMember as ApiPropertyItem).isEventProperty) {
+ eventsTable.addRow(
+ new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember, true),
+ this._createModifiersCell(apiMember),
+ this._createPropertyTypeCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ ])
+ );
+ } else {
+ propertiesTable.addRow(
+ new DocTableRow({configuration}, [
+ this._createTitleCell(apiMember, true),
+ this._createModifiersCell(apiMember),
+ this._createPropertyTypeCell(apiMember),
+ this._createDescriptionCell(apiMember),
+ this._createDefaultCell(apiMember),
+ ])
+ );
+ }
+ break;
+ }
+ }
+ }
+
+ if (eventsTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Events',
+ })
+ );
+ output.appendNode(eventsTable);
+ }
+
+ if (propertiesTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Properties',
+ })
+ );
+ output.appendNode(propertiesTable);
+ }
+
+ if (methodsTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Methods',
+ })
+ );
+ output.appendNode(methodsTable);
+ }
+ }
+
+ /**
+ * GENERATE PAGE: FUNCTION-LIKE
+ */
+ private _writeParameterTables(
+ output: DocSection,
+ apiParameterListMixin: ApiParameterListMixin
+ ): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const parametersTable: DocTable = new DocTable({
+ configuration,
+ headerTitles: ['Parameter', 'Type', 'Description'],
+ });
+ for (const apiParameter of apiParameterListMixin.parameters) {
+ const parameterDescription: DocSection = new DocSection({configuration});
+
+ if (apiParameter.isOptional) {
+ parameterDescription.appendNodesInParagraph([
+ new DocEmphasisSpan({configuration, italic: true}, [
+ new DocPlainText({configuration, text: '(Optional)'}),
+ ]),
+ new DocPlainText({configuration, text: ' '}),
+ ]);
+ }
+
+ if (apiParameter.tsdocParamBlock) {
+ this._appendAndMergeSection(
+ parameterDescription,
+ apiParameter.tsdocParamBlock.content
+ );
+ }
+
+ parametersTable.addRow(
+ new DocTableRow({configuration}, [
+ new DocTableCell({configuration}, [
+ new DocParagraph({configuration}, [
+ new DocPlainText({configuration, text: apiParameter.name}),
+ ]),
+ ]),
+ new DocTableCell({configuration}, [
+ this._createParagraphForTypeExcerpt(
+ apiParameter.parameterTypeExcerpt
+ ),
+ ]),
+ new DocTableCell({configuration}, parameterDescription.nodes),
+ ])
+ );
+ }
+
+ if (parametersTable.rows.length > 0) {
+ output.appendNode(
+ new DocHeading({
+ configuration: this._tsdocConfiguration,
+ title: 'Parameters',
+ })
+ );
+ output.appendNode(parametersTable);
+ }
+
+ if (ApiReturnTypeMixin.isBaseClassOf(apiParameterListMixin)) {
+ const returnTypeExcerpt: Excerpt =
+ apiParameterListMixin.returnTypeExcerpt;
+ output.appendNode(
+ new DocParagraph({configuration}, [
+ new DocEmphasisSpan({configuration, bold: true}, [
+ new DocPlainText({configuration, text: 'Returns:'}),
+ ]),
+ ])
+ );
+
+ output.appendNode(this._createParagraphForTypeExcerpt(returnTypeExcerpt));
+
+ if (apiParameterListMixin instanceof ApiDocumentedItem) {
+ if (
+ apiParameterListMixin.tsdocComment &&
+ apiParameterListMixin.tsdocComment.returnsBlock
+ ) {
+ this._appendSection(
+ output,
+ apiParameterListMixin.tsdocComment.returnsBlock.content
+ );
+ }
+ }
+ }
+ }
+
+ private _createParagraphForTypeExcerpt(excerpt: Excerpt): DocParagraph {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const paragraph: DocParagraph = new DocParagraph({configuration});
+ if (!excerpt.text.trim()) {
+ paragraph.appendNode(
+ new DocPlainText({configuration, text: '(not declared)'})
+ );
+ } else {
+ this._appendExcerptWithHyperlinks(paragraph, excerpt);
+ }
+
+ return paragraph;
+ }
+
+ private _appendExcerptWithHyperlinks(
+ docNodeContainer: DocNodeContainer,
+ excerpt: Excerpt
+ ): void {
+ for (const token of excerpt.spannedTokens) {
+ this._appendExcerptTokenWithHyperlinks(docNodeContainer, token);
+ }
+ }
+
+ private _appendExcerptTokenWithHyperlinks(
+ docNodeContainer: DocNodeContainer,
+ token: ExcerptToken
+ ): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ // Markdown doesn't provide a standardized syntax for hyperlinks inside code
+ // spans, so we will render the type expression as DocPlainText. Instead of
+ // creating multiple DocParagraphs, we can simply discard any newlines and
+ // let the renderer do normal word-wrapping.
+ const unwrappedTokenText: string = token.text.replace(/[\r\n]+/g, ' ');
+
+ // If it's hyperlinkable, then append a DocLinkTag
+ if (token.kind === ExcerptTokenKind.Reference && token.canonicalReference) {
+ const apiItemResult: IResolveDeclarationReferenceResult =
+ this._apiModel.resolveDeclarationReference(
+ token.canonicalReference,
+ undefined
+ );
+
+ if (apiItemResult.resolvedApiItem) {
+ docNodeContainer.appendNode(
+ new DocLinkTag({
+ configuration,
+ tagName: StandardTags.link.tagName,
+ linkText: unwrappedTokenText,
+ urlDestination: this._getLinkFilenameForApiItem(
+ apiItemResult.resolvedApiItem
+ ),
+ })
+ );
+ return;
+ }
+ }
+
+ // Otherwise append non-hyperlinked text
+ docNodeContainer.appendNode(
+ new DocPlainText({configuration, text: unwrappedTokenText})
+ );
+ }
+
+ private _createTitleCell(apiItem: ApiItem, plain = false): DocTableCell {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const text: string = Utilities.getConciseSignature(apiItem);
+
+ return new DocTableCell({configuration}, [
+ new DocParagraph({configuration}, [
+ plain
+ ? new DocPlainText({configuration, text})
+ : new DocLinkTag({
+ configuration,
+ tagName: '@link',
+ linkText: text,
+ urlDestination: this._getLinkFilenameForApiItem(apiItem),
+ }),
+ ]),
+ ]);
+ }
+
+ /**
+ * This generates a DocTableCell for an ApiItem including the summary section
+ * and "(BETA)" annotation.
+ *
+ * @remarks
+ * We mostly assume that the input is an ApiDocumentedItem, but it's easier to
+ * perform this as a runtime check than to have each caller perform a type
+ * cast.
+ */
+ private _createDescriptionCell(apiItem: ApiItem): DocTableCell {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const section: DocSection = new DocSection({configuration});
+
+ if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) {
+ if (apiItem.releaseTag === ReleaseTag.Beta) {
+ section.appendNodesInParagraph([
+ new DocEmphasisSpan({configuration, bold: true, italic: true}, [
+ new DocPlainText({configuration, text: '(BETA)'}),
+ ]),
+ new DocPlainText({configuration, text: ' '}),
+ ]);
+ }
+ }
+
+ if (apiItem instanceof ApiDocumentedItem) {
+ if (apiItem.tsdocComment !== undefined) {
+ this._appendAndMergeSection(
+ section,
+ apiItem.tsdocComment.summarySection
+ );
+ }
+ }
+
+ return new DocTableCell({configuration}, section.nodes);
+ }
+
+ private _createDefaultCell(apiItem: ApiItem): DocTableCell {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ if (apiItem instanceof ApiDocumentedItem) {
+ const block = apiItem.tsdocComment?.customBlocks.find(block => {
+ return (
+ block.blockTag.tagNameWithUpperCase ===
+ StandardTags.defaultValue.tagNameWithUpperCase
+ );
+ });
+ if (block !== undefined) {
+ return new DocTableCell({configuration}, block.content.getChildNodes());
+ }
+ }
+
+ return new DocTableCell({configuration}, []);
+ }
+
+ private _createModifiersCell(apiItem: ApiItem): DocTableCell {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const section: DocSection = new DocSection({configuration});
+
+ if (ApiProtectedMixin.isBaseClassOf(apiItem)) {
+ if (apiItem.isProtected) {
+ section.appendNode(
+ new DocParagraph({configuration}, [
+ new DocCodeSpan({configuration, code: 'protected'}),
+ ])
+ );
+ }
+ }
+
+ if (ApiReadonlyMixin.isBaseClassOf(apiItem)) {
+ if (apiItem.isReadonly) {
+ section.appendNode(
+ new DocParagraph({configuration}, [
+ new DocCodeSpan({configuration, code: 'readonly'}),
+ ])
+ );
+ }
+ }
+
+ if (ApiStaticMixin.isBaseClassOf(apiItem)) {
+ if (apiItem.isStatic) {
+ section.appendNode(
+ new DocParagraph({configuration}, [
+ new DocCodeSpan({configuration, code: 'static'}),
+ ])
+ );
+ }
+ }
+
+ if (ApiOptionalMixin.isBaseClassOf(apiItem)) {
+ if (apiItem.isOptional) {
+ section.appendNode(
+ new DocParagraph({configuration}, [
+ new DocCodeSpan({configuration, code: 'optional'}),
+ ])
+ );
+ }
+ }
+
+ return new DocTableCell({configuration}, section.nodes);
+ }
+
+ private _createPropertyTypeCell(apiItem: ApiItem): DocTableCell {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const section: DocSection = new DocSection({configuration});
+
+ if (apiItem instanceof ApiPropertyItem) {
+ section.appendNode(
+ this._createParagraphForTypeExcerpt(apiItem.propertyTypeExcerpt)
+ );
+ }
+
+ return new DocTableCell({configuration}, section.nodes);
+ }
+
+ private _createInitializerCell(apiItem: ApiItem): DocTableCell {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+
+ const section: DocSection = new DocSection({configuration});
+
+ if (ApiInitializerMixin.isBaseClassOf(apiItem)) {
+ if (apiItem.initializerExcerpt) {
+ section.appendNodeInParagraph(
+ new DocCodeSpan({
+ configuration,
+ code: apiItem.initializerExcerpt.text,
+ })
+ );
+ }
+ }
+
+ return new DocTableCell({configuration}, section.nodes);
+ }
+
+ private _writeBetaWarning(output: DocSection): void {
+ const configuration: TSDocConfiguration = this._tsdocConfiguration;
+ const betaWarning: string =
+ 'This API is provided as a preview for developers and may change' +
+ ' based on feedback that we receive. Do not use this API in a production environment.';
+ output.appendNode(
+ new DocNoteBox({configuration}, [
+ new DocParagraph({configuration}, [
+ new DocPlainText({configuration, text: betaWarning}),
+ ]),
+ ])
+ );
+ }
+
+ private _appendSection(output: DocSection, docSection: DocSection): void {
+ for (const node of docSection.nodes) {
+ output.appendNode(node);
+ }
+ }
+
+ private _appendAndMergeSection(
+ output: DocSection,
+ docSection: DocSection
+ ): void {
+ let firstNode = true;
+ for (const node of docSection.nodes) {
+ if (firstNode) {
+ if (node.kind === DocNodeKind.Paragraph) {
+ output.appendNodesInParagraph(node.getChildNodes());
+ firstNode = false;
+ continue;
+ }
+ }
+ firstNode = false;
+
+ output.appendNode(node);
+ }
+ }
+
+ private _getSidebarLabelForApiItem(apiItem: ApiItem): string {
+ if (apiItem.kind === ApiItemKind.Package) {
+ return 'API';
+ }
+
+ let baseName = '';
+ for (const hierarchyItem of apiItem.getHierarchy()) {
+ // For overloaded methods, add a suffix such as "MyClass.myMethod_2".
+ let qualifiedName: string = hierarchyItem.displayName;
+ if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) {
+ if (hierarchyItem.overloadIndex > 1) {
+ // Subtract one for compatibility with earlier releases of API Documenter.
+ qualifiedName += `_${hierarchyItem.overloadIndex - 1}`;
+ }
+ }
+
+ switch (hierarchyItem.kind) {
+ case ApiItemKind.Model:
+ case ApiItemKind.EntryPoint:
+ case ApiItemKind.EnumMember:
+ case ApiItemKind.Package:
+ break;
+ default:
+ baseName += qualifiedName + '.';
+ }
+ }
+ return baseName.slice(0, baseName.length - 1);
+ }
+
+ private _getFilenameForApiItem(apiItem: ApiItem): string {
+ if (apiItem.kind === ApiItemKind.Package) {
+ return 'index.md';
+ }
+
+ let baseName = '';
+ for (const hierarchyItem of apiItem.getHierarchy()) {
+ // For overloaded methods, add a suffix such as "MyClass.myMethod_2".
+ let qualifiedName: string = Utilities.getSafeFilenameForName(
+ hierarchyItem.displayName
+ );
+ if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) {
+ if (hierarchyItem.overloadIndex > 1) {
+ // Subtract one for compatibility with earlier releases of API Documenter.
+ // (This will get revamped when we fix GitHub issue #1308)
+ qualifiedName += `_${hierarchyItem.overloadIndex - 1}`;
+ }
+ }
+
+ switch (hierarchyItem.kind) {
+ case ApiItemKind.Model:
+ case ApiItemKind.EntryPoint:
+ case ApiItemKind.EnumMember:
+ break;
+ case ApiItemKind.Package:
+ baseName = Utilities.getSafeFilenameForName(
+ PackageName.getUnscopedName(hierarchyItem.displayName)
+ );
+ break;
+ default:
+ baseName += '.' + qualifiedName;
+ }
+ }
+ return baseName + '.md';
+ }
+
+ private _getLinkFilenameForApiItem(apiItem: ApiItem): string {
+ return './' + this._getFilenameForApiItem(apiItem);
+ }
+
+ private _deleteOldOutputFiles(): void {
+ console.log('Deleting old output from ' + this._outputFolder);
+ FileSystem.ensureEmptyFolder(this._outputFolder);
+ }
+}
diff --git a/remote/test/puppeteer/tools/docgen/src/docgen.ts b/remote/test/puppeteer/tools/docgen/src/docgen.ts
new file mode 100644
index 0000000000..c7bafdab3d
--- /dev/null
+++ b/remote/test/puppeteer/tools/docgen/src/docgen.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ApiModel} from '@microsoft/api-extractor-model';
+
+import {MarkdownDocumenter} from './custom_markdown_documenter.js';
+
+export function docgen(jsonPath: string, outputDir: string): void {
+ const apiModel = new ApiModel();
+ apiModel.loadPackage(jsonPath);
+
+ const markdownDocumenter: MarkdownDocumenter = new MarkdownDocumenter({
+ apiModel: apiModel,
+ documenterConfig: undefined,
+ outputFolder: outputDir,
+ });
+ markdownDocumenter.generateFiles();
+}
+
+export function spliceIntoSection(
+ sectionName: string,
+ content: string,
+ sectionContent: string
+): string {
+ const lines = content.split('\n');
+ const offset =
+ lines.findIndex(line => {
+ return line.includes(`<!-- ${sectionName}-start -->`);
+ }) + 1;
+ const limit = lines.slice(offset).findIndex(line => {
+ return line.includes(`<!-- ${sectionName}-end -->`);
+ });
+ lines.splice(offset, limit, ...sectionContent.split('\n'));
+ return lines.join('\n');
+}
diff --git a/remote/test/puppeteer/tools/docgen/tsconfig.json b/remote/test/puppeteer/tools/docgen/tsconfig.json
new file mode 100644
index 0000000000..fcaf1db737
--- /dev/null
+++ b/remote/test/puppeteer/tools/docgen/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./lib",
+ "sourceMap": true,
+ "declaration": false,
+ "declarationMap": false,
+ "composite": false,
+ },
+}
diff --git a/remote/test/puppeteer/tools/docgen/tsdoc.json b/remote/test/puppeteer/tools/docgen/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/tools/docgen/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/tools/doctest/package.json b/remote/test/puppeteer/tools/doctest/package.json
new file mode 100644
index 0000000000..8c7e9544d0
--- /dev/null
+++ b/remote/test/puppeteer/tools/doctest/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@puppeteer/doctest",
+ "version": "0.1.0",
+ "type": "module",
+ "private": true,
+ "bin": "./bin/doctest.js",
+ "description": "Tests JSDoc @example code within a file.",
+ "license": "Apache-2.0",
+ "scripts": {
+ "build": "wireit",
+ "clean": "../clean.js"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b && chmod +x ./bin/doctest.js",
+ "clean": "if-file-deleted",
+ "files": [
+ "src/**"
+ ],
+ "output": [
+ "bin/**",
+ "tsconfig.tsbuildinfo"
+ ]
+ }
+ },
+ "devDependencies": {
+ "@swc/core": "1.3.107",
+ "@types/doctrine": "0.0.9",
+ "@types/source-map-support": "0.5.10",
+ "@types/yargs": "17.0.32",
+ "acorn": "8.11.3",
+ "doctrine": "3.0.0",
+ "glob": "10.3.10",
+ "pkg-dir": "8.0.0",
+ "source-map-support": "0.5.21",
+ "source-map": "0.7.4",
+ "yargs": "17.7.2"
+ }
+}
diff --git a/remote/test/puppeteer/tools/doctest/src/doctest.ts b/remote/test/puppeteer/tools/doctest/src/doctest.ts
new file mode 100644
index 0000000000..34349ef766
--- /dev/null
+++ b/remote/test/puppeteer/tools/doctest/src/doctest.ts
@@ -0,0 +1,349 @@
+#! /usr/bin/env -S node --test-reporter spec
+
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * `@puppeteer/doctest` tests `@example` code within a JavaScript file.
+ *
+ * There are a few reasonable assumptions for this tool to work:
+ *
+ * 1. Examples are written in block comments, not line comments.
+ * 2. Examples do not use packages that are not available to the file it exists
+ * in. (Note the package will always be available).
+ * 3. Examples are strictly written between code fences (\`\`\`) on separate
+ * lines. For example, \`\`\`console.log(1)\`\`\` is not allowed.
+ * 4. Code is written using ES modules.
+ *
+ * By default, code blocks are interpreted as JavaScript. Use \`\`\`ts to change
+ * the language. In general, the format is "\`\`\`[language] [ignore] [fail]".
+ *
+ * If there are several code blocks within an example, they are concatenated.
+ */
+import 'source-map-support/register.js';
+
+import assert from 'node:assert';
+import {createHash} from 'node:crypto';
+import {mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
+import {basename, dirname, join, relative, resolve} from 'node:path';
+import {test} from 'node:test';
+import {pathToFileURL} from 'node:url';
+
+import {transform, type Output} from '@swc/core';
+import {parse as parseJs} from 'acorn';
+import {parse, type Tag} from 'doctrine';
+import {Glob} from 'glob';
+import {packageDirectory} from 'pkg-dir';
+import {
+ SourceMapConsumer,
+ SourceMapGenerator,
+ type RawSourceMap,
+} from 'source-map';
+import yargs from 'yargs';
+import {hideBin} from 'yargs/helpers';
+
+// This is 1-indexed.
+interface Position {
+ line: number;
+ column: number;
+}
+
+interface Comment {
+ file: string;
+ text: string;
+ position: Position;
+}
+
+interface ExtractedSourceLocation {
+ // File path to the original source code.
+ origin: string;
+ // Mappings from the extracted code to the original code.
+ positions: Array<{
+ // The 1-indexed line number for the extracted code.
+ extracted: number;
+ // The position in the original code.
+ original: Position;
+ }>;
+}
+
+interface ExampleCode extends ExtractedSourceLocation {
+ language: Language;
+ code: string;
+ fail: boolean;
+}
+
+const enum Language {
+ JavaScript,
+ TypeScript,
+}
+
+const CODE_FENCE = '```';
+const BLOCK_COMMENT_START = ' * ';
+
+const {files = []} = await yargs(hideBin(process.argv))
+ .scriptName('@puppeteer/doctest')
+ .command('* <files..>', `JSDoc @example code tester.`)
+ .positional('files', {
+ describe: 'Files to test',
+ type: 'string',
+ })
+ .array('files')
+ .version(false)
+ .help()
+ .parse();
+
+for await (const file of new Glob(files, {})) {
+ void test(file, async context => {
+ const testDirectory = await createTestDirectory(file);
+ context.after(async () => {
+ if (!process.env['KEEP_TESTS']) {
+ await rm(testDirectory, {force: true, recursive: true});
+ }
+ });
+ const tests = [];
+ for (const example of await extractJSDocComments(file).then(
+ extractExampleCode
+ )) {
+ tests.push(
+ context.test(
+ `${file}:${example.positions[0]!.original.line}:${
+ example.positions[0]!.original.column
+ }`,
+ async () => {
+ await run(testDirectory, example);
+ }
+ )
+ );
+ }
+ await Promise.all(tests);
+ });
+}
+
+async function createTestDirectory(file: string) {
+ const dir = await packageDirectory({cwd: dirname(file)});
+ if (!dir) {
+ throw new Error(`Could not find package root for ${file}.`);
+ }
+
+ return await mkdtemp(join(dir, 'doctest-'));
+}
+
+async function run(tempdir: string, example: Readonly<ExampleCode>) {
+ const path = getTestPath(tempdir, example.code);
+ await compile(example.language, example.code, path, example);
+ try {
+ await import(pathToFileURL(path).toString());
+ if (example.fail) {
+ throw new Error(`Expected failure.`);
+ }
+ } catch (error) {
+ if (!example.fail) {
+ throw error;
+ }
+ }
+}
+
+function getTestPath(dir: string, code: string) {
+ return join(
+ dir,
+ `doctest-${createHash('md5').update(code).digest('hex')}.js`
+ );
+}
+
+async function compile(
+ language: Language,
+ sourceCode: string,
+ filePath: string,
+ location: ExtractedSourceLocation
+) {
+ const output = await compileCode(language, sourceCode);
+ const map = await getExtractSourceMap(output.map, filePath, location);
+ await writeFile(filePath, inlineSourceMap(output.code, map));
+}
+
+function inlineSourceMap(code: string, sourceMap: RawSourceMap) {
+ return `${code}\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(
+ JSON.stringify(sourceMap)
+ ).toString('base64')}`;
+}
+
+async function getExtractSourceMap(
+ map: string,
+ generatedFile: string,
+ location: ExtractedSourceLocation
+) {
+ const sourceMap = JSON.parse(map) as RawSourceMap;
+ sourceMap.file = basename(generatedFile);
+ sourceMap.sourceRoot = '';
+ sourceMap.sources = [
+ relative(dirname(generatedFile), resolve(location.origin)),
+ ];
+ const consumer = await new SourceMapConsumer(sourceMap);
+ const generator = new SourceMapGenerator({
+ file: consumer.file,
+ sourceRoot: consumer.sourceRoot,
+ });
+ // We want descending order of the `generated` property.
+ const positions = [...location.positions].reverse();
+ consumer.eachMapping(mapping => {
+ // Note `mapping.originalLine` is the line number with respect to the
+ // extracted, raw code.
+ const {extracted, original} = positions.find(({extracted}) => {
+ return mapping.originalLine >= extracted;
+ })!;
+
+ // `original.line` will account for `extracted`, so we need to subtract
+ // `extracted` to avoid duplicity. We also subtract 1 because `extracted` is
+ // 1-indexed.
+ mapping.originalLine -= extracted - 1;
+
+ generator.addMapping({
+ ...mapping,
+ original: {
+ line: mapping.originalLine + original.line - 1,
+ column: mapping.originalColumn + original.column - 1,
+ },
+ generated: {
+ line: mapping.generatedLine,
+ column: mapping.generatedColumn,
+ },
+ });
+ });
+ return generator.toJSON();
+}
+
+const LANGUAGE_TO_SYNTAX = {
+ [Language.TypeScript]: 'typescript',
+ [Language.JavaScript]: 'ecmascript',
+} as const;
+
+async function compileCode(language: Language, code: string) {
+ return (await transform(code, {
+ sourceMaps: true,
+ inlineSourcesContent: false,
+ jsc: {
+ parser: {
+ syntax: LANGUAGE_TO_SYNTAX[language],
+ },
+ target: 'es2022',
+ },
+ })) as Required<Output>;
+}
+
+const enum Option {
+ Ignore = 'ignore',
+ Fail = 'fail',
+}
+
+function* extractExampleCode(
+ comments: Iterable<Readonly<Comment>>
+): Iterable<Readonly<ExampleCode>> {
+ interface Context {
+ language: Language;
+ fail: boolean;
+ start: number;
+ }
+ for (const {file, text, position: loc} of comments) {
+ const {tags} = parse(text, {
+ unwrap: true,
+ tags: ['example'],
+ lineNumbers: true,
+ preserveWhitespace: true,
+ });
+ for (const {description, lineNumber} of tags as Array<
+ Tag & {lineNumber: number}
+ >) {
+ if (!description) {
+ continue;
+ }
+ const lines = description.split('\n');
+ const blocks: ExampleCode[] = [];
+ let context: Context | undefined;
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i]!;
+ const borderIndex = line.indexOf(CODE_FENCE);
+ if (borderIndex === -1) {
+ continue;
+ }
+ if (context) {
+ blocks.push({
+ language: context.language,
+ code: lines.slice(context.start, i).join('\n'),
+ origin: file,
+ positions: [
+ {
+ extracted: 1,
+ original: {
+ line: loc.line + lineNumber + context.start,
+ column:
+ loc.column + borderIndex + BLOCK_COMMENT_START.length + 1,
+ },
+ },
+ ],
+ fail: context.fail,
+ });
+ context = undefined;
+ continue;
+ }
+ const [tag, ...options] = line
+ .slice(borderIndex + CODE_FENCE.length)
+ .split(' ');
+ if (options.includes(Option.Ignore)) {
+ // Ignore the code sample.
+ continue;
+ }
+ const fail = options.includes(Option.Fail);
+ // Code starts on the next line.
+ const start = i + 1;
+ if (!tag || tag.match(/js|javascript/)) {
+ context = {language: Language.JavaScript, fail, start};
+ } else if (tag.match(/ts|typescript/)) {
+ context = {language: Language.TypeScript, fail, start};
+ }
+ }
+ // Merging the blocks into a single block.
+ yield blocks.reduce(
+ (context, {language, code, positions: [position], fail}, index) => {
+ assert(position);
+ return {
+ origin: file,
+ language: language || context.language,
+ code: `${context.code}\n${code}`,
+ positions: [
+ ...context.positions,
+ {
+ ...position,
+ extracted:
+ context.code.split('\n').length +
+ context.positions.at(-1)!.extracted -
+ // We subtract this because of the accumulated '\n'.
+ (index - 1),
+ },
+ ],
+ fail: fail || context.fail,
+ };
+ }
+ );
+ }
+ }
+}
+
+async function extractJSDocComments(file: string) {
+ const contents = await readFile(file, 'utf8');
+ const comments: Comment[] = [];
+ parseJs(contents, {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ locations: true,
+ sourceFile: file,
+ onComment(isBlock, text, _, __, loc) {
+ if (isBlock) {
+ comments.push({file, text, position: loc!});
+ }
+ },
+ });
+ return comments;
+}
diff --git a/remote/test/puppeteer/tools/doctest/tsconfig.json b/remote/test/puppeteer/tools/doctest/tsconfig.json
new file mode 100644
index 0000000000..bd70c0bd5e
--- /dev/null
+++ b/remote/test/puppeteer/tools/doctest/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./bin",
+ "sourceMap": true,
+ "declaration": false,
+ "declarationMap": false,
+ "composite": false,
+ },
+}
diff --git a/remote/test/puppeteer/tools/doctest/tsdoc.json b/remote/test/puppeteer/tools/doctest/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/tools/doctest/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/tools/download_chrome_bidi.mjs b/remote/test/puppeteer/tools/download_chrome_bidi.mjs
new file mode 100644
index 0000000000..faa73d9a95
--- /dev/null
+++ b/remote/test/puppeteer/tools/download_chrome_bidi.mjs
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/* eslint-disable no-console */
+
+/**
+ * @fileoverview Installs a browser defined in `.browser` for Chromium-BiDi using
+ * `@puppeteer/browsers` to the directory provided as the first argument
+ * (default: cwd). The executable path is written to the `executablePath` output
+ * param for GitHub actions.
+ *
+ * Examples:
+ *
+ * - `node install-browser.mjs`
+ * - `node install-browser.mjs /tmp/cache`
+ */
+import {readFile} from 'node:fs/promises';
+import {createRequire} from 'node:module';
+
+import actions from '@actions/core';
+
+import {computeExecutablePath, install} from '@puppeteer/browsers';
+
+const require = createRequire(import.meta.url);
+
+try {
+ const browserSpec = await readFile(
+ require.resolve('chromium-bidi/.browser', {
+ paths: [require.resolve('puppeteer-core')],
+ }),
+ 'utf-8'
+ );
+ const cacheDir = process.argv[2] || process.cwd();
+ // See .browser for the format.
+ const browser = browserSpec.split('@')[0];
+ const buildId = browserSpec.split('@')[1];
+ await install({
+ browser,
+ buildId,
+ cacheDir,
+ });
+ const executablePath = computeExecutablePath({
+ cacheDir,
+ browser,
+ buildId,
+ });
+ if (process.argv.indexOf('--shell') === -1) {
+ actions.setOutput('executablePath', executablePath);
+ }
+ console.log(executablePath);
+} catch (err) {
+ actions.setFailed(`Failed to download the browser: ${err.message}`);
+}
diff --git a/remote/test/puppeteer/tools/ensure-pinned-deps.ts b/remote/test/puppeteer/tools/ensure-pinned-deps.ts
new file mode 100644
index 0000000000..eb21fc647b
--- /dev/null
+++ b/remote/test/puppeteer/tools/ensure-pinned-deps.ts
@@ -0,0 +1,52 @@
+/**
+ * @license
+ * Copyright 2021 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {readdirSync, readFileSync} from 'fs';
+import {join} from 'path';
+
+import {devDependencies} from '../package.json';
+
+const LOCAL_PACKAGE_NAMES: string[] = [];
+
+const packagesDir = join(__dirname, '..', 'packages');
+for (const packageName of readdirSync(packagesDir)) {
+ const {name} = JSON.parse(
+ readFileSync(join(packagesDir, packageName, 'package.json'), 'utf8')
+ );
+ LOCAL_PACKAGE_NAMES.push(name);
+}
+
+const allDeps = {...devDependencies};
+
+const invalidDeps = new Map<string, string>();
+
+for (const [depKey, depValue] of Object.entries(allDeps)) {
+ if (depValue.startsWith('file:')) {
+ continue;
+ }
+ if (LOCAL_PACKAGE_NAMES.includes(depKey)) {
+ continue;
+ }
+ if (/[0-9]/.test(depValue[0]!)) {
+ continue;
+ }
+
+ invalidDeps.set(depKey, depValue);
+}
+
+if (invalidDeps.size > 0) {
+ console.error('Found non-pinned dependencies in package.json:');
+ console.log(
+ [...invalidDeps.keys()]
+ .map(k => {
+ return ` ${k}`;
+ })
+ .join('\n')
+ );
+ process.exit(1);
+}
+
+process.exit(0);
diff --git a/remote/test/puppeteer/tools/eslint/package.json b/remote/test/puppeteer/tools/eslint/package.json
new file mode 100644
index 0000000000..190367ae43
--- /dev/null
+++ b/remote/test/puppeteer/tools/eslint/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@puppeteer/eslint",
+ "version": "0.1.0",
+ "private": true,
+ "type": "commonjs",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/puppeteer/puppeteer/tree/main/tools/eslint"
+ },
+ "scripts": {
+ "build": "wireit",
+ "prepare": "wireit"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b",
+ "clean": "if-file-deleted",
+ "files": [
+ "src/**"
+ ],
+ "output": [
+ "lib/**",
+ "tsconfig.tsbuildinfo"
+ ]
+ },
+ "prepare": {
+ "dependencies": [
+ "build"
+ ]
+ }
+ },
+ "author": "The Chromium Authors",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@prettier/sync": "0.5.0"
+ }
+}
diff --git a/remote/test/puppeteer/tools/eslint/src/check-license.ts b/remote/test/puppeteer/tools/eslint/src/check-license.ts
new file mode 100644
index 0000000000..7ae1a54384
--- /dev/null
+++ b/remote/test/puppeteer/tools/eslint/src/check-license.ts
@@ -0,0 +1,83 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {TSESTree} from '@typescript-eslint/utils';
+import {ESLintUtils} from '@typescript-eslint/utils';
+
+const createRule = ESLintUtils.RuleCreator(name => {
+ return `https://github.com/puppeteer/puppeteer/tree/main/tools/eslint/${name}.ts`;
+});
+
+const copyrightPattern = /Copyright ([0-9]{4}) Google Inc\./;
+
+// const currentYear = new Date().getFullYear;
+
+// const licenseHeader = `/**
+// * @license
+// * Copyright ${currentYear} Google Inc.
+// * SPDX-License-Identifier: Apache-2.0
+// */`;
+
+const enforceLicenseRule = createRule<[], 'licenseRule'>({
+ name: 'check-license',
+ meta: {
+ type: 'layout',
+ docs: {
+ description: 'Validate existence of license header',
+ requiresTypeChecking: false,
+ },
+ fixable: undefined, // TODO: change to 'code' once fixer works.
+ schema: [],
+ messages: {
+ licenseRule: 'Add license header.',
+ },
+ },
+ defaultOptions: [],
+ create(context) {
+ const sourceCode = context.sourceCode;
+ const comments = sourceCode.getAllComments();
+ const header =
+ comments[0]?.type === 'Block' && isHeaderComment(comments[0])
+ ? comments[0]
+ : null;
+
+ function isHeaderComment(comment: TSESTree.Comment) {
+ if (comment && comment.range[0] >= 0 && comment.range[1] <= 88) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ return {
+ Program(node) {
+ if (
+ header &&
+ header.value.includes('@license') &&
+ header.value.includes('SPDX-License-Identifier: Apache-2.0') &&
+ copyrightPattern.test(header.value)
+ ) {
+ return;
+ }
+
+ // Add header license
+ if (!header || !header.value.includes('@license')) {
+ // const startLoc: [number, number] = [0, 88];
+ context.report({
+ node: node,
+ messageId: 'licenseRule',
+ // TODO: fix the fixer.
+ // fix(fixer) {
+ // return fixer.insertTextBeforeRange(startLoc, licenseHeader);
+ // },
+ });
+ }
+ },
+ };
+ },
+});
+
+export = enforceLicenseRule;
diff --git a/remote/test/puppeteer/tools/eslint/src/extensions.ts b/remote/test/puppeteer/tools/eslint/src/extensions.ts
new file mode 100644
index 0000000000..89b9279625
--- /dev/null
+++ b/remote/test/puppeteer/tools/eslint/src/extensions.ts
@@ -0,0 +1,48 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ESLintUtils} from '@typescript-eslint/utils';
+
+const createRule = ESLintUtils.RuleCreator(name => {
+ return `https://github.com/puppeteer/puppeteer/tree/main/tools/eslint/${name}.js`;
+});
+
+const enforceExtensionRule = createRule<[], 'extensionsRule'>({
+ name: 'extensions',
+ meta: {
+ docs: {
+ description: 'Requires `.js` for imports',
+ requiresTypeChecking: false,
+ },
+ messages: {
+ extensionsRule: 'Add `.js` to import.',
+ },
+ schema: [],
+ fixable: 'code',
+ type: 'problem',
+ },
+ defaultOptions: [],
+ create(context) {
+ return {
+ ImportDeclaration(node): void {
+ const file = node.source.value.split('/').pop();
+
+ if (!node.source.value.startsWith('.') || file?.includes('.')) {
+ return;
+ }
+ context.report({
+ node: node.source,
+ messageId: 'extensionsRule',
+ fix(fixer) {
+ return fixer.replaceText(node.source, `'${node.source.value}.js'`);
+ },
+ });
+ },
+ };
+ },
+});
+
+export = enforceExtensionRule;
diff --git a/remote/test/puppeteer/tools/eslint/src/prettier-comments.js b/remote/test/puppeteer/tools/eslint/src/prettier-comments.js
new file mode 100644
index 0000000000..3cbaad2909
--- /dev/null
+++ b/remote/test/puppeteer/tools/eslint/src/prettier-comments.js
@@ -0,0 +1,99 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// @ts-nocheck
+// TODO: We should convert this to types.
+
+const prettier = require('@prettier/sync');
+
+const prettierConfigPath = '../../../.prettierrc.cjs';
+const prettierConfig = require(prettierConfigPath);
+
+const cleanupBlockComment = value => {
+ return value
+ .trim()
+ .split('\n')
+ .map(value => {
+ value = value.trim();
+ if (value.startsWith('*')) {
+ value = value.slice(1);
+ if (value.startsWith(' ')) {
+ value = value.slice(1);
+ }
+ }
+ return value.trimEnd();
+ })
+ .join('\n')
+ .trim();
+};
+
+const format = (value, offset) => {
+ return prettier
+ .format(value, {
+ ...prettierConfig,
+ parser: 'markdown',
+ // This is the print width minus 3 (the length of ` * `) and the offset.
+ printWidth: 80 - (offset + 3),
+ })
+ .trim();
+};
+
+const buildBlockComment = (value, offset) => {
+ const spaces = ' '.repeat(offset);
+ const lines = value.split('\n').map(line => {
+ return ` * ${line}`;
+ });
+ lines.unshift('/**');
+ lines.push(' */');
+ lines.forEach((line, i) => {
+ lines[i] = `${spaces}${line}`;
+ });
+ return lines.join('\n');
+};
+
+/**
+ * @type import("eslint").Rule.RuleModule
+ */
+const prettierCommentsRule = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'Enforce Prettier formatting on comments',
+ recommended: false,
+ },
+ fixable: 'code',
+ schema: [],
+ messages: {},
+ },
+
+ create(context) {
+ for (const comment of context.sourceCode.getAllComments()) {
+ switch (comment.type) {
+ case 'Block': {
+ const offset = comment.loc.start.column;
+ const value = cleanupBlockComment(comment.value);
+ const formattedValue = format(value, offset);
+ if (formattedValue !== value) {
+ context.report({
+ node: comment,
+ message: `Comment is not formatted correctly.`,
+ fix(fixer) {
+ return fixer.replaceText(
+ comment,
+ buildBlockComment(formattedValue, offset).trimStart()
+ );
+ },
+ });
+ }
+ break;
+ }
+ }
+ }
+ return {};
+ },
+};
+
+module.exports = prettierCommentsRule;
diff --git a/remote/test/puppeteer/tools/eslint/src/use-using.ts b/remote/test/puppeteer/tools/eslint/src/use-using.ts
new file mode 100644
index 0000000000..0c727a4334
--- /dev/null
+++ b/remote/test/puppeteer/tools/eslint/src/use-using.ts
@@ -0,0 +1,85 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ESLintUtils, TSESTree} from '@typescript-eslint/utils';
+
+const usingSymbols = ['ElementHandle', 'JSHandle'];
+
+const createRule = ESLintUtils.RuleCreator(name => {
+ return `https://github.com/puppeteer/puppeteer/tree/main/tools/eslint/${name}.js`;
+});
+
+const useUsingRule = createRule<[], 'useUsing' | 'useUsingFix'>({
+ name: 'use-using',
+ meta: {
+ docs: {
+ description: "Requires 'using' for element/JS handles.",
+ requiresTypeChecking: true,
+ },
+ hasSuggestions: true,
+ messages: {
+ useUsing: "Use 'using'.",
+ useUsingFix: "Replace with 'using' to ignore.",
+ },
+ schema: [],
+ type: 'problem',
+ },
+ defaultOptions: [],
+ create(context) {
+ const services = ESLintUtils.getParserServices(context);
+ const checker = services.program.getTypeChecker();
+
+ return {
+ VariableDeclaration(node): void {
+ if (['using', 'await using'].includes(node.kind) || node.declare) {
+ return;
+ }
+ for (const declaration of node.declarations) {
+ if (declaration.id.type === TSESTree.AST_NODE_TYPES.Identifier) {
+ const tsNode = services.esTreeNodeToTSNodeMap.get(declaration.id);
+ const type = checker.getTypeAtLocation(tsNode);
+ let isElementHandleReference = false;
+ if (type.isUnionOrIntersection()) {
+ for (const member of type.types) {
+ if (
+ member.symbol !== undefined &&
+ usingSymbols.includes(member.symbol.escapedName as string)
+ ) {
+ isElementHandleReference = true;
+ break;
+ }
+ }
+ } else {
+ isElementHandleReference =
+ type.symbol !== undefined
+ ? usingSymbols.includes(type.symbol.escapedName as string)
+ : false;
+ }
+ if (isElementHandleReference) {
+ context.report({
+ node: declaration.id,
+ messageId: 'useUsing',
+ suggest: [
+ {
+ messageId: 'useUsingFix',
+ fix(fixer) {
+ return fixer.replaceTextRange(
+ [node.range[0], node.range[0] + node.kind.length],
+ 'using'
+ );
+ },
+ },
+ ],
+ });
+ }
+ }
+ }
+ },
+ };
+ },
+});
+
+export = useUsingRule;
diff --git a/remote/test/puppeteer/tools/eslint/tsconfig.json b/remote/test/puppeteer/tools/eslint/tsconfig.json
new file mode 100644
index 0000000000..da26cc936b
--- /dev/null
+++ b/remote/test/puppeteer/tools/eslint/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "rootDir": "./src",
+ "outDir": "./lib",
+ "declaration": false,
+ "declarationMap": false,
+ "sourceMap": false,
+ "composite": false,
+ "removeComments": true,
+ },
+}
diff --git a/remote/test/puppeteer/tools/eslint/tsdoc.json b/remote/test/puppeteer/tools/eslint/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/tools/eslint/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/tools/generate_module_package_json.ts b/remote/test/puppeteer/tools/generate_module_package_json.ts
new file mode 100644
index 0000000000..f13672e9d3
--- /dev/null
+++ b/remote/test/puppeteer/tools/generate_module_package_json.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {mkdirSync, writeFileSync} from 'fs';
+import {dirname} from 'path';
+
+/**
+ * Outputs the dummy package.json file to the path specified
+ * by the first argument.
+ */
+mkdirSync(dirname(process.argv[2]), {recursive: true});
+writeFileSync(process.argv[2], `{"type": "module"}`);
diff --git a/remote/test/puppeteer/tools/get_deprecated_version_range.js b/remote/test/puppeteer/tools/get_deprecated_version_range.js
new file mode 100644
index 0000000000..bac40e3677
--- /dev/null
+++ b/remote/test/puppeteer/tools/get_deprecated_version_range.js
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const {
+ versionsPerRelease,
+ lastMaintainedChromeVersion,
+} = require('../versions.js');
+
+const version = versionsPerRelease.get(lastMaintainedChromeVersion);
+if (version.toLowerCase() === 'next') {
+ console.error('Unexpected NEXT Puppeteer version in versions.js');
+ process.exit(1);
+}
+console.log(`< ${version.substring(1)}`);
+process.exit(0);
diff --git a/remote/test/puppeteer/tools/mocha-runner/README.md b/remote/test/puppeteer/tools/mocha-runner/README.md
new file mode 100644
index 0000000000..0bdd9f253b
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/README.md
@@ -0,0 +1,103 @@
+# Mocha Runner
+
+Mocha Runner is a test runner on top of mocha.
+It uses `/test/TestSuites.json` and `/test/TestExpectations.json` files to run mocha tests in multiple configurations and interpret results.
+
+## Running tests for Mocha Runner itself.
+
+```bash
+npm test
+```
+
+## Running tests using Mocha Runner
+
+```bash
+npm run build && npm run test
+```
+
+By default, the runner runs all test suites applicable to the current platform.
+To pick a test suite, provide the `--test-suite` arguments. For example,
+
+```bash
+npm run build && npm run test -- --test-suite chrome-headless
+```
+
+## TestSuites.json
+
+Define test suites via the `testSuites` attribute. `parameters` can be used in the `TestExpectations.json` to disable tests
+based on parameters. The meaning for parameters is defined in `parameterDefinitions` which tell what env object corresponds
+to the given parameter.
+
+## TestExpectations.json
+
+An expectation looks like this:
+
+```json
+{
+ "testIdPattern": "[accessibility.spec]",
+ "platforms": ["darwin", "win32", "linux"],
+ "parameters": ["firefox"],
+ "expectations": ["SKIP"]
+}
+```
+
+| Field | Description | Type | Match Logic |
+| --------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------- |
+| `testIdPattern` | Defines the full name (or pattern) to match against test name | string | - |
+| `platforms` | Defines the platforms the expectation is for | Array<`linux` \| `win32` \|`darwin`> | `OR` |
+| `parameters` | Defines the parameters that the test has to match | Array<[ParameterDefinitions](https://github.com/puppeteer/puppeteer/blob/main/test/TestSuites.json)> | `AND` |
+| `expectations` | The list of test results that are considered to be acceptable | Array<`PASS` \| `FAIL` \| `TIMEOUT` \| `SKIP`> | `OR` |
+
+> Order of defining expectations matters. The latest expectation that is set will take president over earlier ones.
+
+> Adding `SKIP` to `expectations` will prevent the test from running, no matter if there are other expectations.
+
+### Using pattern in `testIdPattern`
+
+Sometimes we want a whole group of test to run. For that we can use a
+pattern to achieve.
+Pattern are defined with the use of `*` (using greedy method).
+
+Examples:
+| Pattern | Description | Example Pattern | Example match |
+|------------------------|---------------------------------------------------------------------------------------------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------|
+| `*` | Match all tests | - | - |
+| `[test.spec] *` | Matches tests for the given file | `[jshandle.spec] *` | `[jshandle] JSHandle JSHandle.toString should work for primitives` |
+| `[test.spec] <text> *` | Matches tests with for a given test with a specific prefixed test (usually a describe node) | `[page.spec] Page Page.goto *` | `[page.spec] Page Page.goto should work`,<br>`[page.spec] Page Page.goto should work with anchor navigation` |
+| `[test.spec] * <text>` | Matches test with a surfix | `[navigation.spec] * should work` | `[navigation.spec] navigation Page.goto should work`,<br>`[navigation.spec] navigation Page.waitForNavigation should work` |
+
+## Updating Expectations
+
+Currently, expectations are updated manually. The test runner outputs the
+suggested changes to the expectation file if the test run does not match
+expectations.
+
+## Debugging flaky test
+
+### Utility functions:
+
+| Utility | Params | Description |
+| ------------------------ | ------------------------------- | --------------------------------------------------------------------------------- |
+| `describe.withDebugLogs` | `(title, <DescribeBody>)` | Capture and print debug logs for each test that failed |
+| `it.deflake` | `(repeat, title, <itFunction>)` | Reruns the test N number of times and print the debug logs if for the failed runs |
+| `it.deflakeOnly` | `(repeat, title, <itFunction>)` | Same as `it.deflake` but runs only this specific test |
+
+### With Environment variable
+
+Run the test with the following environment variable to wrap it around `describe.withDebugLogs`. Example:
+
+```bash
+PUPPETEER_DEFLAKE_TESTS="[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0" npm run test:chrome:headless
+```
+
+It also works with [patterns](#1--this-is-my-header) just like `TestExpectations.json`
+
+```bash
+PUPPETEER_DEFLAKE_TESTS="[navigation.spec] *" npm run test:chrome:headless
+```
+
+By default the test is rerun 100 times, but you can control this as well:
+
+```bash
+PUPPETEER_DEFLAKE_RETRIES=1000 PUPPETEER_DEFLAKE_TESTS="[navigation.spec] *" npm run test:chrome:headless
+```
diff --git a/remote/test/puppeteer/tools/mocha-runner/package.json b/remote/test/puppeteer/tools/mocha-runner/package.json
new file mode 100644
index 0000000000..26612e504a
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@puppeteer/mocha-runner",
+ "version": "0.1.0",
+ "type": "commonjs",
+ "private": true,
+ "bin": "./bin/mocha-runner.js",
+ "description": "Mocha runner for Puppeteer",
+ "license": "Apache-2.0",
+ "scripts": {
+ "build": "wireit",
+ "test": "wireit",
+ "clean": "../clean.js"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc -b && chmod +x ./bin/mocha-runner.js",
+ "clean": "if-file-deleted",
+ "files": [
+ "src/**"
+ ],
+ "output": [
+ "bin/**",
+ "tsconfig.tsbuildinfo"
+ ],
+ "dependencies": [
+ "../../packages/puppeteer-core:build"
+ ]
+ },
+ "test": {
+ "command": "c8 node ./bin/test.js",
+ "dependencies": [
+ "build"
+ ]
+ }
+ },
+ "devDependencies": {
+ "@types/yargs": "17.0.32",
+ "c8": "9.1.0",
+ "glob": "10.3.10",
+ "yargs": "17.7.2",
+ "zod": "3.22.4"
+ }
+}
diff --git a/remote/test/puppeteer/tools/mocha-runner/src/interface.ts b/remote/test/puppeteer/tools/mocha-runner/src/interface.ts
new file mode 100644
index 0000000000..fe0f7e18b5
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/src/interface.ts
@@ -0,0 +1,191 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import Mocha from 'mocha';
+import commonInterface from 'mocha/lib/interfaces/common';
+import {
+ setLogCapture,
+ getCapturedLogs,
+} from 'puppeteer-core/internal/common/Debug.js';
+
+import {testIdMatchesExpectationPattern} from './utils.js';
+
+type SuiteFunction = ((this: Mocha.Suite) => void) | undefined;
+type ExclusiveSuiteFunction = (this: Mocha.Suite) => void;
+
+const skippedTests: Array<{testIdPattern: string; skip: true}> = process.env[
+ 'PUPPETEER_SKIPPED_TEST_CONFIG'
+]
+ ? JSON.parse(process.env['PUPPETEER_SKIPPED_TEST_CONFIG'])
+ : [];
+
+const deflakeRetries = Number(
+ process.env['PUPPETEER_DEFLAKE_RETRIES']
+ ? process.env['PUPPETEER_DEFLAKE_RETRIES']
+ : 100
+);
+const deflakeTestPattern: string | undefined =
+ process.env['PUPPETEER_DEFLAKE_TESTS'];
+
+function shouldSkipTest(test: Mocha.Test): boolean {
+ // TODO: more efficient lookup.
+ const definition = skippedTests.find(skippedTest => {
+ return testIdMatchesExpectationPattern(test, skippedTest.testIdPattern);
+ });
+ if (definition && definition.skip) {
+ return true;
+ }
+ return false;
+}
+
+function shouldDeflakeTest(test: Mocha.Test): boolean {
+ if (deflakeTestPattern) {
+ // TODO: cache if we have seen it already
+ return testIdMatchesExpectationPattern(test, deflakeTestPattern);
+ }
+ return false;
+}
+
+function dumpLogsIfFail(this: Mocha.Context) {
+ if (this.currentTest?.state === 'failed') {
+ console.log(
+ `\n"${this.currentTest.fullTitle()}" failed. Here is a debug log:`
+ );
+ console.log(getCapturedLogs().join('\n') + '\n');
+ }
+ setLogCapture(false);
+}
+
+function customBDDInterface(suite: Mocha.Suite) {
+ const suites: [Mocha.Suite] = [suite];
+
+ suite.on(
+ Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE,
+ function (context, file, mocha) {
+ const common = commonInterface(suites, context, mocha);
+
+ context['before'] = common.before;
+ context['after'] = common.after;
+ context['beforeEach'] = common.beforeEach;
+ context['afterEach'] = common.afterEach;
+ if (mocha.options.delay) {
+ context['run'] = common.runWithSuite(suite);
+ }
+ function describe(title: string, fn: SuiteFunction) {
+ return common.suite.create({
+ title: title,
+ file: file,
+ fn: fn,
+ });
+ }
+ describe.only = function (title: string, fn: ExclusiveSuiteFunction) {
+ return common.suite.only({
+ title: title,
+ file: file,
+ fn: fn,
+ isOnly: true,
+ });
+ };
+
+ describe.skip = function (title: string, fn: SuiteFunction) {
+ return common.suite.skip({
+ title: title,
+ file: file,
+ fn: fn,
+ });
+ };
+
+ describe.withDebugLogs = function (
+ description: string,
+ body: (this: Mocha.Suite) => void
+ ): void {
+ context['describe']('with Debug Logs', () => {
+ context['beforeEach'](() => {
+ setLogCapture(true);
+ });
+ context['afterEach'](dumpLogsIfFail);
+ context['describe'](description, body);
+ });
+ };
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ context['describe'] = describe;
+
+ function it(title: string, fn: Mocha.TestFunction, itOnly = false) {
+ const suite = suites[0]! as Mocha.Suite;
+ const test = new Mocha.Test(title, suite.isPending() ? undefined : fn);
+ test.file = file;
+ test.parent = suite;
+
+ const describeOnly = Boolean(
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ suite.parent?._onlySuites.find(child => {
+ return child === suite;
+ })
+ );
+ if (shouldDeflakeTest(test)) {
+ const deflakeSuit = Mocha.Suite.create(suite, 'with Debug Logs');
+ test.file = file;
+ deflakeSuit.beforeEach(function () {
+ setLogCapture(true);
+ });
+ deflakeSuit.afterEach(dumpLogsIfFail);
+ for (let i = 0; i < deflakeRetries; i++) {
+ deflakeSuit.addTest(test.clone());
+ }
+ return test;
+ } else if (!(itOnly || describeOnly) && shouldSkipTest(test)) {
+ const test = new Mocha.Test(title);
+ test.file = file;
+ suite.addTest(test);
+ return test;
+ } else {
+ suite.addTest(test);
+ return test;
+ }
+ }
+
+ it.only = function (title: string, fn: Mocha.TestFunction) {
+ return common.test.only(
+ mocha,
+ (context['it'] as unknown as typeof it)(title, fn, true)
+ );
+ };
+
+ it.skip = function (title: string) {
+ return context['it'](title);
+ };
+
+ function wrapDeflake(
+ func: Function
+ ): (repeats: number, title: string, fn: Mocha.AsyncFunc) => void {
+ return (repeats: number, title: string, fn: Mocha.AsyncFunc): void => {
+ (context['describe'] as unknown as typeof describe).withDebugLogs(
+ 'with Debug Logs',
+ () => {
+ for (let i = 1; i <= repeats; i++) {
+ func(`${i}/${title}`, fn);
+ }
+ }
+ );
+ };
+ }
+
+ it.deflake = wrapDeflake(it);
+ it.deflakeOnly = wrapDeflake(it.only);
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ context.it = it;
+ }
+ );
+}
+
+customBDDInterface.description = 'Custom BDD';
+
+module.exports = customBDDInterface;
diff --git a/remote/test/puppeteer/tools/mocha-runner/src/mocha-runner.ts b/remote/test/puppeteer/tools/mocha-runner/src/mocha-runner.ts
new file mode 100644
index 0000000000..1707e4cc41
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/src/mocha-runner.ts
@@ -0,0 +1,330 @@
+#! /usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {randomUUID} from 'crypto';
+import fs from 'fs';
+import {spawn} from 'node:child_process';
+import os from 'os';
+import path from 'path';
+
+import {globSync} from 'glob';
+import yargs from 'yargs';
+import {hideBin} from 'yargs/helpers';
+
+import {
+ zPlatform,
+ zTestSuiteFile,
+ type MochaResults,
+ type Platform,
+ type TestExpectation,
+ type TestSuite,
+ type TestSuiteFile,
+} from './types.js';
+import {
+ extendProcessEnv,
+ filterByParameters,
+ filterByPlatform,
+ getExpectationUpdates,
+ printSuggestions,
+ readJSON,
+ writeJSON,
+ type RecommendedExpectation,
+} from './utils.js';
+
+const {
+ _: mochaArgs,
+ testSuite: testSuiteId,
+ saveStatsTo,
+ cdpTests: includeCdpTests,
+ suggestions: provideSuggestions,
+ coverage: useCoverage,
+ minTests,
+ shard,
+ reporter,
+ printMemory,
+} = yargs(hideBin(process.argv))
+ .parserConfiguration({'unknown-options-as-args': true})
+ .scriptName('@puppeteer/mocha-runner')
+ .option('coverage', {
+ boolean: true,
+ default: true,
+ })
+ .option('suggestions', {
+ boolean: true,
+ default: true,
+ })
+ .option('cdp-tests', {
+ boolean: true,
+ default: true,
+ })
+ .option('save-stats-to', {
+ string: true,
+ requiresArg: true,
+ })
+ .option('min-tests', {
+ number: true,
+ default: 0,
+ requiresArg: true,
+ })
+ .option('test-suite', {
+ string: true,
+ requiresArg: true,
+ })
+ .option('shard', {
+ string: true,
+ requiresArg: true,
+ })
+ .option('reporter', {
+ string: true,
+ requiresArg: true,
+ })
+ .option('print-memory', {
+ boolean: true,
+ default: false,
+ })
+ .parseSync();
+
+function getApplicableTestSuites(
+ parsedSuitesFile: TestSuiteFile,
+ platform: Platform
+): TestSuite[] {
+ let applicableSuites: TestSuite[] = [];
+
+ if (!testSuiteId) {
+ applicableSuites = filterByPlatform(parsedSuitesFile.testSuites, platform);
+ } else {
+ const testSuite = parsedSuitesFile.testSuites.find(suite => {
+ return suite.id === testSuiteId;
+ });
+
+ if (!testSuite) {
+ console.error(`Test suite ${testSuiteId} is not defined`);
+ process.exit(1);
+ }
+
+ if (!testSuite.platforms.includes(platform)) {
+ console.warn(
+ `Test suite ${testSuiteId} is not enabled for your platform. Running it anyway.`
+ );
+ }
+
+ applicableSuites = [testSuite];
+ }
+
+ return applicableSuites;
+}
+
+async function main() {
+ let statsPath = saveStatsTo;
+ if (statsPath && statsPath.includes('INSERTID')) {
+ statsPath = statsPath.replace(/INSERTID/gi, randomUUID());
+ }
+
+ const platform = zPlatform.parse(os.platform());
+
+ const expectations = readJSON(
+ path.join(process.cwd(), 'test', 'TestExpectations.json')
+ ) as TestExpectation[];
+
+ const parsedSuitesFile = zTestSuiteFile.parse(
+ readJSON(path.join(process.cwd(), 'test', 'TestSuites.json'))
+ );
+
+ const applicableSuites = getApplicableTestSuites(parsedSuitesFile, platform);
+
+ console.log('Planning to run the following test suites', applicableSuites);
+ if (statsPath) {
+ console.log('Test stats will be saved to', statsPath);
+ }
+
+ let fail = false;
+ const recommendations: RecommendedExpectation[] = [];
+ try {
+ for (const suite of applicableSuites) {
+ const parameters = suite.parameters;
+
+ const applicableExpectations = filterByParameters(
+ filterByPlatform(expectations, platform),
+ parameters
+ ).reverse();
+
+ // Add more logging when the GitHub Action Debugging option is set
+ // https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
+ const githubActionDebugging = process.env['RUNNER_DEBUG']
+ ? {
+ DEBUG: 'puppeteer:*',
+ EXTRA_LAUNCH_OPTIONS: JSON.stringify({
+ dumpio: true,
+ extraPrefsFirefox: {
+ 'remote.log.level': 'Trace',
+ },
+ }),
+ }
+ : {};
+
+ const env = extendProcessEnv([
+ ...parameters.map(param => {
+ return parsedSuitesFile.parameterDefinitions[param];
+ }),
+ {
+ PUPPETEER_SKIPPED_TEST_CONFIG: JSON.stringify(
+ applicableExpectations.map(ex => {
+ return {
+ testIdPattern: ex.testIdPattern,
+ skip: ex.expectations.includes('SKIP'),
+ };
+ })
+ ),
+ },
+ githubActionDebugging,
+ ]);
+
+ const tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'puppeteer-test-runner-')
+ );
+ const tmpFilename = statsPath
+ ? statsPath
+ : path.join(tmpDir, 'output.json');
+ console.log('Running', JSON.stringify(parameters), tmpFilename);
+ const args = [
+ '-u',
+ path.join(__dirname, 'interface.js'),
+ '-R',
+ !reporter ? path.join(__dirname, 'reporter.js') : reporter,
+ '-O',
+ `output=${tmpFilename}`,
+ '-n',
+ 'trace-warnings',
+ ];
+
+ if (printMemory) {
+ args.push('-n', 'expose-gc');
+ }
+
+ const specPattern = 'test/build/**/*.spec.js';
+ const specs = globSync(specPattern, {
+ ignore: !includeCdpTests ? 'test/build/cdp/**/*.spec.js' : undefined,
+ }).sort((a, b) => {
+ return a.localeCompare(b);
+ });
+ if (shard) {
+ // Shard ID is 1-based.
+ const [shardId, shards] = shard.split('-').map(s => {
+ return Number(s);
+ }) as [number, number];
+ const argsLength = args.length;
+ for (let i = 0; i < specs.length; i++) {
+ if (i % shards === shardId - 1) {
+ args.push(specs[i]!);
+ }
+ }
+ if (argsLength === args.length) {
+ throw new Error('Shard did not result in any test files');
+ }
+ console.log(
+ `Running shard ${shardId}-${shards}. Picked ${
+ args.length - argsLength
+ } files out of ${specs.length}.`
+ );
+ } else {
+ args.push(...specs);
+ }
+ const handle = spawn(
+ 'npx',
+ [
+ ...(useCoverage
+ ? [
+ 'c8',
+ '--check-coverage',
+ '--lines',
+ String(suite.expectedLineCoverage),
+ 'npx',
+ ]
+ : []),
+ 'mocha',
+ ...mochaArgs.map(String),
+ ...args,
+ ],
+ {
+ shell: true,
+ cwd: process.cwd(),
+ stdio: 'inherit',
+ env,
+ }
+ );
+ await new Promise<void>((resolve, reject) => {
+ handle.on('error', err => {
+ reject(err);
+ });
+ handle.on('close', () => {
+ resolve();
+ });
+ });
+ console.log('Finished', JSON.stringify(parameters));
+ try {
+ const results = readJSON(tmpFilename) as MochaResults;
+ const updates = getExpectationUpdates(results, applicableExpectations, {
+ platforms: [os.platform()],
+ parameters,
+ });
+ const totalTests = results.stats.tests;
+ results.parameters = parameters;
+ results.platform = platform;
+ results.date = new Date().toISOString();
+ if (updates.length > 0) {
+ fail = true;
+ recommendations.push(...updates);
+ results.updates = updates;
+ writeJSON(tmpFilename, results);
+ } else {
+ if (!shard && totalTests < minTests) {
+ fail = true;
+ console.log(
+ `Test run matches expectations but the number of discovered tests is too low (expected: ${minTests}, actual: ${totalTests}).`
+ );
+ writeJSON(tmpFilename, results);
+ continue;
+ }
+ console.log('Test run matches expectations');
+ writeJSON(tmpFilename, results);
+ continue;
+ }
+ } catch (err) {
+ fail = true;
+ console.error(err);
+ }
+ }
+ } catch (err) {
+ fail = true;
+ console.error(err);
+ } finally {
+ if (!!provideSuggestions) {
+ printSuggestions(
+ recommendations,
+ 'add',
+ 'Add the following to TestExpectations.json to ignore the error:'
+ );
+ printSuggestions(
+ recommendations,
+ 'remove',
+ 'Remove the following from the TestExpectations.json to ignore the error:'
+ );
+ printSuggestions(
+ recommendations,
+ 'update',
+ 'Update the following expectations in the TestExpectations.json to ignore the error:'
+ );
+ }
+ process.exit(fail ? 1 : 0);
+ }
+}
+
+main().catch(error => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/remote/test/puppeteer/tools/mocha-runner/src/reporter.ts b/remote/test/puppeteer/tools/mocha-runner/src/reporter.ts
new file mode 100644
index 0000000000..7acd5319fe
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/src/reporter.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import Mocha from 'mocha';
+
+class SpecJSONReporter extends Mocha.reporters.Spec {
+ constructor(runner: Mocha.Runner, options?: Mocha.MochaOptions) {
+ super(runner, options);
+ Mocha.reporters.JSON.call(this, runner, options);
+ }
+}
+
+module.exports = SpecJSONReporter;
diff --git a/remote/test/puppeteer/tools/mocha-runner/src/test.ts b/remote/test/puppeteer/tools/mocha-runner/src/test.ts
new file mode 100644
index 0000000000..5510966235
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/src/test.ts
@@ -0,0 +1,212 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'node:assert/strict';
+import {describe, it} from 'node:test';
+
+import type {Platform, TestExpectation, MochaTestResult} from './types.js';
+import {
+ filterByParameters,
+ getTestResultForFailure,
+ isWildCardPattern,
+ testIdMatchesExpectationPattern,
+ getExpectationUpdates,
+} from './utils.js';
+import {getFilename, extendProcessEnv} from './utils.js';
+
+describe('extendProcessEnv', () => {
+ it('should extend env variables for the subprocess', () => {
+ const env = extendProcessEnv([{TEST: 'TEST'}, {TEST2: 'TEST2'}]);
+ assert.equal(env['TEST'], 'TEST');
+ assert.equal(env['TEST2'], 'TEST2');
+ });
+});
+
+describe('getFilename', () => {
+ it('extract filename for a path', () => {
+ assert.equal(getFilename('/etc/test.ts'), 'test');
+ assert.equal(getFilename('/etc/test.js'), 'test');
+ });
+});
+
+describe('getTestResultForFailure', () => {
+ it('should get a test result for a mocha failure', () => {
+ assert.equal(
+ getTestResultForFailure({err: {code: 'ERR_MOCHA_TIMEOUT'}}),
+ 'TIMEOUT'
+ );
+ assert.equal(getTestResultForFailure({err: {code: 'ERROR'}}), 'FAIL');
+ });
+});
+
+describe('filterByParameters', () => {
+ it('should filter a list of expectations by parameters', () => {
+ const expectations: TestExpectation[] = [
+ {
+ testIdPattern:
+ '[oopif.spec] OOPIF "after all" hook for "should keep track of a frames OOP state"',
+ platforms: ['darwin'],
+ parameters: ['firefox', 'headless'],
+ expectations: ['FAIL'],
+ },
+ ];
+ assert.equal(
+ filterByParameters(expectations, ['firefox', 'headless']).length,
+ 1
+ );
+ assert.equal(filterByParameters(expectations, ['firefox']).length, 0);
+ assert.equal(
+ filterByParameters(expectations, ['firefox', 'headless', 'other']).length,
+ 1
+ );
+ assert.equal(filterByParameters(expectations, ['other']).length, 0);
+ });
+});
+
+describe('isWildCardPattern', () => {
+ it('should detect if an expectation is a wildcard pattern', () => {
+ assert.equal(isWildCardPattern(''), false);
+ assert.equal(isWildCardPattern('a'), false);
+ assert.equal(isWildCardPattern('*'), true);
+
+ assert.equal(isWildCardPattern('[queryHandler.spec]'), false);
+ assert.equal(isWildCardPattern('[queryHandler.spec] *'), true);
+ assert.equal(isWildCardPattern(' [queryHandler.spec] '), false);
+
+ assert.equal(isWildCardPattern('[queryHandler.spec] Query'), false);
+ assert.equal(isWildCardPattern('[queryHandler.spec] Page *'), true);
+ assert.equal(
+ isWildCardPattern('[queryHandler.spec] Page Page.goto *'),
+ true
+ );
+ });
+});
+
+describe('testIdMatchesExpectationPattern', () => {
+ const expectations: Array<[string, boolean]> = [
+ ['', false],
+ ['*', true],
+ ['* should work', true],
+ ['* Page.setContent *', true],
+ ['* should work as expected', false],
+ ['Page.setContent *', false],
+ ['[page.spec]', false],
+ ['[page.spec] *', true],
+ ['[page.spec] Page *', true],
+ ['[page.spec] Page Page.setContent *', true],
+ ['[page.spec] Page Page.setContent should work', true],
+ ['[page.spec] Page * should work', true],
+ ['[page.spec] * Page.setContent *', true],
+ ['[jshandle.spec] *', false],
+ ['[jshandle.spec] JSHandle should work', false],
+ ];
+
+ it('with MochaTest', () => {
+ const test = {
+ title: 'should work',
+ file: 'page.spec.ts',
+ fullTitle() {
+ return 'Page Page.setContent should work';
+ },
+ };
+
+ for (const [pattern, expected] of expectations) {
+ assert.equal(
+ testIdMatchesExpectationPattern(test, pattern),
+ expected,
+ `Expected "${pattern}" to yield "${expected}"`
+ );
+ }
+ });
+
+ it('with MochaTestResult', () => {
+ const test: MochaTestResult = {
+ title: 'should work',
+ file: 'page.spec.ts',
+ fullTitle: 'Page Page.setContent should work',
+ };
+
+ for (const [pattern, expected] of expectations) {
+ assert.equal(
+ testIdMatchesExpectationPattern(test, pattern),
+ expected,
+ `Expected "${pattern}" to yield "${expected}"`
+ );
+ }
+ });
+});
+
+describe('getExpectationUpdates', () => {
+ it('should generate an update for expectations if a test passed with a fail expectation', () => {
+ const mochaResults = {
+ stats: {tests: 1},
+ pending: [],
+ passes: [
+ {
+ fullTitle: 'Page Page.setContent should work',
+ title: 'should work',
+ file: 'page.spec.ts',
+ },
+ ],
+ failures: [],
+ };
+ const expectations = [
+ {
+ testIdPattern: '[page.spec] Page Page.setContent should work',
+ platforms: ['darwin'] as Platform[],
+ parameters: ['test'],
+ expectations: ['FAIL' as const],
+ },
+ ];
+ const updates = getExpectationUpdates(mochaResults, expectations, {
+ platforms: ['darwin'] as Platform[],
+ parameters: ['test'],
+ });
+ assert.deepEqual(updates, [
+ {
+ action: 'remove',
+ basedOn: {
+ expectations: ['FAIL'],
+ parameters: ['test'],
+ platforms: ['darwin'],
+ testIdPattern: '[page.spec] Page Page.setContent should work',
+ },
+ expectation: {
+ expectations: ['FAIL'],
+ parameters: ['test'],
+ platforms: ['darwin'],
+ testIdPattern: '[page.spec] Page Page.setContent should work',
+ },
+ },
+ ]);
+ });
+
+ it('should not generate an update for successful retries', () => {
+ const mochaResults = {
+ stats: {tests: 1},
+ pending: [],
+ passes: [
+ {
+ fullTitle: 'Page Page.setContent should work',
+ title: 'should work',
+ file: 'page.spec.ts',
+ },
+ ],
+ failures: [
+ {
+ fullTitle: 'Page Page.setContent should work',
+ title: 'should work',
+ file: 'page.spec.ts',
+ err: {code: 'Timeout'},
+ },
+ ],
+ };
+ const updates = getExpectationUpdates(mochaResults, [], {
+ platforms: ['darwin'],
+ parameters: ['test'],
+ });
+ assert.deepEqual(updates, []);
+ });
+});
diff --git a/remote/test/puppeteer/tools/mocha-runner/src/types.ts b/remote/test/puppeteer/tools/mocha-runner/src/types.ts
new file mode 100644
index 0000000000..01dc4d6be6
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/src/types.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {z} from 'zod';
+
+import type {RecommendedExpectation} from './utils.js';
+
+export const zPlatform = z.enum(['win32', 'linux', 'darwin']);
+
+export type Platform = z.infer<typeof zPlatform>;
+
+export const zTestSuite = z.object({
+ id: z.string(),
+ platforms: z.array(zPlatform),
+ parameters: z.array(z.string()),
+ expectedLineCoverage: z.number(),
+});
+
+export type TestSuite = z.infer<typeof zTestSuite>;
+
+export const zTestSuiteFile = z.object({
+ testSuites: z.array(zTestSuite),
+ parameterDefinitions: z.record(z.any()),
+});
+
+export type TestSuiteFile = z.infer<typeof zTestSuiteFile>;
+
+export type TestResult = 'PASS' | 'FAIL' | 'TIMEOUT' | 'SKIP';
+
+export interface TestExpectation {
+ testIdPattern: string;
+ platforms: NodeJS.Platform[];
+ parameters: string[];
+ expectations: TestResult[];
+}
+
+export interface MochaTestResult {
+ fullTitle: string;
+ title: string;
+ file: string;
+ err?: {code: string};
+}
+
+export interface MochaResults {
+ stats: {tests: number};
+ pending: MochaTestResult[];
+ passes: MochaTestResult[];
+ failures: MochaTestResult[];
+ // Added by mocha-runner.
+ updates?: RecommendedExpectation[];
+ parameters?: string[];
+ platform?: string;
+ date?: string;
+}
diff --git a/remote/test/puppeteer/tools/mocha-runner/src/utils.ts b/remote/test/puppeteer/tools/mocha-runner/src/utils.ts
new file mode 100644
index 0000000000..066c5fbe57
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/src/utils.ts
@@ -0,0 +1,291 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import type {
+ MochaTestResult,
+ TestExpectation,
+ MochaResults,
+ TestResult,
+} from './types.js';
+
+export function extendProcessEnv(envs: object[]): NodeJS.ProcessEnv {
+ const env = envs.reduce(
+ (acc: object, item: object) => {
+ Object.assign(acc, item);
+ return acc;
+ },
+ {
+ ...process.env,
+ }
+ );
+
+ if (process.env['CI']) {
+ const puppeteerEnv = Object.entries(env).reduce(
+ (acc, [key, value]) => {
+ if (key.startsWith('PUPPETEER_')) {
+ acc[key] = value;
+ }
+
+ return acc;
+ },
+ {} as Record<string, unknown>
+ );
+
+ console.log(
+ 'PUPPETEER env:\n',
+ JSON.stringify(puppeteerEnv, null, 2),
+ '\n'
+ );
+ }
+
+ return env as NodeJS.ProcessEnv;
+}
+
+export function getFilename(file: string): string {
+ return path.basename(file).replace(path.extname(file), '');
+}
+
+export function readJSON(path: string): unknown {
+ return JSON.parse(fs.readFileSync(path, 'utf-8'));
+}
+
+export function writeJSON(path: string, json: unknown): unknown {
+ return fs.writeFileSync(path, JSON.stringify(json, null, 2));
+}
+
+export function filterByPlatform<T extends {platforms: NodeJS.Platform[]}>(
+ items: T[],
+ platform: NodeJS.Platform
+): T[] {
+ return items.filter(item => {
+ return item.platforms.includes(platform);
+ });
+}
+
+export function prettyPrintJSON(json: unknown): void {
+ console.log(JSON.stringify(json, null, 2));
+}
+
+export function printSuggestions(
+ recommendations: RecommendedExpectation[],
+ action: RecommendedExpectation['action'],
+ message: string
+): void {
+ const toPrint = recommendations.filter(item => {
+ return item.action === action;
+ });
+ if (toPrint.length) {
+ console.log(message);
+ prettyPrintJSON(
+ toPrint.map(item => {
+ return item.expectation;
+ })
+ );
+ if (action !== 'remove') {
+ console.log(
+ 'The recommendations are based on the following applied expectations:'
+ );
+ prettyPrintJSON(
+ toPrint.map(item => {
+ return item.basedOn;
+ })
+ );
+ }
+ }
+}
+
+export function filterByParameters(
+ expectations: TestExpectation[],
+ parameters: string[]
+): TestExpectation[] {
+ const querySet = new Set(parameters);
+ return expectations.filter(ex => {
+ return ex.parameters.every(param => {
+ return querySet.has(param);
+ });
+ });
+}
+
+/**
+ * The last expectation that matches an empty string as all tests pattern
+ * or the name of the file or the whole name of the test the filter wins.
+ */
+export function findEffectiveExpectationForTest(
+ expectations: TestExpectation[],
+ result: MochaTestResult
+): TestExpectation | undefined {
+ return expectations.find(expectation => {
+ return testIdMatchesExpectationPattern(result, expectation.testIdPattern);
+ });
+}
+
+export interface RecommendedExpectation {
+ expectation: TestExpectation;
+ action: 'remove' | 'add' | 'update';
+ basedOn?: TestExpectation;
+}
+
+export function isWildCardPattern(testIdPattern: string): boolean {
+ return testIdPattern.includes('*');
+}
+
+export function getExpectationUpdates(
+ results: MochaResults,
+ expectations: TestExpectation[],
+ context: {
+ platforms: NodeJS.Platform[];
+ parameters: string[];
+ }
+): RecommendedExpectation[] {
+ const output = new Map<string, RecommendedExpectation>();
+
+ const passesByKey = results.passes.reduce((acc, pass) => {
+ acc.add(getTestId(pass.file, pass.fullTitle));
+ return acc;
+ }, new Set());
+
+ for (const pass of results.passes) {
+ const expectationEntry = findEffectiveExpectationForTest(
+ expectations,
+ pass
+ );
+ if (expectationEntry && !expectationEntry.expectations.includes('PASS')) {
+ if (isWildCardPattern(expectationEntry.testIdPattern)) {
+ addEntry({
+ expectation: {
+ testIdPattern: getTestId(pass.file, pass.fullTitle),
+ platforms: context.platforms,
+ parameters: context.parameters,
+ expectations: ['PASS'],
+ },
+ action: 'add',
+ basedOn: expectationEntry,
+ });
+ } else {
+ addEntry({
+ expectation: expectationEntry,
+ action: 'remove',
+ basedOn: expectationEntry,
+ });
+ }
+ }
+ }
+
+ for (const failure of results.failures) {
+ // If an error occurs during a hook
+ // the error not have a file associated with it
+ if (!failure.file) {
+ console.error('Hook failed:', failure.err);
+ addEntry({
+ expectation: {
+ testIdPattern: failure.fullTitle,
+ platforms: context.platforms,
+ parameters: context.parameters,
+ expectations: [],
+ },
+ action: 'add',
+ });
+ continue;
+ }
+
+ if (passesByKey.has(getTestId(failure.file, failure.fullTitle))) {
+ continue;
+ }
+
+ const expectationEntry = findEffectiveExpectationForTest(
+ expectations,
+ failure
+ );
+ if (expectationEntry && !expectationEntry.expectations.includes('SKIP')) {
+ if (
+ !expectationEntry.expectations.includes(
+ getTestResultForFailure(failure)
+ )
+ ) {
+ // If the effective explanation is a wildcard, we recommend adding a new
+ // expectation instead of updating the wildcard that might affect multiple
+ // tests.
+ if (isWildCardPattern(expectationEntry.testIdPattern)) {
+ addEntry({
+ expectation: {
+ testIdPattern: getTestId(failure.file, failure.fullTitle),
+ platforms: context.platforms,
+ parameters: context.parameters,
+ expectations: [getTestResultForFailure(failure)],
+ },
+ action: 'add',
+ basedOn: expectationEntry,
+ });
+ } else {
+ addEntry({
+ expectation: {
+ ...expectationEntry,
+ expectations: [
+ ...expectationEntry.expectations,
+ getTestResultForFailure(failure),
+ ],
+ },
+ action: 'update',
+ basedOn: expectationEntry,
+ });
+ }
+ }
+ } else if (!expectationEntry) {
+ addEntry({
+ expectation: {
+ testIdPattern: getTestId(failure.file, failure.fullTitle),
+ platforms: context.platforms,
+ parameters: context.parameters,
+ expectations: [getTestResultForFailure(failure)],
+ },
+ action: 'add',
+ });
+ }
+ }
+
+ function addEntry(value: RecommendedExpectation) {
+ const key = JSON.stringify(value);
+ if (!output.has(key)) {
+ output.set(key, value);
+ }
+ }
+
+ return [...output.values()];
+}
+
+export function getTestResultForFailure(
+ test: Pick<MochaTestResult, 'err'>
+): TestResult {
+ return test.err?.code === 'ERR_MOCHA_TIMEOUT' ? 'TIMEOUT' : 'FAIL';
+}
+
+export function getTestId(file: string, fullTitle?: string): string {
+ return fullTitle
+ ? `[${getFilename(file)}] ${fullTitle}`
+ : `[${getFilename(file)}]`;
+}
+
+export function testIdMatchesExpectationPattern(
+ test: MochaTestResult | Pick<Mocha.Test, 'title' | 'file' | 'fullTitle'>,
+ pattern: string
+): boolean {
+ const patternRegExString = pattern
+ // Replace `*` with non special character
+ .replace(/\*/g, '--STAR--')
+ // Escape special characters https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ // Replace placeholder with greedy match
+ .replace(/--STAR--/g, '(.*)?');
+ // Match beginning and end explicitly
+ const patternRegEx = new RegExp(`^${patternRegExString}$`);
+ const fullTitle =
+ typeof test.fullTitle === 'string' ? test.fullTitle : test.fullTitle();
+
+ return patternRegEx.test(getTestId(test.file ?? '', fullTitle));
+}
diff --git a/remote/test/puppeteer/tools/mocha-runner/tsconfig.json b/remote/test/puppeteer/tools/mocha-runner/tsconfig.json
new file mode 100644
index 0000000000..73a1b17815
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./bin",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "sourceMap": true,
+ "declaration": false,
+ "declarationMap": false,
+ "composite": false,
+ },
+}
diff --git a/remote/test/puppeteer/tools/mocha-runner/tsdoc.json b/remote/test/puppeteer/tools/mocha-runner/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/tools/mocha-runner/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/tools/sort-test-expectations.mjs b/remote/test/puppeteer/tools/sort-test-expectations.mjs
new file mode 100644
index 0000000000..d1c8588d8a
--- /dev/null
+++ b/remote/test/puppeteer/tools/sort-test-expectations.mjs
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// TODO: this could be an eslint rule probably.
+import fs from 'fs';
+import path from 'path';
+import url from 'url';
+
+import prettier from 'prettier';
+
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
+const source = 'test/TestExpectations.json';
+const testExpectations = JSON.parse(fs.readFileSync(source, 'utf-8'));
+const committedExpectations = structuredClone(testExpectations);
+
+const prettierConfig = await import(
+ path.join(__dirname, '..', '.prettierrc.cjs')
+);
+
+function getSpecificity(item) {
+ return (
+ item.parameters.length +
+ (item.testIdPattern.includes('*')
+ ? item.testIdPattern === '*'
+ ? 0
+ : 1
+ : 2)
+ );
+}
+
+testExpectations.sort((a, b) => {
+ const result = getSpecificity(a) - getSpecificity(b);
+ if (result === 0) {
+ return a.testIdPattern.localeCompare(b.testIdPattern);
+ }
+ return result;
+});
+
+testExpectations.forEach(item => {
+ item.parameters.sort();
+ item.expectations.sort();
+ item.platforms.sort();
+});
+
+if (process.argv.includes('--lint')) {
+ if (
+ JSON.stringify(committedExpectations) !== JSON.stringify(testExpectations)
+ ) {
+ console.error(
+ `${source} is not formatted properly. Run 'npm run format:expectations'.`
+ );
+ process.exit(1);
+ }
+} else {
+ fs.writeFileSync(
+ source,
+ await prettier.format(JSON.stringify(testExpectations), {
+ ...prettierConfig,
+ parser: 'json',
+ })
+ );
+}
diff --git a/remote/test/puppeteer/tools/third_party/validate-licenses.ts b/remote/test/puppeteer/tools/third_party/validate-licenses.ts
new file mode 100644
index 0000000000..56964854bd
--- /dev/null
+++ b/remote/test/puppeteer/tools/third_party/validate-licenses.ts
@@ -0,0 +1,154 @@
+// The MIT License
+
+// Copyright (c) 2010-2022 Google LLC. http://angular.io/license
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// Taken and adapted from https://github.com/angular/angular-cli/blob/173823d/scripts/validate-licenses.ts.
+
+import * as path from 'path';
+
+import checker from 'license-checker';
+import spdxSatisfies from 'spdx-satisfies';
+
+/**
+ * A general note on some black listed specific licenses:
+ *
+ * - CC0 This is not a valid license. It does not grant copyright of the
+ * code/asset, and does not resolve patents or other licensed work. The
+ * different claims also have no standing in court and do not provide
+ * protection to or from Google and/or third parties. We cannot use nor
+ * contribute to CC0 licenses.
+ * - Public Domain Same as CC0, it is not a valid license.
+ */
+const allowedLicenses = [
+ // Regular valid open source licenses supported by Google.
+ 'MIT',
+ 'ISC',
+ 'Apache-2.0',
+ 'Python-2.0',
+ 'Artistic-2.0',
+ 'BlueOak-1.0.0',
+
+ 'BSD-2-Clause',
+ 'BSD-3-Clause',
+ 'BSD-4-Clause',
+
+ // All CC-BY licenses have a full copyright grant and attribution section.
+ 'CC-BY-3.0',
+ 'CC-BY-4.0',
+
+ // Have a full copyright grant. Validated by opensource team.
+ 'Unlicense',
+ 'CC0-1.0',
+ '0BSD',
+
+ // Combinations.
+ '(AFL-2.1 OR BSD-2-Clause)',
+];
+
+// Name variations of SPDX licenses that some packages have.
+// Licenses not included in SPDX but accepted will be converted to MIT.
+const licenseReplacements: {[key: string]: string} = {
+ // Just a longer string that our script catches. SPDX official name is the shorter one.
+ 'Apache License, Version 2.0': 'Apache-2.0',
+ Apache2: 'Apache-2.0',
+ 'Apache 2.0': 'Apache-2.0',
+ 'Apache v2': 'Apache-2.0',
+ 'AFLv2.1': 'AFL-2.1',
+ // BSD is BSD-2-clause by default.
+ BSD: 'BSD-2-Clause',
+};
+
+// Specific packages to ignore, add a reason in a comment. Format: package-name@version.
+const ignoredPackages = [
+ // * Development only
+ 'spdx-license-ids@3.0.5', // CC0 but it's content only (index.json, no code) and not distributed.
+];
+
+// Check if a license is accepted by an array of accepted licenses
+function _passesSpdx(licenses: string[], accepted: string[]) {
+ try {
+ return spdxSatisfies(licenses.join(' AND '), accepted.join(' OR '));
+ } catch {
+ return false;
+ }
+}
+
+function main(): Promise<number> {
+ return new Promise(resolve => {
+ const startFolder = path.join(__dirname, '..', '..');
+ checker.init(
+ {start: startFolder, excludePrivatePackages: true},
+ (err: Error, json: object) => {
+ if (err) {
+ console.error(`Something happened:\n${err.message}`);
+ resolve(1);
+ } else {
+ console.info(`Testing ${Object.keys(json).length} packages.\n`);
+
+ // Packages with bad licenses are those that neither pass SPDX nor are ignored.
+ const badLicensePackages = Object.keys(json)
+ .map(key => {
+ return {
+ id: key,
+ licenses: ([] as string[])
+ .concat((json[key] as {licenses: string[]}).licenses)
+ // `*` is used when the license is guessed.
+ .map(x => {
+ return x.replace(/\*$/, '');
+ })
+ .map(x => {
+ return x in licenseReplacements
+ ? licenseReplacements[x]
+ : x;
+ }),
+ };
+ })
+ .filter(pkg => {
+ return !_passesSpdx(pkg.licenses, allowedLicenses);
+ })
+ .filter(pkg => {
+ return !ignoredPackages.find(ignored => {
+ return ignored === pkg.id;
+ });
+ });
+
+ // Report packages with bad licenses
+ if (badLicensePackages.length > 0) {
+ console.error('Invalid package licences found:');
+ badLicensePackages.forEach(pkg => {
+ console.error(`${pkg.id}: ${JSON.stringify(pkg.licenses)}`);
+ });
+ console.error(
+ `\n${badLicensePackages.length} total packages with invalid licenses.`
+ );
+ resolve(2);
+ } else {
+ console.info('All package licenses are valid.');
+ resolve(0);
+ }
+ }
+ }
+ );
+ });
+}
+
+main().then(code => {
+ return process.exit(code);
+});
diff --git a/remote/test/puppeteer/tools/tsconfig.json b/remote/test/puppeteer/tools/tsconfig.json
new file mode 100644
index 0000000000..964d349435
--- /dev/null
+++ b/remote/test/puppeteer/tools/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../tsconfig.base.json",
+ "files": ["../package.json"],
+}
diff --git a/remote/test/puppeteer/tools/tsdoc.json b/remote/test/puppeteer/tools/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/tools/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/tools/update_chrome_revision.mjs b/remote/test/puppeteer/tools/update_chrome_revision.mjs
new file mode 100644
index 0000000000..64eeef74d5
--- /dev/null
+++ b/remote/test/puppeteer/tools/update_chrome_revision.mjs
@@ -0,0 +1,162 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {execSync, exec} from 'child_process';
+import {writeFile, readFile} from 'fs/promises';
+import {promisify} from 'util';
+
+import actions from '@actions/core';
+import {SemVer} from 'semver';
+
+import packageJson from '../packages/puppeteer-core/package.json' assert {type: 'json'};
+import {versionsPerRelease, lastMaintainedChromeVersion} from '../versions.js';
+
+import {PUPPETEER_REVISIONS} from 'puppeteer-core/internal/revisions.js';
+
+const execAsync = promisify(exec);
+
+const CHROME_CURRENT_VERSION = PUPPETEER_REVISIONS.chrome;
+const VERSIONS_PER_RELEASE_COMMENT =
+ '// In Chrome roll patches, use `NEXT` for the Puppeteer version.';
+
+const touchedFiles = [];
+
+function checkIfNeedsUpdate(oldVersion, newVersion, newRevision) {
+ const oldSemVer = new SemVer(oldVersion, true);
+ const newSemVer = new SemVer(newVersion, true);
+ let message = `roll to Chrome ${newVersion} (r${newRevision})`;
+
+ if (newSemVer.compare(oldSemVer) <= 0) {
+ // Exit the process without setting up version
+ console.warn(
+ `Version ${newVersion} is older or the same as the current ${oldVersion}`
+ );
+ process.exit(0);
+ } else if (newSemVer.compareMain(oldSemVer) === 0) {
+ message = `fix: ${message}`;
+ } else {
+ message = `feat: ${message}`;
+ }
+ actions.setOutput('commit', message);
+}
+
+/**
+ * We cant use `npm run format` as it's too slow
+ * so we only scope the files we updated
+ */
+async function formatUpdateFiles() {
+ await Promise.all(
+ touchedFiles.map(file => {
+ return execAsync(`npx eslint --ext js --ext ts --fix ${file}`);
+ })
+ );
+ await Promise.all(
+ touchedFiles.map(file => {
+ return execAsync(`npx prettier --write ${file}`);
+ })
+ );
+}
+
+async function replaceInFile(filePath, search, replace) {
+ const buffer = await readFile(filePath);
+ const update = buffer.toString().replaceAll(search, replace);
+
+ await writeFile(filePath, update);
+
+ touchedFiles.push(filePath);
+}
+
+async function getVersionAndRevisionForStable() {
+ const result = await fetch(
+ 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json'
+ ).then(response => {
+ return response.json();
+ });
+
+ const {version, revision} = result.channels['Stable'];
+
+ return {
+ version,
+ revision,
+ };
+}
+
+async function updateDevToolsProtocolVersion(revision) {
+ const currentProtocol = packageJson.dependencies['devtools-protocol'];
+ const command = `npm view "devtools-protocol@<=0.0.${revision}" version | tail -1`;
+
+ const bestNewProtocol = execSync(command, {
+ encoding: 'utf8',
+ })
+ .split(' ')[1]
+ .replace(/'|\n/g, '');
+
+ await replaceInFile(
+ './packages/puppeteer-core/package.json',
+ `"devtools-protocol": "${currentProtocol}"`,
+ `"devtools-protocol": "${bestNewProtocol}"`
+ );
+}
+
+async function updateVersionFileLastMaintained(oldVersion, newVersion) {
+ const versions = [...versionsPerRelease.keys()];
+ if (versions.indexOf(newVersion) !== -1) {
+ return;
+ }
+
+ // If we have manually rolled Chrome but not yet released
+ // We will have NEXT as value in the Map
+ if (versionsPerRelease.get(oldVersion) === 'NEXT') {
+ await replaceInFile('./versions.js', oldVersion, newVersion);
+ return;
+ }
+
+ await replaceInFile(
+ './versions.js',
+ VERSIONS_PER_RELEASE_COMMENT,
+ `${VERSIONS_PER_RELEASE_COMMENT}\n ['${version}', 'NEXT'],`
+ );
+
+ const oldSemVer = new SemVer(oldVersion, true);
+ const newSemVer = new SemVer(newVersion, true);
+
+ if (newSemVer.compareMain(oldSemVer) !== 0) {
+ const lastMaintainedSemVer = new SemVer(lastMaintainedChromeVersion, true);
+ const newLastMaintainedMajor = lastMaintainedSemVer.major + 1;
+
+ const nextMaintainedVersion = versions.find(version => {
+ return new SemVer(version, true).major === newLastMaintainedMajor;
+ });
+
+ await replaceInFile(
+ './versions.js',
+ `const lastMaintainedChromeVersion = '${lastMaintainedChromeVersion}';`,
+ `const lastMaintainedChromeVersion = '${nextMaintainedVersion}';`
+ );
+ }
+}
+
+const {version, revision} = await getVersionAndRevisionForStable();
+
+checkIfNeedsUpdate(CHROME_CURRENT_VERSION, version, revision);
+
+await replaceInFile(
+ './packages/puppeteer-core/src/revisions.ts',
+ CHROME_CURRENT_VERSION,
+ version
+);
+
+await updateVersionFileLastMaintained(CHROME_CURRENT_VERSION, version);
+await updateDevToolsProtocolVersion(revision);
+
+// Create new `package-lock.json` as we update devtools-protocol
+execSync('npm install --ignore-scripts');
+// Make sure we pass CI formatter check by running all the new files though it
+await formatUpdateFiles();
+
+// Keep this as they can be used to debug GitHub Actions if needed
+actions.setOutput('version', version);
+actions.setOutput('revision', revision);
diff --git a/remote/test/puppeteer/tsconfig.base.json b/remote/test/puppeteer/tsconfig.base.json
new file mode 100644
index 0000000000..d0382df698
--- /dev/null
+++ b/remote/test/puppeteer/tsconfig.base.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "alwaysStrict": true,
+ "checkJs": true,
+ "composite": true,
+ "declaration": true,
+ "declarationMap": true,
+ "esModuleInterop": true,
+ "incremental": true,
+ "module": "ES2022",
+ "moduleResolution": "Bundler",
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "resolveJsonModule": true,
+ "sourceMap": true,
+ "strict": true,
+ "strictBindCallApply": true,
+ "strictFunctionTypes": true,
+ "strictNullChecks": true,
+ "strictPropertyInitialization": true,
+ "target": "ES2022",
+ "useUnknownInCatchVariables": true,
+ "skipLibCheck": true
+ }
+}
diff --git a/remote/test/puppeteer/tsdoc.json b/remote/test/puppeteer/tsdoc.json
new file mode 100644
index 0000000000..f5b91f4af6
--- /dev/null
+++ b/remote/test/puppeteer/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/versions.js b/remote/test/puppeteer/versions.js
new file mode 100644
index 0000000000..cbd835efc6
--- /dev/null
+++ b/remote/test/puppeteer/versions.js
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const versionsPerRelease = new Map([
+ // This is a mapping from Chrome version => Puppeteer version.
+ // In Chrome roll patches, use `NEXT` for the Puppeteer version.
+ ['121.0.6167.85', 'v21.9.0'],
+ ['120.0.6099.109', 'v21.8.0'],
+ ['119.0.6045.105', 'v21.5.0'],
+ ['118.0.5993.70', 'v21.4.0'],
+ ['117.0.5938.149', 'v21.3.7'],
+ ['117.0.5938.92', 'v21.3.2'],
+ ['117.0.5938.62', 'v21.3.0'],
+ ['116.0.5845.96', 'v21.1.0'],
+ ['115.0.5790.170', 'v21.0.2'],
+ ['115.0.5790.102', 'v21.0.0'],
+ ['115.0.5790.98', 'v20.9.0'],
+ ['114.0.5735.133', 'v20.7.2'],
+ ['114.0.5735.90', 'v20.6.0'],
+ ['113.0.5672.63', 'v20.1.0'],
+ ['112.0.5615.121', 'v20.0.0'],
+ ['112.0.5614.0', 'v19.8.0'],
+ ['111.0.5556.0', 'v19.7.0'],
+ ['110.0.5479.0', 'v19.6.0'],
+ ['109.0.5412.0', 'v19.4.0'],
+ ['108.0.5351.0', 'v19.2.0'],
+ ['107.0.5296.0', 'v18.1.0'],
+ ['106.0.5249.0', 'v17.1.0'],
+ ['105.0.5173.0', 'v15.5.0'],
+ ['104.0.5109.0', 'v15.1.0'],
+ ['103.0.5059.0', 'v14.2.0'],
+ ['102.0.5002.0', 'v14.0.0'],
+ ['101.0.4950.0', 'v13.6.0'],
+ ['100.0.4889.0', 'v13.5.0'],
+ ['99.0.4844.16', 'v13.2.0'],
+ ['98.0.4758.0', 'v13.1.0'],
+ ['97.0.4692.0', 'v12.0.0'],
+ ['93.0.4577.0', 'v10.2.0'],
+ ['92.0.4512.0', 'v10.0.0'],
+ ['91.0.4469.0', 'v9.0.0'],
+ ['90.0.4427.0', 'v8.0.0'],
+ ['90.0.4403.0', 'v7.0.0'],
+ ['89.0.4389.0', 'v6.0.0'],
+ ['88.0.4298.0', 'v5.5.0'],
+ ['87.0.4272.0', 'v5.4.0'],
+ ['86.0.4240.0', 'v5.3.0'],
+ ['85.0.4182.0', 'v5.2.1'],
+ ['84.0.4147.0', 'v5.1.0'],
+ ['83.0.4103.0', 'v3.1.0'],
+ ['81.0.4044.0', 'v3.0.0'],
+ ['80.0.3987.0', 'v2.1.0'],
+ ['79.0.3942.0', 'v2.0.0'],
+ ['78.0.3882.0', 'v1.20.0'],
+ ['77.0.3803.0', 'v1.19.0'],
+ ['76.0.3803.0', 'v1.17.0'],
+ ['75.0.3765.0', 'v1.15.0'],
+ ['74.0.3723.0', 'v1.13.0'],
+ ['73.0.3679.0', 'v1.12.2'],
+]);
+
+// Should not be more than 2 major versions behind Chrome Stable (https://chromestatus.com/roadmap).
+const lastMaintainedChromeVersion = '119.0.6045.105';
+
+if (!versionsPerRelease.has(lastMaintainedChromeVersion)) {
+ throw new Error(
+ 'lastMaintainedChromeVersion is missing from versionsPerRelease'
+ );
+}
+
+module.exports = {
+ versionsPerRelease,
+ lastMaintainedChromeVersion,
+};