From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- remote/test/puppeteer/.editorconfig | 9 + remote/test/puppeteer/.eslintignore | 46 + remote/test/puppeteer/.eslintplugin.js | 95 + remote/test/puppeteer/.eslintrc.js | 205 + remote/test/puppeteer/.eslintrc.types.cjs | 17 + remote/test/puppeteer/.mocharc.cjs | 27 + remote/test/puppeteer/.npmrc | 1 + remote/test/puppeteer/.prettierignore | 53 + remote/test/puppeteer/.prettierrc.cjs | 7 + .../test/puppeteer/.release-please-manifest.json | 7 + remote/test/puppeteer/LICENSE | 202 + remote/test/puppeteer/README.md | 253 + remote/test/puppeteer/SECURITY.md | 7 + remote/test/puppeteer/commitlint.config.js | 28 + remote/test/puppeteer/examples/README.md | 42 + remote/test/puppeteer/examples/block-images.js | 36 + remote/test/puppeteer/examples/cross-browser.js | 48 + remote/test/puppeteer/examples/custom-event.js | 50 + remote/test/puppeteer/examples/detect-sniff.js | 49 + remote/test/puppeteer/examples/oopif.js | 49 + remote/test/puppeteer/examples/pdf.js | 35 + remote/test/puppeteer/examples/proxy.js | 35 + .../test/puppeteer/examples/screenshot-fullpage.js | 28 + remote/test/puppeteer/examples/screenshot.js | 27 + remote/test/puppeteer/examples/search.js | 55 + remote/test/puppeteer/json-mocha-reporter.js | 69 + remote/test/puppeteer/moz.yaml | 10 + remote/test/puppeteer/package-lock.json | 16194 +++++++++++++++++++ remote/test/puppeteer/package.json | 193 + remote/test/puppeteer/packages/browsers/.gitignore | 1 + .../test/puppeteer/packages/browsers/.mocharc.cjs | 7 + .../test/puppeteer/packages/browsers/CHANGELOG.md | 131 + remote/test/puppeteer/packages/browsers/README.md | 28 + .../packages/browsers/api-extractor.docs.json | 15 + .../puppeteer/packages/browsers/api-extractor.json | 40 + .../test/puppeteer/packages/browsers/package.json | 123 + remote/test/puppeteer/packages/browsers/src/CLI.ts | 313 + .../test/puppeteer/packages/browsers/src/Cache.ts | 119 + .../browsers/src/browser-data/browser-data.ts | 124 + .../packages/browsers/src/browser-data/chrome.ts | 169 + .../browsers/src/browser-data/chromedriver.ts | 93 + .../packages/browsers/src/browser-data/chromium.ts | 125 + .../packages/browsers/src/browser-data/firefox.ts | 355 + .../packages/browsers/src/browser-data/types.ts | 75 + .../test/puppeteer/packages/browsers/src/debug.ts | 19 + .../packages/browsers/src/detectPlatform.ts | 61 + .../puppeteer/packages/browsers/src/fileUtil.ts | 89 + .../puppeteer/packages/browsers/src/httpUtil.ts | 141 + .../puppeteer/packages/browsers/src/install.ts | 218 + .../test/puppeteer/packages/browsers/src/launch.ts | 494 + .../puppeteer/packages/browsers/src/main-cli.ts | 21 + .../test/puppeteer/packages/browsers/src/main.ts | 40 + .../packages/browsers/src/tsconfig.cjs.json | 7 + .../packages/browsers/src/tsconfig.esm.json | 6 + .../browsers/test/src/chrome/chrome-data.spec.ts | 120 + .../packages/browsers/test/src/chrome/cli.spec.ts | 104 + .../browsers/test/src/chrome/install.spec.ts | 239 + .../browsers/test/src/chrome/launch.spec.ts | 132 + .../src/chromedriver/chromedriver-data.spec.ts | 71 + .../browsers/test/src/chromedriver/cli.spec.ts | 89 + .../browsers/test/src/chromedriver/install.spec.ts | 102 + .../test/src/chromium/chromium-data.spec.ts | 108 + .../browsers/test/src/chromium/launch.spec.ts | 132 + .../packages/browsers/test/src/firefox/cli.spec.ts | 79 + .../browsers/test/src/firefox/firefox-data.spec.ts | 107 + .../browsers/test/src/firefox/install.spec.ts | 85 + .../packages/browsers/test/src/firefox/launch.ts | 102 + .../packages/browsers/test/src/tsconfig.json | 8 + .../puppeteer/packages/browsers/test/src/utils.ts | 85 + .../packages/browsers/test/src/versions.ts | 22 + .../browsers/tools/downloadTestBrowsers.mjs | 81 + .../test/puppeteer/packages/browsers/tsconfig.json | 8 + .../puppeteer/packages/ng-schematics/.eslintignore | 5 + .../puppeteer/packages/ng-schematics/.gitignore | 3 + .../puppeteer/packages/ng-schematics/.mocharc.cjs | 6 + .../puppeteer/packages/ng-schematics/CHANGELOG.md | 19 + .../puppeteer/packages/ng-schematics/README.md | 67 + .../packages/ng-schematics/package-lock.json | 1098 ++ .../puppeteer/packages/ng-schematics/package.json | 68 + .../ng-schematics/src/builders/builders.json | 10 + .../ng-schematics/src/builders/puppeteer/index.ts | 138 + .../src/builders/puppeteer/schema.json | 22 + .../ng-schematics/src/builders/puppeteer/types.ts | 24 + .../ng-schematics/src/schematics/collection.json | 10 + .../ng-add/files/base/.puppeteerrc.cjs.template | 4 + .../files/base/e2e/tests/app.e2e.ts.template | 59 + .../ng-add/files/base/e2e/tsconfig.json.template | 15 + .../ng-add/files/jasmine/e2e/helpers/babel.js | 4 + .../ng-add/files/jasmine/e2e/support/jasmine.json | 9 + .../ng-add/files/jest/e2e/jest.config.js | 11 + .../schematics/ng-add/files/mocha/e2e/.mocharc.js | 4 + .../src/schematics/ng-add/files/mocha/e2e/babel.js | 4 + .../ng-add/files/node/e2e/.gitignore.template | 3 + .../ng-schematics/src/schematics/ng-add/index.ts | 127 + .../src/schematics/ng-add/schema.json | 49 + .../ng-schematics/src/schematics/utils/files.ts | 162 + .../ng-schematics/src/schematics/utils/json.ts | 38 + .../ng-schematics/src/schematics/utils/packages.ts | 204 + .../ng-schematics/src/schematics/utils/types.ts | 28 + .../packages/ng-schematics/test/src/index.spec.ts | 212 + .../ng-schematics/tools/copySchemaFiles.js | 72 + .../packages/ng-schematics/tools/sandbox.js | 104 + .../puppeteer/packages/ng-schematics/tsconfig.json | 19 + .../packages/ng-schematics/tsconfig.spec.json | 10 + .../puppeteer/packages/puppeteer-core/.gitignore | 1 + .../puppeteer/packages/puppeteer-core/CHANGELOG.md | 1322 ++ .../puppeteer-core/api-extractor.docs.json | 15 + .../packages/puppeteer-core/api-extractor.json | 46 + .../puppeteer/packages/puppeteer-core/package.json | 159 + .../puppeteer-core/rollup.third_party.config.mjs | 35 + .../packages/puppeteer-core/src/api/Browser.ts | 473 + .../puppeteer-core/src/api/BrowserContext.ts | 186 + .../puppeteer-core/src/api/ElementHandle.ts | 917 ++ .../packages/puppeteer-core/src/api/HTTPRequest.ts | 567 + .../puppeteer-core/src/api/HTTPResponse.ts | 168 + .../packages/puppeteer-core/src/api/JSHandle.ts | 197 + .../packages/puppeteer-core/src/api/Page.ts | 2748 ++++ .../packages/puppeteer-core/src/api/api.ts | 23 + .../puppeteer-core/src/common/Accessibility.ts | 577 + .../puppeteer-core/src/common/AriaQueryHandler.ts | 124 + .../packages/puppeteer-core/src/common/Binding.ts | 123 + .../packages/puppeteer-core/src/common/Browser.ts | 737 + .../puppeteer-core/src/common/BrowserConnector.ts | 176 + .../src/common/BrowserWebSocketTransport.ts | 60 + .../src/common/ChromeTargetManager.ts | 401 + .../puppeteer-core/src/common/Configuration.ts | 121 + .../puppeteer-core/src/common/Connection.ts | 615 + .../src/common/ConnectionTransport.ts | 25 + .../puppeteer-core/src/common/ConsoleMessage.ts | 123 + .../packages/puppeteer-core/src/common/Coverage.ts | 501 + .../src/common/CustomQueryHandler.ts | 227 + .../packages/puppeteer-core/src/common/Debug.ts | 136 + .../packages/puppeteer-core/src/common/Device.ts | 1562 ++ .../src/common/DeviceRequestPrompt.ts | 293 + .../packages/puppeteer-core/src/common/Dialog.ts | 117 + .../puppeteer-core/src/common/ElementHandle.ts | 777 + .../puppeteer-core/src/common/EmulationManager.ts | 63 + .../packages/puppeteer-core/src/common/Errors.ts | 115 + .../puppeteer-core/src/common/EventEmitter.ts | 165 + .../puppeteer-core/src/common/ExecutionContext.ts | 402 + .../puppeteer-core/src/common/FileChooser.ts | 97 + .../src/common/FirefoxTargetManager.ts | 259 + .../packages/puppeteer-core/src/common/Frame.ts | 1160 ++ .../puppeteer-core/src/common/FrameManager.ts | 478 + .../puppeteer-core/src/common/FrameTree.ts | 112 + .../puppeteer-core/src/common/GetQueryHandler.ts | 70 + .../puppeteer-core/src/common/HTTPRequest.ts | 445 + .../puppeteer-core/src/common/HTTPResponse.ts | 188 + .../puppeteer-core/src/common/HandleIterator.ts | 84 + .../packages/puppeteer-core/src/common/Input.ts | 920 ++ .../puppeteer-core/src/common/IsolatedWorld.ts | 537 + .../puppeteer-core/src/common/IsolatedWorlds.ts | 30 + .../packages/puppeteer-core/src/common/JSHandle.ts | 168 + .../packages/puppeteer-core/src/common/LazyArg.ts | 39 + .../puppeteer-core/src/common/LifecycleWatcher.ts | 304 + .../src/common/NetworkEventManager.ts | 220 + .../puppeteer-core/src/common/NetworkManager.ts | 652 + .../src/common/NodeWebSocketTransport.ts | 74 + .../puppeteer-core/src/common/PDFOptions.ts | 221 + .../puppeteer-core/src/common/PQueryHandler.ts | 37 + .../packages/puppeteer-core/src/common/Page.ts | 1656 ++ .../src/common/PierceQueryHandler.ts | 39 + .../src/common/PredefinedNetworkConditions.ts | 59 + .../packages/puppeteer-core/src/common/Product.ts | 21 + .../puppeteer-core/src/common/Puppeteer.ts | 147 + .../puppeteer-core/src/common/PuppeteerViewport.ts | 55 + .../puppeteer-core/src/common/QueryHandler.ts | 226 + .../puppeteer-core/src/common/ScriptInjector.ts | 49 + .../puppeteer-core/src/common/SecurityDetails.ts | 88 + .../packages/puppeteer-core/src/common/Target.ts | 285 + .../puppeteer-core/src/common/TargetManager.ts | 72 + .../puppeteer-core/src/common/TaskQueue.ts | 39 + .../puppeteer-core/src/common/TextQueryHandler.ts | 30 + .../puppeteer-core/src/common/TimeoutSettings.ts | 55 + .../packages/puppeteer-core/src/common/Tracing.ts | 144 + .../puppeteer-core/src/common/USKeyboardLayout.ts | 681 + .../packages/puppeteer-core/src/common/WaitTask.ts | 260 + .../puppeteer-core/src/common/WebWorker.ts | 179 + .../puppeteer-core/src/common/XPathQueryHandler.ts | 30 + .../puppeteer-core/src/common/bidi/BidiOverCDP.ts | 190 + .../puppeteer-core/src/common/bidi/Browser.ts | 89 + .../src/common/bidi/BrowserContext.ts | 59 + .../puppeteer-core/src/common/bidi/Connection.ts | 215 + .../puppeteer-core/src/common/bidi/Context.ts | 282 + .../src/common/bidi/ElementHandle.ts | 52 + .../puppeteer-core/src/common/bidi/JSHandle.ts | 159 + .../puppeteer-core/src/common/bidi/Page.ts | 345 + .../puppeteer-core/src/common/bidi/Serializer.ts | 273 + .../puppeteer-core/src/common/bidi/bidi.ts | 21 + .../puppeteer-core/src/common/bidi/utils.ts | 47 + .../packages/puppeteer-core/src/common/common.ts | 70 + .../packages/puppeteer-core/src/common/fetch.ts | 24 + .../packages/puppeteer-core/src/common/types.ts | 213 + .../packages/puppeteer-core/src/common/util.ts | 472 + .../packages/puppeteer-core/src/environment.ts | 29 + .../puppeteer-core/src/generated/injected.ts | 8 + .../puppeteer-core/src/generated/version.ts | 4 + .../src/injected/ARIAQuerySelector.ts | 41 + .../src/injected/CustomQuerySelector.ts | 69 + .../puppeteer-core/src/injected/PQuerySelector.ts | 308 + .../puppeteer-core/src/injected/PSelectorParser.ts | 119 + .../src/injected/PierceQuerySelector.ts | 73 + .../packages/puppeteer-core/src/injected/Poller.ts | 181 + .../puppeteer-core/src/injected/TextContent.ts | 155 + .../src/injected/TextQuerySelector.ts | 56 + .../src/injected/XPathQuerySelector.ts | 35 + .../puppeteer-core/src/injected/injected.ts | 61 + .../packages/puppeteer-core/src/injected/util.ts | 67 + .../puppeteer-core/src/node/ChromeLauncher.ts | 257 + .../puppeteer-core/src/node/FirefoxLauncher.ts | 225 + .../puppeteer-core/src/node/LaunchOptions.ts | 150 + .../puppeteer-core/src/node/PipeTransport.ts | 93 + .../puppeteer-core/src/node/ProductLauncher.ts | 448 + .../puppeteer-core/src/node/PuppeteerNode.ts | 267 + .../packages/puppeteer-core/src/node/node.ts | 22 + .../packages/puppeteer-core/src/node/util/fs.ts | 37 + .../packages/puppeteer-core/src/puppeteer-core.ts | 58 + .../packages/puppeteer-core/src/revisions.ts | 23 + .../puppeteer-core/src/templates/injected.ts.tmpl | 8 + .../puppeteer-core/src/templates/version.ts.tmpl | 4 + .../packages/puppeteer-core/src/tsconfig.cjs.json | 8 + .../packages/puppeteer-core/src/tsconfig.esm.json | 7 + .../puppeteer-core/src/util/AsyncIterableUtil.ts | 56 + .../src/util/DebuggableDeferredPromise.ts | 21 + .../puppeteer-core/src/util/DeferredPromise.ts | 68 + .../packages/puppeteer-core/src/util/ErrorLike.ts | 27 + .../packages/puppeteer-core/src/util/Function.ts | 98 + .../packages/puppeteer-core/src/util/assert.ts | 31 + .../packages/puppeteer-core/src/util/util.ts | 21 + .../puppeteer-core/third_party/mitt/index.ts | 18 + .../puppeteer-core/third_party/tsconfig.cjs.json | 8 + .../puppeteer-core/third_party/tsconfig.json | 8 + .../ensure-correct-devtools-protocol-package.ts | 97 + .../puppeteer-core/tools/generate_sources.ts | 88 + .../packages/puppeteer-core/tsconfig.json | 8 + .../test/puppeteer/packages/puppeteer/.gitignore | 1 + .../test/puppeteer/packages/puppeteer/CHANGELOG.md | 1457 ++ .../packages/puppeteer/api-extractor.docs.json | 15 + .../packages/puppeteer/api-extractor.json | 49 + .../test/puppeteer/packages/puppeteer/install.js | 43 + .../test/puppeteer/packages/puppeteer/package.json | 126 + .../packages/puppeteer/src/getConfiguration.ts | 97 + .../packages/puppeteer/src/node/install.ts | 131 + .../puppeteer/packages/puppeteer/src/puppeteer.ts | 54 + .../packages/puppeteer/src/tsconfig.cjs.json | 8 + .../packages/puppeteer/src/tsconfig.esm.json | 7 + .../puppeteer/packages/puppeteer/tsconfig.json | 16 + .../puppeteer/packages/testserver/CHANGELOG.md | 8 + remote/test/puppeteer/packages/testserver/LICENSE | 202 + .../test/puppeteer/packages/testserver/README.md | 18 + remote/test/puppeteer/packages/testserver/cert.pem | 20 + remote/test/puppeteer/packages/testserver/key.pem | 28 + .../puppeteer/packages/testserver/package.json | 33 + .../puppeteer/packages/testserver/src/index.ts | 303 + .../puppeteer/packages/testserver/tsconfig.json | 11 + remote/test/puppeteer/release-please-config.json | 29 + .../test/puppeteer/test-d/ElementHandle.test-d.ts | 1024 ++ remote/test/puppeteer/test-d/JSHandle.test-d.ts | 83 + remote/test/puppeteer/test-d/NodeFor.test-d.ts | 156 + remote/test/puppeteer/test-d/puppeteer.test-d.ts | 12 + remote/test/puppeteer/test/.eslintrc.js | 13 + remote/test/puppeteer/test/README.md | 95 + remote/test/puppeteer/test/TestExpectations.json | 2234 +++ remote/test/puppeteer/test/TestSuites.json | 68 + .../test/puppeteer/test/assets/abort-request.html | 13 + .../test/puppeteer/test/assets/beforeunload.html | 10 + .../test/assets/cached/one-style-font.css | 9 + .../test/assets/cached/one-style-font.html | 2 + .../puppeteer/test/assets/cached/one-style.css | 3 + .../puppeteer/test/assets/cached/one-style.html | 2 + .../test/puppeteer/test/assets/chromium-linux.zip | Bin 0 -> 325 bytes remote/test/puppeteer/test/assets/consolelog.html | 17 + remote/test/puppeteer/test/assets/csp.html | 1 + .../test/assets/csscoverage/Dosis-Regular.ttf | Bin 0 -> 136940 bytes .../test/puppeteer/test/assets/csscoverage/OFL.txt | 95 + .../puppeteer/test/assets/csscoverage/empty.html | 3 + .../test/assets/csscoverage/involved.html | 26 + .../puppeteer/test/assets/csscoverage/media.html | 4 + .../test/assets/csscoverage/multiple.html | 8 + .../puppeteer/test/assets/csscoverage/simple.html | 6 + .../test/assets/csscoverage/sourceurl.html | 7 + .../test/assets/csscoverage/stylesheet1.css | 3 + .../test/assets/csscoverage/stylesheet2.css | 4 + .../puppeteer/test/assets/csscoverage/unused.html | 7 + .../test/puppeteer/test/assets/detect-touch.html | 12 + remote/test/puppeteer/test/assets/digits/0.png | Bin 0 -> 434 bytes remote/test/puppeteer/test/assets/digits/1.png | Bin 0 -> 346 bytes remote/test/puppeteer/test/assets/digits/2.png | Bin 0 -> 413 bytes remote/test/puppeteer/test/assets/digits/3.png | Bin 0 -> 434 bytes remote/test/puppeteer/test/assets/digits/4.png | Bin 0 -> 403 bytes remote/test/puppeteer/test/assets/digits/5.png | Bin 0 -> 422 bytes remote/test/puppeteer/test/assets/digits/6.png | Bin 0 -> 445 bytes remote/test/puppeteer/test/assets/digits/7.png | Bin 0 -> 387 bytes remote/test/puppeteer/test/assets/digits/8.png | Bin 0 -> 447 bytes remote/test/puppeteer/test/assets/digits/9.png | Bin 0 -> 437 bytes .../test/puppeteer/test/assets/dynamic-oopif.html | 10 + remote/test/puppeteer/test/assets/empty.html | 0 remote/test/puppeteer/test/assets/error.html | 15 + remote/test/puppeteer/test/assets/es6/.eslintrc | 5 + remote/test/puppeteer/test/assets/es6/es6import.js | 2 + remote/test/puppeteer/test/assets/es6/es6module.js | 1 + .../puppeteer/test/assets/es6/es6pathimport.js | 2 + remote/test/puppeteer/test/assets/favicon.ico | Bin 0 -> 70 bytes .../test/puppeteer/test/assets/file-to-upload.txt | 1 + .../firefox-75.0a1.en-US.linux-x86_64.tar.bz2 | Bin 0 -> 211 bytes .../test/puppeteer/test/assets/frames/frame.html | 8 + .../puppeteer/test/assets/frames/frameset.html | 8 + .../puppeteer/test/assets/frames/lazy-frame.html | 3 + .../test/assets/frames/nested-frames.html | 25 + .../test/assets/frames/one-frame-url-fragment.html | 1 + .../puppeteer/test/assets/frames/one-frame.html | 1 + remote/test/puppeteer/test/assets/frames/script.js | 1 + remote/test/puppeteer/test/assets/frames/style.css | 3 + .../puppeteer/test/assets/frames/two-frames.html | 13 + remote/test/puppeteer/test/assets/global-var.html | 3 + remote/test/puppeteer/test/assets/grid.html | 52 + remote/test/puppeteer/test/assets/historyapi.html | 5 + .../test/puppeteer/test/assets/idle-detector.html | 23 + remote/test/puppeteer/test/assets/initiator.html | 2 + remote/test/puppeteer/test/assets/initiator.js | 8 + remote/test/puppeteer/test/assets/injectedfile.js | 2 + .../test/puppeteer/test/assets/injectedstyle.css | 3 + remote/test/puppeteer/test/assets/inline-svg.html | 14 + .../test/puppeteer/test/assets/inner-frame1.html | 10 + .../test/puppeteer/test/assets/inner-frame2.html | 1 + .../test/puppeteer/test/assets/input/button.html | 16 + .../test/puppeteer/test/assets/input/checkbox.html | 42 + .../puppeteer/test/assets/input/drag-and-drop.html | 46 + .../puppeteer/test/assets/input/fileupload.html | 9 + .../test/puppeteer/test/assets/input/keyboard.html | 42 + .../puppeteer/test/assets/input/mouse-helper.js | 74 + .../puppeteer/test/assets/input/rotatedButton.html | 21 + .../puppeteer/test/assets/input/scrollable.html | 37 + .../test/puppeteer/test/assets/input/select.html | 70 + .../test/puppeteer/test/assets/input/textarea.html | 15 + .../puppeteer/test/assets/input/touches-move.html | 65 + .../test/puppeteer/test/assets/input/touches.html | 35 + remote/test/puppeteer/test/assets/input/wheel.html | 43 + .../puppeteer/test/assets/jscoverage/eval.html | 1 + .../puppeteer/test/assets/jscoverage/involved.html | 16 + .../puppeteer/test/assets/jscoverage/multiple.html | 2 + .../puppeteer/test/assets/jscoverage/ranges.html | 2 + .../puppeteer/test/assets/jscoverage/script1.js | 1 + .../puppeteer/test/assets/jscoverage/script2.js | 1 + .../puppeteer/test/assets/jscoverage/simple.html | 2 + .../test/assets/jscoverage/sourceurl.html | 4 + .../puppeteer/test/assets/jscoverage/unused.html | 1 + .../puppeteer/test/assets/lazy-oopif-frame.html | 3 + remote/test/puppeteer/test/assets/main-frame.html | 10 + remote/test/puppeteer/test/assets/mobile.html | 1 + remote/test/puppeteer/test/assets/modernizr.js | 3 + remote/test/puppeteer/test/assets/networkidle.html | 19 + .../puppeteer/test/assets/offscreenbuttons.html | 40 + remote/test/puppeteer/test/assets/one-style.css | 3 + remote/test/puppeteer/test/assets/one-style.html | 2 + remote/test/puppeteer/test/assets/oopif.html | 2 + remote/test/puppeteer/test/assets/p-selectors.html | 13 + remote/test/puppeteer/test/assets/pdf.html | 11 + remote/test/puppeteer/test/assets/playground.html | 15 + remote/test/puppeteer/test/assets/popup/popup.html | 9 + .../puppeteer/test/assets/popup/window-open.html | 11 + remote/test/puppeteer/test/assets/pptr.png | Bin 0 -> 6138 bytes remote/test/puppeteer/test/assets/resetcss.html | 50 + .../test/puppeteer/test/assets/self-request.html | 5 + .../test/assets/serviceworkers/empty/sw.html | 3 + .../test/assets/serviceworkers/empty/sw.js | 0 .../assets/serviceworkers/extension/background.js | 1 + .../assets/serviceworkers/extension/manifest.json | 9 + .../test/assets/serviceworkers/fetch/style.css | 3 + .../test/assets/serviceworkers/fetch/sw.html | 5 + .../test/assets/serviceworkers/fetch/sw.js | 7 + remote/test/puppeteer/test/assets/shadow.html | 17 + .../test/assets/simple-extension/content-script.js | 2 + .../test/assets/simple-extension/index.js | 2 + .../test/assets/simple-extension/manifest.json | 14 + remote/test/puppeteer/test/assets/simple.json | 1 + remote/test/puppeteer/test/assets/tamperable.html | 3 + remote/test/puppeteer/test/assets/title.html | 1 + .../test/puppeteer/test/assets/worker/worker.html | 14 + remote/test/puppeteer/test/assets/worker/worker.js | 16 + remote/test/puppeteer/test/assets/wrappedlink.html | 32 + remote/test/puppeteer/test/fixtures/closeme.js | 5 + remote/test/puppeteer/test/fixtures/dumpio.js | 10 + .../test/golden-chrome/csscoverage-involved.txt | 16 + .../puppeteer/test/golden-chrome/grid-cell-0.png | Bin 0 -> 436 bytes .../puppeteer/test/golden-chrome/grid-cell-1.png | Bin 0 -> 276 bytes .../puppeteer/test/golden-chrome/grid-cell-2.png | Bin 0 -> 428 bytes .../puppeteer/test/golden-chrome/grid-cell-3.png | Bin 0 -> 448 bytes .../test/golden-chrome/jscoverage-involved.txt | 36 + .../test/golden-chrome/mock-binary-response.png | Bin 0 -> 6789 bytes .../golden-chrome/screenshot-clip-odd-size.png | Bin 0 -> 81 bytes .../golden-chrome/screenshot-clip-rect-scale2.png | Bin 0 -> 8472 bytes .../test/golden-chrome/screenshot-clip-rect.png | Bin 0 -> 1962 bytes .../screenshot-element-bounding-box.png | Bin 0 -> 461 bytes .../screenshot-element-fractional-offset.png | Bin 0 -> 138 bytes .../screenshot-element-fractional.png | Bin 0 -> 138 bytes .../screenshot-element-larger-than-viewport.png | Bin 0 -> 2807 bytes .../screenshot-element-padding-border.png | Bin 0 -> 168 bytes .../golden-chrome/screenshot-element-rotate.png | Bin 0 -> 2342 bytes .../screenshot-element-scrolled-into-view.png | Bin 0 -> 168 bytes .../golden-chrome/screenshot-grid-fullpage.png | Bin 0 -> 74972 bytes .../golden-chrome/screenshot-offscreen-clip.png | Bin 0 -> 266 bytes .../test/golden-chrome/screenshot-sanity.png | Bin 0 -> 36252 bytes .../puppeteer/test/golden-chrome/transparent.png | Bin 0 -> 119 bytes .../vision-deficiency-achromatopsia.png | Bin 0 -> 33569 bytes .../vision-deficiency-blurredVision.png | Bin 0 -> 81174 bytes .../vision-deficiency-deuteranopia.png | Bin 0 -> 37483 bytes .../golden-chrome/vision-deficiency-protanopia.png | Bin 0 -> 36282 bytes .../golden-chrome/vision-deficiency-tritanopia.png | Bin 0 -> 37282 bytes remote/test/puppeteer/test/golden-chrome/white.jpg | Bin 0 -> 357 bytes .../puppeteer/test/golden-firefox/grid-cell-0.png | Bin 0 -> 331 bytes .../puppeteer/test/golden-firefox/grid-cell-1.png | Bin 0 -> 201 bytes .../golden-firefox/screenshot-clip-odd-size.png | Bin 0 -> 75 bytes .../golden-firefox/screenshot-clip-rect-scale2.png | Bin 0 -> 8472 bytes .../test/golden-firefox/screenshot-clip-rect.png | Bin 0 -> 1371 bytes .../screenshot-element-bounding-box.png | Bin 0 -> 311 bytes .../screenshot-element-fractional-offset.png | Bin 0 -> 113 bytes .../screenshot-element-fractional.png | Bin 0 -> 109 bytes .../screenshot-element-larger-than-viewport.png | Bin 0 -> 2797 bytes .../screenshot-element-padding-border.png | Bin 0 -> 153 bytes .../golden-firefox/screenshot-element-rotate.png | Bin 0 -> 1800 bytes .../screenshot-element-scrolled-into-view.png | Bin 0 -> 153 bytes .../golden-firefox/screenshot-grid-fullpage.png | Bin 0 -> 55662 bytes .../golden-firefox/screenshot-offscreen-clip.png | Bin 0 -> 279 bytes .../test/golden-firefox/screenshot-sanity.png | Bin 0 -> 26146 bytes .../test/puppeteer/test/installation/.mocharc.cjs | 21 + .../installation/assets/puppeteer-core/imports.js | 19 + .../installation/assets/puppeteer-core/launch.js | 32 + .../assets/puppeteer-core/requires.cjs | 19 + .../test/installation/assets/puppeteer/basic.js | 25 + .../puppeteer/configuration/.puppeteerrc.cjs | 8 + .../test/installation/assets/puppeteer/imports.js | 20 + .../installation/assets/puppeteer/requires.cjs | 20 + .../assets/puppeteer/webpack/webpack.config.js | 25 + .../test/puppeteer/test/installation/package.json | 54 + .../test/installation/src/browsers.spec.ts | 39 + .../puppeteer/test/installation/src/constants.ts | 35 + .../src/puppeteer-configuration.spec.ts | 48 + .../test/installation/src/puppeteer-core.spec.ts | 44 + .../installation/src/puppeteer-firefox.spec.ts | 47 + .../installation/src/puppeteer-webpack.spec.ts | 57 + .../test/installation/src/puppeteer.spec.ts | 46 + .../puppeteer/test/installation/src/sandbox.ts | 137 + .../test/puppeteer/test/installation/src/util.ts | 27 + .../test/puppeteer/test/installation/tsconfig.json | 10 + remote/test/puppeteer/test/package.json | 30 + remote/test/puppeteer/test/src/CDPSession.spec.ts | 139 + .../puppeteer/test/src/DeviceRequestPrompt.spec.ts | 457 + .../test/puppeteer/test/src/EventEmitter.spec.ts | 170 + .../test/puppeteer/test/src/NetworkManager.spec.ts | 1537 ++ .../test/puppeteer/test/src/TargetManager.spec.ts | 110 + .../test/puppeteer/test/src/accessibility.spec.ts | 582 + .../puppeteer/test/src/ariaqueryhandler.spec.ts | 698 + .../puppeteer/test/src/bidi/Connection.spec.ts | 61 + remote/test/puppeteer/test/src/browser.spec.ts | 91 + .../test/puppeteer/test/src/browsercontext.spec.ts | 243 + .../test/puppeteer/test/src/chromiumonly.spec.ts | 172 + remote/test/puppeteer/test/src/click.spec.ts | 492 + remote/test/puppeteer/test/src/cookies.spec.ts | 570 + remote/test/puppeteer/test/src/coverage.spec.ts | 356 + .../test/src/defaultbrowsercontext.spec.ts | 115 + remote/test/puppeteer/test/src/dialog.spec.ts | 79 + remote/test/puppeteer/test/src/diffstyle.css | 13 + .../test/puppeteer/test/src/drag-and-drop.spec.ts | 197 + .../test/puppeteer/test/src/elementhandle.spec.ts | 802 + remote/test/puppeteer/test/src/emulation.spec.ts | 516 + remote/test/puppeteer/test/src/evaluation.spec.ts | 600 + remote/test/puppeteer/test/src/fixtures.spec.ts | 112 + remote/test/puppeteer/test/src/frame.spec.ts | 350 + remote/test/puppeteer/test/src/golden-utils.ts | 173 + remote/test/puppeteer/test/src/headful.spec.ts | 438 + .../test/puppeteer/test/src/idle_override.spec.ts | 95 + .../puppeteer/test/src/ignorehttpserrors.spec.ts | 143 + remote/test/puppeteer/test/src/injected.spec.ts | 65 + remote/test/puppeteer/test/src/input.spec.ts | 409 + remote/test/puppeteer/test/src/jshandle.spec.ts | 339 + remote/test/puppeteer/test/src/keyboard.spec.ts | 557 + remote/test/puppeteer/test/src/launcher.spec.ts | 897 + remote/test/puppeteer/test/src/mocha-utils.ts | 330 + remote/test/puppeteer/test/src/mouse.spec.ts | 354 + remote/test/puppeteer/test/src/navigation.spec.ts | 850 + remote/test/puppeteer/test/src/network.spec.ts | 863 + remote/test/puppeteer/test/src/oopif.spec.ts | 454 + remote/test/puppeteer/test/src/page.spec.ts | 2330 +++ remote/test/puppeteer/test/src/proxy.spec.ts | 236 + .../test/puppeteer/test/src/queryhandler.spec.ts | 594 + .../test/puppeteer/test/src/queryselector.spec.ts | 505 + .../src/requestinterception-experimental.spec.ts | 983 ++ .../puppeteer/test/src/requestinterception.spec.ts | 934 ++ remote/test/puppeteer/test/src/screenshot.spec.ts | 388 + remote/test/puppeteer/test/src/target.spec.ts | 336 + remote/test/puppeteer/test/src/touchscreen.spec.ts | 80 + remote/test/puppeteer/test/src/tracing.spec.ts | 157 + remote/test/puppeteer/test/src/utils.ts | 150 + remote/test/puppeteer/test/src/waittask.spec.ts | 867 + remote/test/puppeteer/test/src/worker.spec.ts | 123 + remote/test/puppeteer/test/tsconfig.json | 11 + remote/test/puppeteer/tools/analyze_issue.mjs | 280 + remote/test/puppeteer/tools/assets/verify_issue.ts | 68 + remote/test/puppeteer/tools/chmod.ts | 27 + remote/test/puppeteer/tools/cp.ts | 22 + remote/test/puppeteer/tools/ensure-pinned-deps.ts | 59 + remote/test/puppeteer/tools/generate-matrix.js | 43 + remote/test/puppeteer/tools/generate_docs.ts | 152 + .../tools/generate_module_package_json.ts | 26 + .../tools/get_deprecated_version_range.js | 27 + .../tools/internal/custom_markdown_action.ts | 31 + .../tools/internal/custom_markdown_documenter.ts | 1481 ++ remote/test/puppeteer/tools/internal/job.ts | 153 + remote/test/puppeteer/tools/internal/util.ts | 14 + remote/test/puppeteer/tools/mochaRunner/README.md | 73 + .../puppeteer/tools/mochaRunner/src/interface.ts | 130 + .../test/puppeteer/tools/mochaRunner/src/main.ts | 259 + .../puppeteer/tools/mochaRunner/src/reporter.ts | 26 + .../test/puppeteer/tools/mochaRunner/src/test.ts | 134 + .../test/puppeteer/tools/mochaRunner/src/types.ts | 67 + .../test/puppeteer/tools/mochaRunner/src/utils.ts | 265 + .../test/puppeteer/tools/mochaRunner/tsconfig.json | 11 + .../test/puppeteer/tools/remove_version_suffix.js | 26 + .../test/puppeteer/tools/sort-test-expectations.js | 59 + .../tools/third_party/validate-licenses.ts | 153 + remote/test/puppeteer/tools/tsconfig.json | 4 + remote/test/puppeteer/tsconfig.base.json | 32 + remote/test/puppeteer/versions.js | 73 + 524 files changed, 89668 insertions(+) create mode 100644 remote/test/puppeteer/.editorconfig create mode 100644 remote/test/puppeteer/.eslintignore create mode 100644 remote/test/puppeteer/.eslintplugin.js create mode 100644 remote/test/puppeteer/.eslintrc.js create mode 100644 remote/test/puppeteer/.eslintrc.types.cjs create mode 100644 remote/test/puppeteer/.mocharc.cjs create mode 100644 remote/test/puppeteer/.npmrc create mode 100644 remote/test/puppeteer/.prettierignore create mode 100644 remote/test/puppeteer/.prettierrc.cjs create mode 100644 remote/test/puppeteer/.release-please-manifest.json create mode 100644 remote/test/puppeteer/LICENSE create mode 100644 remote/test/puppeteer/README.md create mode 100644 remote/test/puppeteer/SECURITY.md create mode 100644 remote/test/puppeteer/commitlint.config.js create mode 100644 remote/test/puppeteer/examples/README.md create mode 100644 remote/test/puppeteer/examples/block-images.js create mode 100644 remote/test/puppeteer/examples/cross-browser.js create mode 100644 remote/test/puppeteer/examples/custom-event.js create mode 100644 remote/test/puppeteer/examples/detect-sniff.js create mode 100644 remote/test/puppeteer/examples/oopif.js create mode 100644 remote/test/puppeteer/examples/pdf.js create mode 100644 remote/test/puppeteer/examples/proxy.js create mode 100644 remote/test/puppeteer/examples/screenshot-fullpage.js create mode 100644 remote/test/puppeteer/examples/screenshot.js create mode 100644 remote/test/puppeteer/examples/search.js create mode 100644 remote/test/puppeteer/json-mocha-reporter.js create mode 100644 remote/test/puppeteer/moz.yaml create mode 100644 remote/test/puppeteer/package-lock.json create mode 100644 remote/test/puppeteer/package.json create mode 100644 remote/test/puppeteer/packages/browsers/.gitignore create mode 100644 remote/test/puppeteer/packages/browsers/.mocharc.cjs create mode 100644 remote/test/puppeteer/packages/browsers/CHANGELOG.md create mode 100644 remote/test/puppeteer/packages/browsers/README.md create mode 100644 remote/test/puppeteer/packages/browsers/api-extractor.docs.json create mode 100644 remote/test/puppeteer/packages/browsers/api-extractor.json create mode 100644 remote/test/puppeteer/packages/browsers/package.json create mode 100644 remote/test/puppeteer/packages/browsers/src/CLI.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/Cache.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/browser-data/types.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/debug.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/detectPlatform.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/fileUtil.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/httpUtil.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/install.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/launch.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/main-cli.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/main.ts create mode 100644 remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json create mode 100644 remote/test/puppeteer/packages/browsers/src/tsconfig.esm.json create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/firefox/launch.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/tsconfig.json create mode 100644 remote/test/puppeteer/packages/browsers/test/src/utils.ts create mode 100644 remote/test/puppeteer/packages/browsers/test/src/versions.ts create mode 100644 remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs create mode 100644 remote/test/puppeteer/packages/browsers/tsconfig.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/.eslintignore create mode 100644 remote/test/puppeteer/packages/ng-schematics/.gitignore create mode 100644 remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs create mode 100644 remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md create mode 100644 remote/test/puppeteer/packages/ng-schematics/README.md create mode 100644 remote/test/puppeteer/packages/ng-schematics/package-lock.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/package.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/.puppeteerrc.cjs.template create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.e2e.ts.template create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tsconfig.json.template create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/helpers/babel.js create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/support/jasmine.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/babel.js create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/node/e2e/.gitignore.template create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts create mode 100644 remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts create mode 100644 remote/test/puppeteer/packages/ng-schematics/test/src/index.spec.ts create mode 100644 remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.js create mode 100644 remote/test/puppeteer/packages/ng-schematics/tools/sandbox.js create mode 100644 remote/test/puppeteer/packages/ng-schematics/tsconfig.json create mode 100644 remote/test/puppeteer/packages/ng-schematics/tsconfig.spec.json create mode 100644 remote/test/puppeteer/packages/puppeteer-core/.gitignore create mode 100644 remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md create mode 100644 remote/test/puppeteer/packages/puppeteer-core/api-extractor.docs.json create mode 100644 remote/test/puppeteer/packages/puppeteer-core/api-extractor.json create mode 100644 remote/test/puppeteer/packages/puppeteer-core/package.json create mode 100644 remote/test/puppeteer/packages/puppeteer-core/rollup.third_party.config.mjs create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPResponse.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/api/JSHandle.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/api/api.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Accessibility.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/AriaQueryHandler.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Binding.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Browser.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/ChromeTargetManager.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Connection.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Coverage.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Dialog.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/ElementHandle.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/EmulationManager.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/FirefoxTargetManager.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Frame.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/FrameManager.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/FrameTree.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/HTTPRequest.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/HTTPResponse.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Input.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/IsolatedWorld.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/IsolatedWorlds.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/JSHandle.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/LifecycleWatcher.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkEventManager.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManager.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/NodeWebSocketTransport.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Page.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/PredefinedNetworkConditions.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/PuppeteerViewport.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Target.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/TargetManager.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/Tracing.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/WebWorker.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BidiOverCDP.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Browser.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BrowserContext.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Connection.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Context.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/ElementHandle.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/JSHandle.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Page.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Serializer.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/bidi.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/utils.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/environment.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/generated/injected.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/generated/version.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/ARIAQuerySelector.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/CustomQuerySelector.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/PQuerySelector.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/PierceQuerySelector.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/Poller.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/TextQuerySelector.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/XPathQuerySelector.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/injected.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/node/PipeTransport.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/node/node.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/node/util/fs.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/puppeteer-core.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/templates/injected.ts.tmpl create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/templates/version.ts.tmpl create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.cjs.json create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.esm.json create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/util/DebuggableDeferredPromise.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/util/DeferredPromise.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/index.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.cjs.json create mode 100644 remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json create mode 100644 remote/test/puppeteer/packages/puppeteer-core/tools/ensure-correct-devtools-protocol-package.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/tools/generate_sources.ts create mode 100644 remote/test/puppeteer/packages/puppeteer-core/tsconfig.json create mode 100644 remote/test/puppeteer/packages/puppeteer/.gitignore create mode 100644 remote/test/puppeteer/packages/puppeteer/CHANGELOG.md create mode 100644 remote/test/puppeteer/packages/puppeteer/api-extractor.docs.json create mode 100644 remote/test/puppeteer/packages/puppeteer/api-extractor.json create mode 100644 remote/test/puppeteer/packages/puppeteer/install.js create mode 100644 remote/test/puppeteer/packages/puppeteer/package.json create mode 100644 remote/test/puppeteer/packages/puppeteer/src/getConfiguration.ts create mode 100644 remote/test/puppeteer/packages/puppeteer/src/node/install.ts create mode 100644 remote/test/puppeteer/packages/puppeteer/src/puppeteer.ts create mode 100644 remote/test/puppeteer/packages/puppeteer/src/tsconfig.cjs.json create mode 100644 remote/test/puppeteer/packages/puppeteer/src/tsconfig.esm.json create mode 100644 remote/test/puppeteer/packages/puppeteer/tsconfig.json create mode 100644 remote/test/puppeteer/packages/testserver/CHANGELOG.md create mode 100644 remote/test/puppeteer/packages/testserver/LICENSE create mode 100644 remote/test/puppeteer/packages/testserver/README.md create mode 100644 remote/test/puppeteer/packages/testserver/cert.pem create mode 100644 remote/test/puppeteer/packages/testserver/key.pem create mode 100644 remote/test/puppeteer/packages/testserver/package.json create mode 100644 remote/test/puppeteer/packages/testserver/src/index.ts create mode 100644 remote/test/puppeteer/packages/testserver/tsconfig.json create mode 100644 remote/test/puppeteer/release-please-config.json create mode 100644 remote/test/puppeteer/test-d/ElementHandle.test-d.ts create mode 100644 remote/test/puppeteer/test-d/JSHandle.test-d.ts create mode 100644 remote/test/puppeteer/test-d/NodeFor.test-d.ts create mode 100644 remote/test/puppeteer/test-d/puppeteer.test-d.ts create mode 100644 remote/test/puppeteer/test/.eslintrc.js create mode 100644 remote/test/puppeteer/test/README.md create mode 100644 remote/test/puppeteer/test/TestExpectations.json create mode 100644 remote/test/puppeteer/test/TestSuites.json create mode 100644 remote/test/puppeteer/test/assets/abort-request.html create mode 100644 remote/test/puppeteer/test/assets/beforeunload.html create mode 100644 remote/test/puppeteer/test/assets/cached/one-style-font.css create mode 100644 remote/test/puppeteer/test/assets/cached/one-style-font.html create mode 100644 remote/test/puppeteer/test/assets/cached/one-style.css create mode 100644 remote/test/puppeteer/test/assets/cached/one-style.html create mode 100644 remote/test/puppeteer/test/assets/chromium-linux.zip create mode 100644 remote/test/puppeteer/test/assets/consolelog.html create mode 100644 remote/test/puppeteer/test/assets/csp.html create mode 100644 remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf create mode 100644 remote/test/puppeteer/test/assets/csscoverage/OFL.txt create mode 100644 remote/test/puppeteer/test/assets/csscoverage/empty.html create mode 100644 remote/test/puppeteer/test/assets/csscoverage/involved.html create mode 100644 remote/test/puppeteer/test/assets/csscoverage/media.html create mode 100644 remote/test/puppeteer/test/assets/csscoverage/multiple.html create mode 100644 remote/test/puppeteer/test/assets/csscoverage/simple.html create mode 100644 remote/test/puppeteer/test/assets/csscoverage/sourceurl.html create mode 100644 remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css create mode 100644 remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css create mode 100644 remote/test/puppeteer/test/assets/csscoverage/unused.html create mode 100644 remote/test/puppeteer/test/assets/detect-touch.html create mode 100644 remote/test/puppeteer/test/assets/digits/0.png create mode 100644 remote/test/puppeteer/test/assets/digits/1.png create mode 100644 remote/test/puppeteer/test/assets/digits/2.png create mode 100644 remote/test/puppeteer/test/assets/digits/3.png create mode 100644 remote/test/puppeteer/test/assets/digits/4.png create mode 100644 remote/test/puppeteer/test/assets/digits/5.png create mode 100644 remote/test/puppeteer/test/assets/digits/6.png create mode 100644 remote/test/puppeteer/test/assets/digits/7.png create mode 100644 remote/test/puppeteer/test/assets/digits/8.png create mode 100644 remote/test/puppeteer/test/assets/digits/9.png create mode 100644 remote/test/puppeteer/test/assets/dynamic-oopif.html create mode 100644 remote/test/puppeteer/test/assets/empty.html create mode 100644 remote/test/puppeteer/test/assets/error.html create mode 100644 remote/test/puppeteer/test/assets/es6/.eslintrc create mode 100644 remote/test/puppeteer/test/assets/es6/es6import.js create mode 100644 remote/test/puppeteer/test/assets/es6/es6module.js create mode 100644 remote/test/puppeteer/test/assets/es6/es6pathimport.js create mode 100644 remote/test/puppeteer/test/assets/favicon.ico create mode 100644 remote/test/puppeteer/test/assets/file-to-upload.txt create mode 100644 remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 create mode 100644 remote/test/puppeteer/test/assets/frames/frame.html create mode 100644 remote/test/puppeteer/test/assets/frames/frameset.html create mode 100644 remote/test/puppeteer/test/assets/frames/lazy-frame.html create mode 100644 remote/test/puppeteer/test/assets/frames/nested-frames.html create mode 100644 remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html create mode 100644 remote/test/puppeteer/test/assets/frames/one-frame.html create mode 100644 remote/test/puppeteer/test/assets/frames/script.js create mode 100644 remote/test/puppeteer/test/assets/frames/style.css create mode 100644 remote/test/puppeteer/test/assets/frames/two-frames.html create mode 100644 remote/test/puppeteer/test/assets/global-var.html create mode 100644 remote/test/puppeteer/test/assets/grid.html create mode 100644 remote/test/puppeteer/test/assets/historyapi.html create mode 100644 remote/test/puppeteer/test/assets/idle-detector.html create mode 100644 remote/test/puppeteer/test/assets/initiator.html create mode 100644 remote/test/puppeteer/test/assets/initiator.js create mode 100644 remote/test/puppeteer/test/assets/injectedfile.js create mode 100644 remote/test/puppeteer/test/assets/injectedstyle.css create mode 100644 remote/test/puppeteer/test/assets/inline-svg.html create mode 100644 remote/test/puppeteer/test/assets/inner-frame1.html create mode 100644 remote/test/puppeteer/test/assets/inner-frame2.html create mode 100644 remote/test/puppeteer/test/assets/input/button.html create mode 100644 remote/test/puppeteer/test/assets/input/checkbox.html create mode 100644 remote/test/puppeteer/test/assets/input/drag-and-drop.html create mode 100644 remote/test/puppeteer/test/assets/input/fileupload.html create mode 100644 remote/test/puppeteer/test/assets/input/keyboard.html create mode 100644 remote/test/puppeteer/test/assets/input/mouse-helper.js create mode 100644 remote/test/puppeteer/test/assets/input/rotatedButton.html create mode 100644 remote/test/puppeteer/test/assets/input/scrollable.html create mode 100644 remote/test/puppeteer/test/assets/input/select.html create mode 100644 remote/test/puppeteer/test/assets/input/textarea.html create mode 100644 remote/test/puppeteer/test/assets/input/touches-move.html create mode 100644 remote/test/puppeteer/test/assets/input/touches.html create mode 100644 remote/test/puppeteer/test/assets/input/wheel.html create mode 100644 remote/test/puppeteer/test/assets/jscoverage/eval.html create mode 100644 remote/test/puppeteer/test/assets/jscoverage/involved.html create mode 100644 remote/test/puppeteer/test/assets/jscoverage/multiple.html create mode 100644 remote/test/puppeteer/test/assets/jscoverage/ranges.html create mode 100644 remote/test/puppeteer/test/assets/jscoverage/script1.js create mode 100644 remote/test/puppeteer/test/assets/jscoverage/script2.js create mode 100644 remote/test/puppeteer/test/assets/jscoverage/simple.html create mode 100644 remote/test/puppeteer/test/assets/jscoverage/sourceurl.html create mode 100644 remote/test/puppeteer/test/assets/jscoverage/unused.html create mode 100644 remote/test/puppeteer/test/assets/lazy-oopif-frame.html create mode 100644 remote/test/puppeteer/test/assets/main-frame.html create mode 100644 remote/test/puppeteer/test/assets/mobile.html create mode 100644 remote/test/puppeteer/test/assets/modernizr.js create mode 100644 remote/test/puppeteer/test/assets/networkidle.html create mode 100644 remote/test/puppeteer/test/assets/offscreenbuttons.html create mode 100644 remote/test/puppeteer/test/assets/one-style.css create mode 100644 remote/test/puppeteer/test/assets/one-style.html create mode 100644 remote/test/puppeteer/test/assets/oopif.html create mode 100644 remote/test/puppeteer/test/assets/p-selectors.html create mode 100644 remote/test/puppeteer/test/assets/pdf.html create mode 100644 remote/test/puppeteer/test/assets/playground.html create mode 100644 remote/test/puppeteer/test/assets/popup/popup.html create mode 100644 remote/test/puppeteer/test/assets/popup/window-open.html create mode 100644 remote/test/puppeteer/test/assets/pptr.png create mode 100644 remote/test/puppeteer/test/assets/resetcss.html create mode 100644 remote/test/puppeteer/test/assets/self-request.html create mode 100644 remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html create mode 100644 remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js create mode 100644 remote/test/puppeteer/test/assets/serviceworkers/extension/background.js create mode 100644 remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json create mode 100644 remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css create mode 100644 remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html create mode 100644 remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js create mode 100644 remote/test/puppeteer/test/assets/shadow.html create mode 100644 remote/test/puppeteer/test/assets/simple-extension/content-script.js create mode 100644 remote/test/puppeteer/test/assets/simple-extension/index.js create mode 100644 remote/test/puppeteer/test/assets/simple-extension/manifest.json create mode 100644 remote/test/puppeteer/test/assets/simple.json create mode 100644 remote/test/puppeteer/test/assets/tamperable.html create mode 100644 remote/test/puppeteer/test/assets/title.html create mode 100644 remote/test/puppeteer/test/assets/worker/worker.html create mode 100644 remote/test/puppeteer/test/assets/worker/worker.js create mode 100644 remote/test/puppeteer/test/assets/wrappedlink.html create mode 100644 remote/test/puppeteer/test/fixtures/closeme.js create mode 100644 remote/test/puppeteer/test/fixtures/dumpio.js create mode 100644 remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt create mode 100644 remote/test/puppeteer/test/golden-chrome/grid-cell-0.png create mode 100644 remote/test/puppeteer/test/golden-chrome/grid-cell-1.png create mode 100644 remote/test/puppeteer/test/golden-chrome/grid-cell-2.png create mode 100644 remote/test/puppeteer/test/golden-chrome/grid-cell-3.png create mode 100644 remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt create mode 100644 remote/test/puppeteer/test/golden-chrome/mock-binary-response.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png create mode 100644 remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png create mode 100644 remote/test/puppeteer/test/golden-chrome/transparent.png create mode 100644 remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png create mode 100644 remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png create mode 100644 remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png create mode 100644 remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png create mode 100644 remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png create mode 100644 remote/test/puppeteer/test/golden-chrome/white.jpg create mode 100644 remote/test/puppeteer/test/golden-firefox/grid-cell-0.png create mode 100644 remote/test/puppeteer/test/golden-firefox/grid-cell-1.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png create mode 100644 remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png create mode 100644 remote/test/puppeteer/test/installation/.mocharc.cjs create mode 100644 remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js create mode 100644 remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js create mode 100644 remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs create mode 100644 remote/test/puppeteer/test/installation/assets/puppeteer/basic.js create mode 100644 remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs create mode 100644 remote/test/puppeteer/test/installation/assets/puppeteer/imports.js create mode 100644 remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs create mode 100644 remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js create mode 100644 remote/test/puppeteer/test/installation/package.json create mode 100644 remote/test/puppeteer/test/installation/src/browsers.spec.ts create mode 100644 remote/test/puppeteer/test/installation/src/constants.ts create mode 100644 remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts create mode 100644 remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts create mode 100644 remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts create mode 100644 remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts create mode 100644 remote/test/puppeteer/test/installation/src/puppeteer.spec.ts create mode 100644 remote/test/puppeteer/test/installation/src/sandbox.ts create mode 100644 remote/test/puppeteer/test/installation/src/util.ts create mode 100644 remote/test/puppeteer/test/installation/tsconfig.json create mode 100644 remote/test/puppeteer/test/package.json create mode 100644 remote/test/puppeteer/test/src/CDPSession.spec.ts create mode 100644 remote/test/puppeteer/test/src/DeviceRequestPrompt.spec.ts create mode 100644 remote/test/puppeteer/test/src/EventEmitter.spec.ts create mode 100644 remote/test/puppeteer/test/src/NetworkManager.spec.ts create mode 100644 remote/test/puppeteer/test/src/TargetManager.spec.ts create mode 100644 remote/test/puppeteer/test/src/accessibility.spec.ts create mode 100644 remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts create mode 100644 remote/test/puppeteer/test/src/bidi/Connection.spec.ts create mode 100644 remote/test/puppeteer/test/src/browser.spec.ts create mode 100644 remote/test/puppeteer/test/src/browsercontext.spec.ts create mode 100644 remote/test/puppeteer/test/src/chromiumonly.spec.ts create mode 100644 remote/test/puppeteer/test/src/click.spec.ts create mode 100644 remote/test/puppeteer/test/src/cookies.spec.ts create mode 100644 remote/test/puppeteer/test/src/coverage.spec.ts create mode 100644 remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts create mode 100644 remote/test/puppeteer/test/src/dialog.spec.ts create mode 100644 remote/test/puppeteer/test/src/diffstyle.css create mode 100644 remote/test/puppeteer/test/src/drag-and-drop.spec.ts create mode 100644 remote/test/puppeteer/test/src/elementhandle.spec.ts create mode 100644 remote/test/puppeteer/test/src/emulation.spec.ts create mode 100644 remote/test/puppeteer/test/src/evaluation.spec.ts create mode 100644 remote/test/puppeteer/test/src/fixtures.spec.ts create mode 100644 remote/test/puppeteer/test/src/frame.spec.ts create mode 100644 remote/test/puppeteer/test/src/golden-utils.ts create mode 100644 remote/test/puppeteer/test/src/headful.spec.ts create mode 100644 remote/test/puppeteer/test/src/idle_override.spec.ts create mode 100644 remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts create mode 100644 remote/test/puppeteer/test/src/injected.spec.ts create mode 100644 remote/test/puppeteer/test/src/input.spec.ts create mode 100644 remote/test/puppeteer/test/src/jshandle.spec.ts create mode 100644 remote/test/puppeteer/test/src/keyboard.spec.ts create mode 100644 remote/test/puppeteer/test/src/launcher.spec.ts create mode 100644 remote/test/puppeteer/test/src/mocha-utils.ts create mode 100644 remote/test/puppeteer/test/src/mouse.spec.ts create mode 100644 remote/test/puppeteer/test/src/navigation.spec.ts create mode 100644 remote/test/puppeteer/test/src/network.spec.ts create mode 100644 remote/test/puppeteer/test/src/oopif.spec.ts create mode 100644 remote/test/puppeteer/test/src/page.spec.ts create mode 100644 remote/test/puppeteer/test/src/proxy.spec.ts create mode 100644 remote/test/puppeteer/test/src/queryhandler.spec.ts create mode 100644 remote/test/puppeteer/test/src/queryselector.spec.ts create mode 100644 remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts create mode 100644 remote/test/puppeteer/test/src/requestinterception.spec.ts create mode 100644 remote/test/puppeteer/test/src/screenshot.spec.ts create mode 100644 remote/test/puppeteer/test/src/target.spec.ts create mode 100644 remote/test/puppeteer/test/src/touchscreen.spec.ts create mode 100644 remote/test/puppeteer/test/src/tracing.spec.ts create mode 100644 remote/test/puppeteer/test/src/utils.ts create mode 100644 remote/test/puppeteer/test/src/waittask.spec.ts create mode 100644 remote/test/puppeteer/test/src/worker.spec.ts create mode 100644 remote/test/puppeteer/test/tsconfig.json create mode 100755 remote/test/puppeteer/tools/analyze_issue.mjs create mode 100755 remote/test/puppeteer/tools/assets/verify_issue.ts create mode 100644 remote/test/puppeteer/tools/chmod.ts create mode 100644 remote/test/puppeteer/tools/cp.ts create mode 100644 remote/test/puppeteer/tools/ensure-pinned-deps.ts create mode 100644 remote/test/puppeteer/tools/generate-matrix.js create mode 100644 remote/test/puppeteer/tools/generate_docs.ts create mode 100644 remote/test/puppeteer/tools/generate_module_package_json.ts create mode 100644 remote/test/puppeteer/tools/get_deprecated_version_range.js create mode 100644 remote/test/puppeteer/tools/internal/custom_markdown_action.ts create mode 100644 remote/test/puppeteer/tools/internal/custom_markdown_documenter.ts create mode 100644 remote/test/puppeteer/tools/internal/job.ts create mode 100644 remote/test/puppeteer/tools/internal/util.ts create mode 100644 remote/test/puppeteer/tools/mochaRunner/README.md create mode 100644 remote/test/puppeteer/tools/mochaRunner/src/interface.ts create mode 100644 remote/test/puppeteer/tools/mochaRunner/src/main.ts create mode 100644 remote/test/puppeteer/tools/mochaRunner/src/reporter.ts create mode 100644 remote/test/puppeteer/tools/mochaRunner/src/test.ts create mode 100644 remote/test/puppeteer/tools/mochaRunner/src/types.ts create mode 100644 remote/test/puppeteer/tools/mochaRunner/src/utils.ts create mode 100644 remote/test/puppeteer/tools/mochaRunner/tsconfig.json create mode 100644 remote/test/puppeteer/tools/remove_version_suffix.js create mode 100644 remote/test/puppeteer/tools/sort-test-expectations.js create mode 100644 remote/test/puppeteer/tools/third_party/validate-licenses.ts create mode 100644 remote/test/puppeteer/tools/tsconfig.json create mode 100644 remote/test/puppeteer/tsconfig.base.json create mode 100644 remote/test/puppeteer/versions.js (limited to 'remote/test') 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..1700c34b9f --- /dev/null +++ b/remote/test/puppeteer/.eslintignore @@ -0,0 +1,46 @@ +## [START] Keep in sync with .gitignore +# Dependencies +node_modules + +# Production +build/ +lib/ + +# 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/ diff --git a/remote/test/puppeteer/.eslintplugin.js b/remote/test/puppeteer/.eslintplugin.js new file mode 100644 index 0000000000..0e61040f45 --- /dev/null +++ b/remote/test/puppeteer/.eslintplugin.js @@ -0,0 +1,95 @@ +const prettier = require('prettier'); + +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, prettierOptions) => { + return prettier + .format(value, { + ...prettierOptions, + // This is the print width minus 3 (the length of ` * `) and the offset. + printWidth: prettierOptions.printWidth - (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 rule = { + meta: { + type: 'suggestion', + docs: { + description: 'Enforce Prettier formatting on comments', + recommended: false, + }, + fixable: 'code', + schema: [], + messages: {}, + }, + + create(context) { + const prettierOptions = { + printWidth: 80, + ...prettier.resolveConfig.sync(context.getPhysicalFilename()), + parser: 'markdown', + }; + for (const comment of context.getSourceCode().getAllComments()) { + switch (comment.type) { + case 'Block': { + const offset = comment.loc.start.column; + const value = cleanupBlockComment(comment.value); + const formattedValue = format(value, offset, prettierOptions); + 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 = { + rules: { + 'prettier-comments': rule, + }, +}; diff --git a/remote/test/puppeteer/.eslintrc.js b/remote/test/puppeteer/.eslintrc.js new file mode 100644 index 0000000000..7de1595db1 --- /dev/null +++ b/remote/test/puppeteer/.eslintrc.js @@ -0,0 +1,205 @@ +module.exports = { + root: true, + env: { + node: true, + es6: true, + }, + + parser: '@typescript-eslint/parser', + + plugins: ['mocha', '@typescript-eslint', 'import'], + + extends: ['plugin:prettier/recommended'], + + 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', + + 'no-restricted-imports': [ + 'error', + { + patterns: ['*Events'], + paths: [ + { + name: 'mitt', + message: + 'Import `mitt` from the vendored location: third_party/mitt/index.js', + }, + ], + }, + ], + 'import/extensions': ['error', 'ignorePackages'], + + 'import/order': [ + 'error', + { + 'newlines-between': 'always', + alphabetize: {order: 'asc', caseInsensitive: true}, + }, + ], + + 'no-restricted-syntax': [ + 'error', + // Don't allow underscored declarations on camelCased variables/properties. + // ...RESTRICTED_UNDERSCORED_IDENTIFIERS, + ], + }, + overrides: [ + { + files: ['*.ts'], + parserOptions: { + allowAutomaticSingleRunInference: true, + project: './tsconfig.base.json', + }, + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + ], + plugins: ['eslint-plugin-tsdoc', 'local'], + rules: { + // Keeps comments formatted. + 'local/prettier-comments': '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: '^_'}, + ], + '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', + /** + * 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`.', + }, + ], + '@typescript-eslint/no-floating-promises': [ + 'error', + {ignoreVoid: true, ignoreIIFE: true}, + ], + }, + }, + ], +}; 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..54fae5d77b --- /dev/null +++ b/remote/test/puppeteer/.mocharc.cjs @@ -0,0 +1,27 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +module.exports = { + reporter: 'dot', + logLevel: 'debug', + require: ['./test/build/mocha-utils.js', 'source-map-support/register'], + spec: 'test/build/**/*.spec.js', + exit: !!process.env.CI, + retries: process.env.CI ? 3 : 0, + parallel: !!process.env.PARALLEL, + timeout: 10_000, + reporter: process.env.CI ? 'spec' : 'dot', +}; 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/.prettierignore b/remote/test/puppeteer/.prettierignore new file mode 100644 index 0000000000..7a08fe069b --- /dev/null +++ b/remote/test/puppeteer/.prettierignore @@ -0,0 +1,53 @@ +## [START] Keep in sync with .gitignore +# Dependencies +node_modules + +# Production +build/ +lib/ + +# 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 +versioned_*/ + +# Ng-schematics +/packages/ng-schematics/files/ +/packages/ng-schematics/sandbox/ \ No newline at end of file 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..ebd5be1459 --- /dev/null +++ b/remote/test/puppeteer/.release-please-manifest.json @@ -0,0 +1,7 @@ +{ + "packages/puppeteer": "20.1.0", + "packages/puppeteer-core": "20.1.0", + "packages/testserver": "0.6.0", + "packages/ng-schematics": "0.2.0", + "packages/browsers": "1.0.0" +} 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..a7df543c82 --- /dev/null +++ b/remote/test/puppeteer/README.md @@ -0,0 +1,253 @@ +# 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) + + + +#### [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://goo.gle/chrome-for-testing) (~170MB macOS, ~282MB Linux, ~280MB Windows) 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). + +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). + +#### 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` + +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/api/puppeteer.browserfetcher). +If you are managing browsers yourself, you will need to call +[`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) with +an 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 () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + await page.goto('https://developer.chrome.com/'); + + // Set screen size + await page.setViewport({width: 1080, height: 1024}); + + // Type into search box + await page.type('.search-box__input', 'automate beyond recorder'); + + // Wait and click on first result + const searchResultSelector = '.search-box__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/commitlint.config.js b/remote/test/puppeteer/commitlint.config.js new file mode 100644 index 0000000000..af86dc9735 --- /dev/null +++ b/remote/test/puppeteer/commitlint.config.js @@ -0,0 +1,28 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +// See https://github.com/conventional-changelog/commitlint/blob/master/docs/reference-rules.md +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + // Override. The subject may be the name of a class. + 'subject-case': [0], + // Override. Most UIs wrap the body. + 'body-max-line-length': [0], + // Override. Most UIs wrap the footer. + 'footer-max-line-length': [0], + }, +}; diff --git a/remote/test/puppeteer/examples/README.md b/remote/test/puppeteer/examples/README.md new file mode 100644 index 0000000000..9dd85c6ba7 --- /dev/null +++ b/remote/test/puppeteer/examples/README.md @@ -0,0 +1,42 @@ +# Running the examples + +Assuming you have a checkout of the Puppeteer repo and have run npm i (or yarn) to install the dependencies, 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 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..5027afb875 --- /dev/null +++ b/remote/test/puppeteer/examples/cross-browser.js @@ -0,0 +1,48 @@ +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..cedf33e999 --- /dev/null +++ b/remote/test/puppeteer/examples/custom-event.js @@ -0,0 +1,50 @@ +/** + * Copyright 2017 Google Inc. 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(); + + // 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..a8631abc54 --- /dev/null +++ b/remote/test/puppeteer/examples/oopif.js @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Google Inc. 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 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..9f7ac92e0a --- /dev/null +++ b/remote/test/puppeteer/examples/pdf.js @@ -0,0 +1,35 @@ +/** + * Copyright 2017 Google Inc. 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.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..5490822b8b --- /dev/null +++ b/remote/test/puppeteer/examples/proxy.js @@ -0,0 +1,35 @@ +/** + * Copyright 2017 Google Inc. 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({ + // 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..95f469eb7f --- /dev/null +++ b/remote/test/puppeteer/examples/screenshot-fullpage.js @@ -0,0 +1,28 @@ +/** + * Copyright 2017 Google Inc. 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.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..7d54806c4c --- /dev/null +++ b/remote/test/puppeteer/examples/screenshot.js @@ -0,0 +1,27 @@ +/** + * Copyright 2017 Google Inc. 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.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..df0abc213b --- /dev/null +++ b/remote/test/puppeteer/examples/search.js @@ -0,0 +1,55 @@ +/** + * Copyright 2017 Google Inc. 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. + */ + +/** + * @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..abedd615f6 --- /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-v20.1.0 + url: https://github.com/puppeteer/puppeteer.git +schema: 1 diff --git a/remote/test/puppeteer/package-lock.json b/remote/test/puppeteer/package-lock.json new file mode 100644 index 0000000000..133066234d --- /dev/null +++ b/remote/test/puppeteer/package-lock.json @@ -0,0 +1,16194 @@ +{ + "name": "puppeteer-repo", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "puppeteer-repo", + "hasInstallScript": true, + "workspaces": [ + "packages/*", + "test", + "test/installation" + ], + "devDependencies": { + "@actions/core": "1.10.0", + "@commitlint/cli": "17.6.1", + "@commitlint/config-conventional": "17.6.1", + "@microsoft/api-documenter": "7.21.7", + "@microsoft/api-extractor": "7.34.4", + "@microsoft/api-extractor-model": "7.26.4", + "@pptr/testserver": "file:packages/testserver", + "@rollup/plugin-commonjs": "24.0.1", + "@rollup/plugin-node-resolve": "15.0.1", + "@types/debug": "4.1.7", + "@types/diff": "5.0.2", + "@types/mime": "3.0.1", + "@types/mocha": "10.0.1", + "@types/node": "18.14.6", + "@types/pixelmatch": "5.2.4", + "@types/pngjs": "6.0.1", + "@types/progress": "2.0.5", + "@types/proxy-from-env": "1.0.1", + "@types/semver": "7.3.13", + "@types/sinon": "10.0.13", + "@types/tar-fs": "2.0.1", + "@types/unbzip2-stream": "1.4.0", + "@types/ws": "8.5.4", + "@typescript-eslint/eslint-plugin": "5.59.1", + "@typescript-eslint/parser": "5.59.1", + "c8": "7.13.0", + "commitlint": "17.6.1", + "commonmark": "0.30.0", + "cross-env": "7.0.3", + "diff": "5.1.0", + "esbuild": "^0.17.11", + "eslint": "8.39.0", + "eslint-config-prettier": "8.8.0", + "eslint-formatter-codeframe": "7.32.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-local": "1.0.0", + "eslint-plugin-mocha": "10.1.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-tsdoc": "0.2.17", + "eslint-plugin-unused-imports": "2.0.0", + "esprima": "4.0.1", + "expect": "29.4.3", + "glob": "8.1.0", + "gts": "4.0.0", + "husky": "8.0.3", + "jpeg-js": "0.4.4", + "license-checker": "25.0.1", + "mime": "3.0.0", + "minimist": "1.2.8", + "mocha": "10.2.0", + "ncp": "2.0.0", + "npm-run-all": "4.1.5", + "pixelmatch": "5.3.0", + "pngjs": "7.0.0", + "prettier": "2.8.8", + "puppeteer": "file:packages/puppeteer", + "rimraf": "3.0.2", + "rollup": "3.18.0", + "semver": "7.3.8", + "sinon": "15.0.1", + "source-map-support": "0.5.21", + "spdx-satisfies": "5.0.1", + "text-diff": "1.0.1", + "tsd": "0.26.0", + "tsx": "3.12.3", + "typescript": "4.9.5", + "wireit": "0.9.5", + "zod": "3.21.2" + } + }, + "node_modules/@actions/core": { + "version": "1.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/http-client": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1502.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.7.tgz", + "integrity": "sha512-MzB6D/yUo6cBJfQ31zNDHJ3C3iKmBtxP3i9WIRnnkZwS1VUfO8OX3TZ6lycYbREF1oL/AQ/r9GK+KA5DNEBSAw==", + "dependencies": { + "@angular-devkit/core": "15.2.7", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.7.tgz", + "integrity": "sha512-k2MKUm4ygTD9+89neqMmBphDr0o8Tp9RtgfzbS8VHgGkGYlbu0KPsxHyHB3Mvzl1EkSz6EHyrU3t89m+Rcj1lw==", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/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" + } + }, + "node_modules/@angular-devkit/core/node_modules/source-map": { + "version": "0.7.4", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.7.tgz", + "integrity": "sha512-umQ+SgEMjqPHimHOBVhDn5NNGVoMLKQkI2fwbENXV72BqQqdh1K3D4QSNlUXitTaH0NEZZaAawE1vZHzzeAoNA==", + "dependencies": { + "@angular-devkit/core": "15.2.7", + "jsonc-parser": "3.2.0", + "magic-string": "0.29.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/cli": { + "version": "15.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1502.2", + "@angular-devkit/core": "15.2.2", + "@angular-devkit/schematics": "15.2.2", + "@schematics/angular": "15.2.2", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.1", + "ora": "5.4.1", + "pacote": "15.1.0", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1502.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.2.tgz", + "integrity": "sha512-+NE2IV+tuPgcBdC+1ac7eYIBqZDH0VskXTqbhHiRKySbK0vF3/cwTw6Ie07phl0xC1dxLXeRE52L5YwX5jERFQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "15.2.2", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.2.tgz", + "integrity": "sha512-YPwDSldpVcuSJuIkXy5iRzaPA78ySXKc80OicHR6XtMsrHlwY7DuxQoSWr+ih9LAqpeeBz9ECMalztwohdy0MA==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/schematics": { + "version": "15.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.2.2", + "jsonc-parser": "3.2.0", + "magic-string": "0.29.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@schematics/angular": { + "version": "15.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.2.2", + "@angular-devkit/schematics": "15.2.2", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "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/@angular/cli/node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular/cli/node_modules/ini": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@angular/cli/node_modules/inquirer": { + "version": "8.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@angular/cli/node_modules/inquirer/node_modules/rxjs": { + "version": "7.8.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/cli/node_modules/magic-string": { + "version": "0.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/cli/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@angular/cli/node_modules/tslib": { + "version": "2.5.0", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular/cli/node_modules/yargs": { + "version": "17.6.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" + } + }, + "node_modules/@angular/cli/node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "license": "MIT", + "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", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@commitlint/cli": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.6.1.tgz", + "integrity": "sha512-kCnDD9LE2ySiTnj/VPaxy4/oRayRcdv4aCuVxtoum8SxIU7OADHc0nJPQfheE8bHcs3zZdWzDMWltRosuT13bg==", + "dev": true, + "dependencies": { + "@commitlint/format": "^17.4.4", + "@commitlint/lint": "^17.6.1", + "@commitlint/load": "^17.5.0", + "@commitlint/read": "^17.5.1", + "@commitlint/types": "^17.4.4", + "execa": "^5.0.0", + "lodash.isfunction": "^3.0.9", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.6.1.tgz", + "integrity": "sha512-ng/ybaSLuTCH9F+7uavSOnEQ9EFMl7lHEjfAEgRh1hwmEe8SpLKpQeMo2aT1IWvHaGMuTb+gjfbzoRf2IR23NQ==", + "dev": true, + "dependencies": { + "conventional-changelog-conventionalcommits": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.4.4.tgz", + "integrity": "sha512-bi0+TstqMiqoBAQDvdEP4AFh0GaKyLFlPPEObgI29utoKEYoPQTvF0EYqIwYYLEoJYhj5GfMIhPHJkTJhagfeg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.4.4", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/ensure": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-17.4.4.tgz", + "integrity": "sha512-AHsFCNh8hbhJiuZ2qHv/m59W/GRE9UeOXbkOqxYMNNg9pJ7qELnFcwj5oYpa6vzTSHtPGKf3C2yUFNy1GGHq6g==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.4.4", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-17.4.0.tgz", + "integrity": "sha512-LIgYXuCSO5Gvtc0t9bebAMSwd68ewzmqLypqI2Kke1rqOqqDbMpYcYfoPfFlv9eyLIh4jocHWwCK5FS7z9icUA==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/format": { + "version": "17.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^17.4.4", + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.4.4.tgz", + "integrity": "sha512-Y3eo1SFJ2JQDik4rWkBC4tlRIxlXEFrRWxcyrzb1PUT2k3kZ/XGNuCDfk/u0bU2/yS0tOA/mTjFsV+C4qyACHw==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.4.4", + "semver": "7.3.8" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/lint": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.6.1.tgz", + "integrity": "sha512-VARJ9kxH64isgwVnC+ABPafCYzqxpsWJIpDaTuI0gh8aX4GQ0i7cn9tvxtFNfJj4ER2BAJeWJ0vURdNYjK2RQQ==", + "dev": true, + "dependencies": { + "@commitlint/is-ignored": "^17.4.4", + "@commitlint/parse": "^17.4.4", + "@commitlint/rules": "^17.6.1", + "@commitlint/types": "^17.4.4" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/load": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.5.0.tgz", + "integrity": "sha512-l+4W8Sx4CD5rYFsrhHH8HP01/8jEP7kKf33Xlx2Uk2out/UKoKPYMOIRcDH5ppT8UXLMV+x6Wm5osdRKKgaD1Q==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^17.4.4", + "@commitlint/execute-rule": "^17.4.0", + "@commitlint/resolve-extends": "^17.4.4", + "@commitlint/types": "^17.4.4", + "@types/node": "*", + "chalk": "^4.1.0", + "cosmiconfig": "^8.0.0", + "cosmiconfig-typescript-loader": "^4.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "resolve-from": "^5.0.0", + "ts-node": "^10.8.1", + "typescript": "^4.6.4 || ^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/message": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-17.4.2.tgz", + "integrity": "sha512-3XMNbzB+3bhKA1hSAWPCQA3lNxR4zaeQAQcHj0Hx5sVdO6ryXtgUBGGv+1ZCLMgAPRixuc6en+iNAzZ4NzAa8Q==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/parse": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-17.4.4.tgz", + "integrity": "sha512-EKzz4f49d3/OU0Fplog7nwz/lAfXMaDxtriidyGF9PtR+SRbgv4FhsfF310tKxs6EPj8Y+aWWuX3beN5s+yqGg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.4.4", + "conventional-changelog-angular": "^5.0.11", + "conventional-commits-parser": "^3.2.2" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/read": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-17.5.1.tgz", + "integrity": "sha512-7IhfvEvB//p9aYW09YVclHbdf1u7g7QhxeYW9ZHSO8Huzp8Rz7m05aCO1mFG7G8M+7yfFnXB5xOmG18brqQIBg==", + "dev": true, + "dependencies": { + "@commitlint/top-level": "^17.4.0", + "@commitlint/types": "^17.4.4", + "fs-extra": "^11.0.0", + "git-raw-commits": "^2.0.11", + "minimist": "^1.2.6" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.4.4.tgz", + "integrity": "sha512-znXr1S0Rr8adInptHw0JeLgumS11lWbk5xAWFVno+HUFVN45875kUtqjrI6AppmD3JI+4s0uZlqqlkepjJd99A==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^17.4.4", + "@commitlint/types": "^17.4.4", + "import-fresh": "^3.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/rules": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-17.6.1.tgz", + "integrity": "sha512-lUdHw6lYQ1RywExXDdLOKxhpp6857/4c95Dc/1BikrHgdysVUXz26yV0vp1GL7Gv+avx9WqZWTIVB7pNouxlfw==", + "dev": true, + "dependencies": { + "@commitlint/ensure": "^17.4.4", + "@commitlint/message": "^17.4.2", + "@commitlint/to-lines": "^17.4.0", + "@commitlint/types": "^17.4.4", + "execa": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-17.4.0.tgz", + "integrity": "sha512-LcIy/6ZZolsfwDUWfN1mJ+co09soSuNASfKEU5sCmgFCvX5iHwRYLiIuoqXzOVDYOy7E7IcHilr/KS0e5T+0Hg==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/top-level": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-17.4.0.tgz", + "integrity": "sha512-/1loE/g+dTTQgHnjoCy0AexKAEFyHsR2zRB4NWrZ6lZSMIxAhBJnmCqwao7b4H8888PsfoTBCLBYIw8vGnej8g==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/types": { + "version": "17.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/cjs-loader": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.17.6", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.5.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz", + "integrity": "sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.11.tgz", + "integrity": "sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.11.tgz", + "integrity": "sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.11.tgz", + "integrity": "sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.11.tgz", + "integrity": "sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.11.tgz", + "integrity": "sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.11.tgz", + "integrity": "sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.11.tgz", + "integrity": "sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.11.tgz", + "integrity": "sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.11.tgz", + "integrity": "sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.11.tgz", + "integrity": "sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.11.tgz", + "integrity": "sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.11.tgz", + "integrity": "sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.11.tgz", + "integrity": "sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.11.tgz", + "integrity": "sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.11", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.11.tgz", + "integrity": "sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.11.tgz", + "integrity": "sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.11.tgz", + "integrity": "sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.11.tgz", + "integrity": "sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.11.tgz", + "integrity": "sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz", + "integrity": "sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==", + "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.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", + "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.1", + "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/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/@eslint/eslintrc/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==", + "dev": true + }, + "node_modules/@eslint/eslintrc/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==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/js": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.4.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.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@microsoft/api-documenter": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.21.7.tgz", + "integrity": "sha512-qlCJ9dSefL6Rmuv1FKtxg7i6N7Bv6LUPrA5Bb0dR/9Ffac2xQuXtuSXOJ09seM7G0WkpQOzta2Kc1CPixzDaIw==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "js-yaml": "~3.13.1", + "resolve": "~1.22.1" + }, + "bin": { + "api-documenter": "bin/api-documenter" + } + }, + "node_modules/@microsoft/api-extractor": { + "version": "7.34.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.8.4" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.26.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "4.8.4", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "dev": true, + "license": "MIT", + "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/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "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/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "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/move-file": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pptr/testserver": { + "resolved": "packages/testserver", + "link": true + }, + "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/ng-schematics": { + "resolved": "packages/ng-schematics", + "link": true + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "24.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.0", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/@types/resolve": { + "version": "1.20.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/node-core-library": { + "version": "3.55.2", + "dev": true, + "license": "MIT", + "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.3.0", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.3.18", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.13.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "node_modules/@schematics/angular": { + "version": "14.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "14.2.8", + "@angular-devkit/schematics": "14.2.8", + "jsonc-parser": "3.1.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "14.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": { + "version": "14.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.8.tgz", + "integrity": "sha512-L5GEgueZV4vqZy9Ar0zxVJOHK/4ttF1nPjW4Ut1vRFJGxsHFVEpxq5eGBf2JYSiOhqmFYc6GnJOxA6C4xAIHjA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "14.2.8", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/jsonc-parser": { + "version": "3.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@schematics/angular/node_modules/magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@schematics/angular/node_modules/source-map": { + "version": "0.7.4", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "2.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "7.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@tsd/typescript": { + "version": "4.9.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/models": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^6.1.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "6.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/diff": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.14.6", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/pixelmatch": { + "version": "5.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pngjs": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/progress": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/proxy-from-env": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.3.13", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sinon": { + "version": "10.0.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tar-fs": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tar-stream": "*" + } + }, + "node_modules/@types/tar-stream": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/through": { + "version": "0.0.30", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unbzip2-stream": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/through": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.22", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "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/@typescript-eslint/parser": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", + "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/@typescript-eslint/scope-manager": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", + "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/@typescript-eslint/types": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", + "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/@typescript-eslint/utils": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "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.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", + "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/@typescript-eslint/visitor-keys": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.1", + "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/@yarnpkg/lockfile": { + "version": "1.1.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.8.1", + "dev": true, + "license": "MIT", + "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/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.11.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" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "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.2", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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-ify": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "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.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "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/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/builtins": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/c8": { + "version": "7.13.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "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/cacache": { + "version": "17.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "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", + "license": "MIT", + "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", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "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", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "license": "ISC" + }, + "node_modules/chromium-bidi": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", + "integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", + "dependencies": { + "mitt": "3.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.7.0", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "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" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colors": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/commitlint": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/commitlint/-/commitlint-17.6.1.tgz", + "integrity": "sha512-yO11o5DmN/X4VCL+aLzgfJ1YXOM7qFzMN659SpISS4EBiv+QO16A0jeJU9rgVRbM2K06M7AfBQkZz7EPR3sAnQ==", + "dev": true, + "dependencies": { + "@commitlint/cli": "^17.6.1", + "@commitlint/types": "^17.4.4" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/commonmark": { + "version": "0.30.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "entities": "~2.0", + "mdurl": "~1.0.1", + "minimist": ">=1.2.2", + "string.prototype.repeat": "^0.2.0" + }, + "bin": { + "commonmark": "bin/commonmark" + }, + "engines": { + "node": "*" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/conventional-changelog-angular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", + "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", + "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", + "dev": true, + "dependencies": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", + "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=7", + "ts-node": ">=10", + "typescript": ">=3" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-env": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "license": "MIT", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "license": "MIT", + "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==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1120988", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", + "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==" + }, + "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.1.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "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", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "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-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "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.17.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.11.tgz", + "integrity": "sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.11", + "@esbuild/android-arm64": "0.17.11", + "@esbuild/android-x64": "0.17.11", + "@esbuild/darwin-arm64": "0.17.11", + "@esbuild/darwin-x64": "0.17.11", + "@esbuild/freebsd-arm64": "0.17.11", + "@esbuild/freebsd-x64": "0.17.11", + "@esbuild/linux-arm": "0.17.11", + "@esbuild/linux-arm64": "0.17.11", + "@esbuild/linux-ia32": "0.17.11", + "@esbuild/linux-loong64": "0.17.11", + "@esbuild/linux-mips64el": "0.17.11", + "@esbuild/linux-ppc64": "0.17.11", + "@esbuild/linux-riscv64": "0.17.11", + "@esbuild/linux-s390x": "0.17.11", + "@esbuild/linux-x64": "0.17.11", + "@esbuild/netbsd-x64": "0.17.11", + "@esbuild/openbsd-x64": "0.17.11", + "@esbuild/sunos-x64": "0.17.11", + "@esbuild/win32-arm64": "0.17.11", + "@esbuild/win32-ia32": "0.17.11", + "@esbuild/win32-x64": "0.17.11" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.39.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "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.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.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", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "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.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "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": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-formatter-codeframe": { + "version": "7.32.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.12.11", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint-formatter-pretty": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "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-import-resolver-node": { + "version": "0.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.4", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.27.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-local": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-mocha": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^3.0.0", + "rambda": "^7.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "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/eslint-utils": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-tsdoc": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-rule-docs": { + "version": "1.1.235", + "dev": true, + "license": "MIT" + }, + "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/eslint-utils": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "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/eslint/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "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/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "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/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "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/expect": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.4.3", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.4.3", + "jest-message-util": "^29.4.3", + "jest-util": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "license": "BSD-2-Clause", + "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", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.13.0", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "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.4.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/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==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "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/graceful-fs": { + "version": "4.2.10", + "dev": true, + "license": "ISC" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/gts": { + "version": "4.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "chalk": "^4.1.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^8.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "~2.7.0", + "rimraf": "^3.0.2", + "write-file-atomic": "^4.0.0" + }, + "bin": { + "gts": "build/src/cli.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, + "node_modules/gts/node_modules/meow": { + "version": "9.0.0", + "dev": true, + "license": "MIT", + "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/gts/node_modules/prettier": { + "version": "2.7.1", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/gts/node_modules/type-fest": { + "version": "0.18.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/he": { + "version": "1.2.0", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "dev": true, + "license": "MIT", + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^6.1.6" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "6.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inquirer": { + "version": "7.3.3", + "dev": true, + "license": "MIT", + "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.3", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/irregular-plurals": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "license": "MIT" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "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-builtin-module": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-module": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.4.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.4.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@babel/code-frame": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/jest-util": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.4.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", + "dev": true, + "license": "MIT" + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-sdsl": { + "version": "4.1.5", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/just-extend": { + "version": "4.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "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/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/license-checker/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/license-checker/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "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", + "license": "MIT" + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "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/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "license": "MIT", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "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", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minipass": { + "version": "4.2.4", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "license": "MIT" + }, + "node_modules/mocha": { + "version": "10.2.0", + "license": "MIT", + "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/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "license": "ISC", + "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/minimatch": { + "version": "3.1.2", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "license": "MIT", + "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": { + "version": "16.2.0", + "license": "MIT", + "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/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "dev": true, + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/nise": { + "version": "5.1.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "7.1.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "1.8.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "license": "MIT", + "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": "9.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "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/nopt": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "dev": true, + "license": "BSD-2-Clause", + "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", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "6.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^4.0.0", + "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/npm-run-all": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.1", + "dev": true, + "license": "MIT", + "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.1", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "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", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "15.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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" + } + }, + "node_modules/parsel-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.0.tgz", + "integrity": "sha512-+CAY5A3p8b6he3OzlY/naXpeeiLMjEqFUyMwiPrwnemG5yh0/sgygYMKRhtn6/YSriyy4KZwQLnpBfs36GnXUg==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/plur": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.4.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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "resolved": "packages/puppeteer", + "link": true + }, + "node_modules/puppeteer-core": { + "resolved": "packages/puppeteer-core", + "link": true + }, + "node_modules/q": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "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" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rambda": { + "version": "7.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "dev": true, + "license": "MIT" + }, + "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/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-installed/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-installed/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-installed/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/read-installed/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-installed/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-package-json": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^8.0.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.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": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "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.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/hosted-git-info": { + "version": "6.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/read-package-json/node_modules/normalize-package-data": { + "version": "5.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "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", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/read-pkg": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "license": "MIT", + "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", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.9.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": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "dependencies": { + "global-dirs": "^0.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "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/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/rollup": { + "version": "3.18.0", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "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" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.3.8", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.7.4", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "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": "3.0.7", + "license": "ISC" + }, + "node_modules/sigstore": { + "version": "1.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "make-fetch-happen": "^11.0.1", + "tuf-js": "^1.0.0" + }, + "bin": { + "sigstore": "bin/sigstore.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/sigstore/node_modules/make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/minipass-fetch": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^4.0.0", + "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/sinon": { + "version": "15.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "10.0.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "dev": true, + "license": "MIT", + "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": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "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.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.12", + "dev": true, + "license": "CC0-1.0" + }, + "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/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "10.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "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.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "0.2.0", + "dev": true + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tar": { + "version": "6.1.13", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "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/text-diff": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "license": "MIT" + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsd": { + "version": "0.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tsd/typescript": "~4.9.5", + "eslint-formatter-pretty": "^4.1.0", + "globby": "^11.0.1", + "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/tsd/node_modules/meow": { + "version": "9.0.0", + "dev": true, + "license": "MIT", + "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/tsd/node_modules/type-fest": { + "version": "0.18.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "dev": true, + "license": "MIT", + "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": "3.12.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/cjs-loader": "^2.4.2", + "@esbuild-kit/core-utils": "^3.0.0", + "@esbuild-kit/esm-loader": "^2.5.5" + }, + "bin": { + "tsx": "dist/cli.js" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tuf-js": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "1.0.0", + "make-fetch-happen": "^11.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/tuf-js/node_modules/make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/minipass-fetch": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^4.0.0", + "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/tunnel": { + "version": "0.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "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/typescript": { + "version": "4.9.5", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "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", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "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", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.16", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/validator": { + "version": "13.9.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "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/wide-align": { + "version": "1.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wireit": { + "version": "0.9.5", + "dev": true, + "license": "Apache-2.0", + "workspaces": [ + "vscode-extension", + "website" + ], + "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/word-wrap": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "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" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "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", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.1", + "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" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "license": "MIT", + "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", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "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.21.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/browsers": { + "name": "@puppeteer/browsers", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "devDependencies": { + "@types/node": "^14.15.0", + "@types/yargs": "17.0.22" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/browsers/node_modules/@types/node": { + "version": "14.18.36", + "dev": true, + "license": "MIT" + }, + "packages/ng-schematics": { + "name": "@puppeteer/ng-schematics", + "version": "0.2.0", + "license": "Apache-2.0", + "dependencies": { + "@angular-devkit/architect": "^0.1502.7", + "@angular-devkit/core": "^15.2.7", + "@angular-devkit/schematics": "^15.2.7" + }, + "devDependencies": { + "@angular/cli": "^15.2.2", + "@schematics/angular": "^14.2.8", + "@types/node": "^14.15.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/ng-schematics/node_modules/@types/node": { + "version": "14.18.33", + "dev": true, + "license": "MIT" + }, + "packages/puppeteer": { + "version": "20.1.0", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "1.0.0", + "cosmiconfig": "8.1.3", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "20.1.0" + } + }, + "packages/puppeteer-core": { + "version": "20.1.0", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "1.0.0", + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1120988", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0" + }, + "devDependencies": { + "mitt": "3.0.0", + "parsel-js": "1.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/testserver": { + "name": "@pptr/testserver", + "version": "0.6.0", + "license": "Apache-2.0", + "dependencies": { + "mime": "3.0.0", + "ws": "8.13.0" + } + }, + "test": { + "name": "@puppeteer-test/test", + "version": "latest", + "dependencies": { + "puppeteer": "file:../packages/puppeteer" + } + }, + "test/installation": { + "name": "@puppeteer-test/installation", + "version": "latest", + "dependencies": { + "glob": "8.1.0", + "mocha": "10.2.0" + }, + "devDependencies": { + "@types/glob": "8.1.0" + } + }, + "test/installation/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "test/installation/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "test/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" + } + } + }, + "dependencies": { + "@actions/core": { + "version": "1.10.0", + "dev": true, + "requires": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "@actions/http-client": { + "version": "2.0.1", + "dev": true, + "requires": { + "tunnel": "^0.0.6" + } + }, + "@angular-devkit/architect": { + "version": "0.1502.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.7.tgz", + "integrity": "sha512-MzB6D/yUo6cBJfQ31zNDHJ3C3iKmBtxP3i9WIRnnkZwS1VUfO8OX3TZ6lycYbREF1oL/AQ/r9GK+KA5DNEBSAw==", + "requires": { + "@angular-devkit/core": "15.2.7", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.7.tgz", + "integrity": "sha512-k2MKUm4ygTD9+89neqMmBphDr0o8Tp9RtgfzbS8VHgGkGYlbu0KPsxHyHB3Mvzl1EkSz6EHyrU3t89m+Rcj1lw==", + "requires": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "source-map": { + "version": "0.7.4" + } + } + }, + "@angular-devkit/schematics": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.7.tgz", + "integrity": "sha512-umQ+SgEMjqPHimHOBVhDn5NNGVoMLKQkI2fwbENXV72BqQqdh1K3D4QSNlUXitTaH0NEZZaAawE1vZHzzeAoNA==", + "requires": { + "@angular-devkit/core": "15.2.7", + "jsonc-parser": "3.2.0", + "magic-string": "0.29.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "dependencies": { + "magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + } + } + }, + "@angular/cli": { + "version": "15.2.2", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1502.2", + "@angular-devkit/core": "15.2.2", + "@angular-devkit/schematics": "15.2.2", + "@schematics/angular": "15.2.2", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.1", + "ora": "5.4.1", + "pacote": "15.1.0", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1502.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.2.tgz", + "integrity": "sha512-+NE2IV+tuPgcBdC+1ac7eYIBqZDH0VskXTqbhHiRKySbK0vF3/cwTw6Ie07phl0xC1dxLXeRE52L5YwX5jERFQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "15.2.2", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.2.tgz", + "integrity": "sha512-YPwDSldpVcuSJuIkXy5iRzaPA78ySXKc80OicHR6XtMsrHlwY7DuxQoSWr+ih9LAqpeeBz9ECMalztwohdy0MA==", + "dev": true, + "requires": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + } + }, + "@angular-devkit/schematics": { + "version": "15.2.2", + "dev": true, + "requires": { + "@angular-devkit/core": "15.2.2", + "jsonc-parser": "3.2.0", + "magic-string": "0.29.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + } + }, + "@schematics/angular": { + "version": "15.2.2", + "dev": true, + "requires": { + "@angular-devkit/core": "15.2.2", + "@angular-devkit/schematics": "15.2.2", + "jsonc-parser": "3.2.0" + } + }, + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.3", + "dev": true + }, + "ini": { + "version": "3.0.1", + "dev": true + }, + "inquirer": { + "version": "8.2.4", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "rxjs": { + "version": "7.8.0", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "magic-string": { + "version": "0.29.0", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + }, + "tslib": { + "version": "2.5.0", + "dev": true + }, + "yargs": { + "version": "17.6.2", + "dev": true, + "requires": { + "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" + } + }, + "yargs-parser": { + "version": "21.1.1", + "dev": true + } + } + }, + "@babel/code-frame": { + "version": "7.12.11", + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1" + }, + "@babel/highlight": { + "version": "7.18.6", + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3" + }, + "escape-string-regexp": { + "version": "1.0.5" + }, + "has-flag": { + "version": "3.0.0" + }, + "supports-color": { + "version": "5.5.0", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true + }, + "@commitlint/cli": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.6.1.tgz", + "integrity": "sha512-kCnDD9LE2ySiTnj/VPaxy4/oRayRcdv4aCuVxtoum8SxIU7OADHc0nJPQfheE8bHcs3zZdWzDMWltRosuT13bg==", + "dev": true, + "requires": { + "@commitlint/format": "^17.4.4", + "@commitlint/lint": "^17.6.1", + "@commitlint/load": "^17.5.0", + "@commitlint/read": "^17.5.1", + "@commitlint/types": "^17.4.4", + "execa": "^5.0.0", + "lodash.isfunction": "^3.0.9", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + } + }, + "@commitlint/config-conventional": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.6.1.tgz", + "integrity": "sha512-ng/ybaSLuTCH9F+7uavSOnEQ9EFMl7lHEjfAEgRh1hwmEe8SpLKpQeMo2aT1IWvHaGMuTb+gjfbzoRf2IR23NQ==", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "^5.0.0" + } + }, + "@commitlint/config-validator": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.4.4.tgz", + "integrity": "sha512-bi0+TstqMiqoBAQDvdEP4AFh0GaKyLFlPPEObgI29utoKEYoPQTvF0EYqIwYYLEoJYhj5GfMIhPHJkTJhagfeg==", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "ajv": "^8.11.0" + } + }, + "@commitlint/ensure": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-17.4.4.tgz", + "integrity": "sha512-AHsFCNh8hbhJiuZ2qHv/m59W/GRE9UeOXbkOqxYMNNg9pJ7qELnFcwj5oYpa6vzTSHtPGKf3C2yUFNy1GGHq6g==", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + } + }, + "@commitlint/execute-rule": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-17.4.0.tgz", + "integrity": "sha512-LIgYXuCSO5Gvtc0t9bebAMSwd68ewzmqLypqI2Kke1rqOqqDbMpYcYfoPfFlv9eyLIh4jocHWwCK5FS7z9icUA==", + "dev": true + }, + "@commitlint/format": { + "version": "17.4.4", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "chalk": "^4.1.0" + } + }, + "@commitlint/is-ignored": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.4.4.tgz", + "integrity": "sha512-Y3eo1SFJ2JQDik4rWkBC4tlRIxlXEFrRWxcyrzb1PUT2k3kZ/XGNuCDfk/u0bU2/yS0tOA/mTjFsV+C4qyACHw==", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "semver": "7.3.8" + } + }, + "@commitlint/lint": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.6.1.tgz", + "integrity": "sha512-VARJ9kxH64isgwVnC+ABPafCYzqxpsWJIpDaTuI0gh8aX4GQ0i7cn9tvxtFNfJj4ER2BAJeWJ0vURdNYjK2RQQ==", + "dev": true, + "requires": { + "@commitlint/is-ignored": "^17.4.4", + "@commitlint/parse": "^17.4.4", + "@commitlint/rules": "^17.6.1", + "@commitlint/types": "^17.4.4" + } + }, + "@commitlint/load": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.5.0.tgz", + "integrity": "sha512-l+4W8Sx4CD5rYFsrhHH8HP01/8jEP7kKf33Xlx2Uk2out/UKoKPYMOIRcDH5ppT8UXLMV+x6Wm5osdRKKgaD1Q==", + "dev": true, + "requires": { + "@commitlint/config-validator": "^17.4.4", + "@commitlint/execute-rule": "^17.4.0", + "@commitlint/resolve-extends": "^17.4.4", + "@commitlint/types": "^17.4.4", + "@types/node": "*", + "chalk": "^4.1.0", + "cosmiconfig": "^8.0.0", + "cosmiconfig-typescript-loader": "^4.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "resolve-from": "^5.0.0", + "ts-node": "^10.8.1", + "typescript": "^4.6.4 || ^5.0.0" + } + }, + "@commitlint/message": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-17.4.2.tgz", + "integrity": "sha512-3XMNbzB+3bhKA1hSAWPCQA3lNxR4zaeQAQcHj0Hx5sVdO6ryXtgUBGGv+1ZCLMgAPRixuc6en+iNAzZ4NzAa8Q==", + "dev": true + }, + "@commitlint/parse": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-17.4.4.tgz", + "integrity": "sha512-EKzz4f49d3/OU0Fplog7nwz/lAfXMaDxtriidyGF9PtR+SRbgv4FhsfF310tKxs6EPj8Y+aWWuX3beN5s+yqGg==", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "conventional-changelog-angular": "^5.0.11", + "conventional-commits-parser": "^3.2.2" + } + }, + "@commitlint/read": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-17.5.1.tgz", + "integrity": "sha512-7IhfvEvB//p9aYW09YVclHbdf1u7g7QhxeYW9ZHSO8Huzp8Rz7m05aCO1mFG7G8M+7yfFnXB5xOmG18brqQIBg==", + "dev": true, + "requires": { + "@commitlint/top-level": "^17.4.0", + "@commitlint/types": "^17.4.4", + "fs-extra": "^11.0.0", + "git-raw-commits": "^2.0.11", + "minimist": "^1.2.6" + } + }, + "@commitlint/resolve-extends": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.4.4.tgz", + "integrity": "sha512-znXr1S0Rr8adInptHw0JeLgumS11lWbk5xAWFVno+HUFVN45875kUtqjrI6AppmD3JI+4s0uZlqqlkepjJd99A==", + "dev": true, + "requires": { + "@commitlint/config-validator": "^17.4.4", + "@commitlint/types": "^17.4.4", + "import-fresh": "^3.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + } + }, + "@commitlint/rules": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-17.6.1.tgz", + "integrity": "sha512-lUdHw6lYQ1RywExXDdLOKxhpp6857/4c95Dc/1BikrHgdysVUXz26yV0vp1GL7Gv+avx9WqZWTIVB7pNouxlfw==", + "dev": true, + "requires": { + "@commitlint/ensure": "^17.4.4", + "@commitlint/message": "^17.4.2", + "@commitlint/to-lines": "^17.4.0", + "@commitlint/types": "^17.4.4", + "execa": "^5.0.0" + } + }, + "@commitlint/to-lines": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-17.4.0.tgz", + "integrity": "sha512-LcIy/6ZZolsfwDUWfN1mJ+co09soSuNASfKEU5sCmgFCvX5iHwRYLiIuoqXzOVDYOy7E7IcHilr/KS0e5T+0Hg==", + "dev": true + }, + "@commitlint/top-level": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-17.4.0.tgz", + "integrity": "sha512-/1loE/g+dTTQgHnjoCy0AexKAEFyHsR2zRB4NWrZ6lZSMIxAhBJnmCqwao7b4H8888PsfoTBCLBYIw8vGnej8g==", + "dev": true, + "requires": { + "find-up": "^5.0.0" + } + }, + "@commitlint/types": { + "version": "17.4.4", + "dev": true, + "requires": { + "chalk": "^4.1.0" + } + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@esbuild-kit/cjs-loader": { + "version": "2.4.2", + "dev": true, + "requires": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "@esbuild-kit/core-utils": { + "version": "3.1.0", + "dev": true, + "requires": { + "esbuild": "~0.17.6", + "source-map-support": "^0.5.21" + } + }, + "@esbuild-kit/esm-loader": { + "version": "2.5.5", + "dev": true, + "requires": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "@esbuild/android-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz", + "integrity": "sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.11.tgz", + "integrity": "sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.11.tgz", + "integrity": "sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.11.tgz", + "integrity": "sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.11.tgz", + "integrity": "sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.11.tgz", + "integrity": "sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.11.tgz", + "integrity": "sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.11.tgz", + "integrity": "sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.11.tgz", + "integrity": "sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.11.tgz", + "integrity": "sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.11.tgz", + "integrity": "sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.11.tgz", + "integrity": "sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.11.tgz", + "integrity": "sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.11.tgz", + "integrity": "sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.11.tgz", + "integrity": "sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.11", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.11.tgz", + "integrity": "sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.11.tgz", + "integrity": "sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.11.tgz", + "integrity": "sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.11.tgz", + "integrity": "sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.11.tgz", + "integrity": "sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz", + "integrity": "sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==", + "dev": true, + "optional": true + }, + "@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, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", + "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.1", + "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" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "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 + } + } + }, + "@eslint/js": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "dev": true + }, + "@gar/promisify": { + "version": "1.1.3", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.8", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "dev": true + }, + "@jest/expect-utils": { + "version": "29.4.3", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3" + } + }, + "@jest/schemas": { + "version": "29.4.3", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.25.16" + } + }, + "@jest/types": { + "version": "29.4.3", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.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" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@microsoft/api-documenter": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.21.7.tgz", + "integrity": "sha512-qlCJ9dSefL6Rmuv1FKtxg7i6N7Bv6LUPrA5Bb0dR/9Ffac2xQuXtuSXOJ09seM7G0WkpQOzta2Kc1CPixzDaIw==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "js-yaml": "~3.13.1", + "resolve": "~1.22.1" + } + }, + "@microsoft/api-extractor": { + "version": "7.34.4", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.8.4" + }, + "dependencies": { + "typescript": { + "version": "4.8.4", + "dev": true + } + } + }, + "@microsoft/api-extractor-model": { + "version": "7.26.4", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2" + } + }, + "@microsoft/tsdoc": { + "version": "0.14.2", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.16.2", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "dev": true + }, + "resolve": { + "version": "1.19.0", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "@npmcli/git": { + "version": "4.0.3", + "dev": true, + "requires": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "which": { + "version": "3.0.0", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "requires": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + } + }, + "@npmcli/move-file": { + "version": "2.0.1", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true + }, + "@npmcli/promise-spawn": { + "version": "6.0.2", + "dev": true, + "requires": { + "which": "^3.0.0" + }, + "dependencies": { + "which": { + "version": "3.0.0", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/run-script": { + "version": "6.0.0", + "dev": true, + "requires": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "dependencies": { + "which": { + "version": "3.0.0", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@pptr/testserver": { + "version": "file:packages/testserver", + "requires": { + "mime": "3.0.0", + "ws": "8.13.0" + } + }, + "@puppeteer-test/installation": { + "version": "file:test/installation", + "requires": { + "@types/glob": "8.1.0", + "glob": "8.1.0", + "mocha": "10.2.0" + }, + "dependencies": { + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@puppeteer-test/test": { + "version": "file:test", + "requires": { + "puppeteer": "file:../packages/puppeteer" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "@puppeteer/browsers": { + "version": "file:packages/browsers", + "requires": { + "@types/node": "^14.15.0", + "@types/yargs": "17.0.22", + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "dependencies": { + "@types/node": { + "version": "14.18.36", + "dev": true + } + } + }, + "@puppeteer/ng-schematics": { + "version": "file:packages/ng-schematics", + "requires": { + "@angular-devkit/architect": "^0.1502.7", + "@angular-devkit/core": "^15.2.7", + "@angular-devkit/schematics": "^15.2.7", + "@angular/cli": "^15.2.2", + "@schematics/angular": "^14.2.8", + "@types/node": "^14.15.0" + }, + "dependencies": { + "@types/node": { + "version": "14.18.33", + "dev": true + } + } + }, + "@rollup/plugin-commonjs": { + "version": "24.0.1", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "dev": true + } + } + }, + "@rollup/plugin-node-resolve": { + "version": "15.0.1", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.0", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "dependencies": { + "@types/resolve": { + "version": "1.20.2", + "dev": true + } + } + }, + "@rollup/pluginutils": { + "version": "5.0.2", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "dev": true + } + } + }, + "@rushstack/node-core-library": { + "version": "3.55.2", + "dev": true, + "requires": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "dev": true + } + } + }, + "@rushstack/rig-package": { + "version": "0.3.18", + "dev": true, + "requires": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "@rushstack/ts-command-line": { + "version": "4.13.2", + "dev": true, + "requires": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "@schematics/angular": { + "version": "14.2.8", + "dev": true, + "requires": { + "@angular-devkit/core": "14.2.8", + "@angular-devkit/schematics": "14.2.8", + "jsonc-parser": "3.1.0" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "14.2.8", + "dev": true, + "requires": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + } + }, + "@angular-devkit/schematics": { + "version": "14.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.8.tgz", + "integrity": "sha512-L5GEgueZV4vqZy9Ar0zxVJOHK/4ttF1nPjW4Ut1vRFJGxsHFVEpxq5eGBf2JYSiOhqmFYc6GnJOxA6C4xAIHjA==", + "dev": true, + "requires": { + "@angular-devkit/core": "14.2.8", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + } + }, + "jsonc-parser": { + "version": "3.1.0", + "dev": true + }, + "magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "source-map": { + "version": "0.7.4", + "dev": true + } + } + }, + "@sinclair/typebox": { + "version": "0.25.24", + "dev": true + }, + "@sinonjs/commons": { + "version": "2.0.0", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.0.2", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0" + } + }, + "@sinonjs/samsam": { + "version": "7.0.1", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "dev": true + }, + "@tootallnate/once": { + "version": "2.0.0", + "dev": true + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "@tsd/typescript": { + "version": "4.9.5", + "dev": true + }, + "@tufjs/models": { + "version": "1.0.0", + "dev": true, + "requires": { + "minimatch": "^6.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "6.2.0", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@types/argparse": { + "version": "1.0.38", + "dev": true + }, + "@types/debug": { + "version": "4.1.7", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/diff": { + "version": "5.0.2", + "dev": true + }, + "@types/eslint": { + "version": "7.29.0", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "1.0.0", + "dev": true + }, + "@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "requires": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "dev": true + }, + "@types/mime": { + "version": "3.0.1", + "dev": true + }, + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.2", + "dev": true + }, + "@types/mocha": { + "version": "10.0.1", + "dev": true + }, + "@types/ms": { + "version": "0.7.31", + "dev": true + }, + "@types/node": { + "version": "18.14.6", + "devOptional": true + }, + "@types/normalize-package-data": { + "version": "2.4.1", + "dev": true + }, + "@types/pixelmatch": { + "version": "5.2.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/pngjs": { + "version": "6.0.1", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/progress": { + "version": "2.0.5", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/proxy-from-env": { + "version": "1.0.1", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/semver": { + "version": "7.3.13", + "dev": true + }, + "@types/sinon": { + "version": "10.0.13", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.2", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "dev": true + }, + "@types/tar-fs": { + "version": "2.0.1", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tar-stream": "*" + } + }, + "@types/tar-stream": { + "version": "2.2.2", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/through": { + "version": "0.0.30", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/unbzip2-stream": { + "version": "1.4.0", + "dev": true, + "requires": { + "@types/through": "*" + } + }, + "@types/ws": { + "version": "8.5.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "17.0.22", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "dev": true + }, + "@types/yauzl": { + "version": "2.10.0", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.1", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "dev": true + }, + "acorn": { + "version": "8.8.1", + "dev": true + }, + "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, + "requires": {} + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.3.0", + "dev": true, + "requires": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "8.11.0", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "requires": { + "ajv": "^8.0.0" + } + }, + "ansi-colors": { + "version": "4.1.1" + }, + "ansi-escapes": { + "version": "4.3.2", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.1" + }, + "ansi-styles": { + "version": "4.3.0", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "2.0.0", + "dev": true + }, + "are-we-there-yet": { + "version": "3.0.1", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "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 + }, + "array-ify": { + "version": "1.0.0", + "dev": true + }, + "array-includes": { + "version": "3.1.6", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "arrify": { + "version": "1.0.1", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2" + }, + "base64-js": { + "version": "1.5.1" + }, + "binary-extensions": { + "version": "2.2.0" + }, + "bl": { + "version": "4.1.0", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1" + }, + "buffer": { + "version": "5.7.1", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13" + }, + "buffer-from": { + "version": "1.1.2", + "dev": true + }, + "builtin-modules": { + "version": "3.3.0", + "dev": true + }, + "builtins": { + "version": "5.0.1", + "dev": true, + "requires": { + "semver": "^7.0.0" + } + }, + "c8": { + "version": "7.13.0", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "dependencies": { + "cliui": { + "version": "7.0.4", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "dev": true, + "requires": { + "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" + } + } + } + }, + "cacache": { + "version": "17.0.4", + "dev": true, + "requires": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + } + } + }, + "call-bind": { + "version": "1.0.2", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0" + }, + "camelcase": { + "version": "5.3.1", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.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" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "chownr": { + "version": "1.1.4" + }, + "chromium-bidi": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", + "integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", + "requires": { + "mitt": "3.0.0" + } + }, + "ci-info": { + "version": "3.8.0", + "dev": true + }, + "clean-stack": { + "version": "2.2.0", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.7.0" + }, + "cli-width": { + "version": "3.0.0", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4" + }, + "color-convert": { + "version": "2.0.1", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4" + }, + "color-support": { + "version": "1.1.3", + "dev": true + }, + "colors": { + "version": "1.2.5", + "dev": true + }, + "commander": { + "version": "9.5.0", + "dev": true, + "optional": true + }, + "commitlint": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/commitlint/-/commitlint-17.6.1.tgz", + "integrity": "sha512-yO11o5DmN/X4VCL+aLzgfJ1YXOM7qFzMN659SpISS4EBiv+QO16A0jeJU9rgVRbM2K06M7AfBQkZz7EPR3sAnQ==", + "dev": true, + "requires": { + "@commitlint/cli": "^17.6.1", + "@commitlint/types": "^17.4.4" + } + }, + "commondir": { + "version": "1.0.1", + "dev": true + }, + "commonmark": { + "version": "0.30.0", + "dev": true, + "requires": { + "entities": "~2.0", + "mdurl": "~1.0.1", + "minimist": ">=1.2.2", + "string.prototype.repeat": "^0.2.0" + } + }, + "compare-func": { + "version": "2.0.0", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "concat-map": { + "version": "0.0.1" + }, + "console-control-strings": { + "version": "1.1.0", + "dev": true + }, + "conventional-changelog-angular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", + "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + } + }, + "conventional-changelog-conventionalcommits": { + "version": "5.0.0", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + }, + "conventional-commits-parser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", + "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", + "dev": true, + "requires": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + } + }, + "convert-source-map": { + "version": "1.9.0", + "dev": true + }, + "cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "requires": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1" + }, + "js-yaml": { + "version": "4.1.0", + "requires": { + "argparse": "^2.0.1" + } + } + } + }, + "cosmiconfig-typescript-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", + "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", + "dev": true, + "requires": {} + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-env": { + "version": "7.0.3", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "cross-fetch": { + "version": "3.1.5", + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "requires": { + "ms": "2.1.2" + } + }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.4", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "dev": true + }, + "defaults": { + "version": "1.0.4", + "requires": { + "clone": "^1.0.2" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delegates": { + "version": "1.0.0", + "dev": true + }, + "depd": { + "version": "2.0.0", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1120988", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", + "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==" + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diff": { + "version": "5.1.0", + "dev": true + }, + "diff-sequences": { + "version": "29.4.3", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dot-prop": { + "version": "5.3.0", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "emoji-regex": { + "version": "8.0.0" + }, + "encoding": { + "version": "0.1.13", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "requires": { + "once": "^1.4.0" + } + }, + "entities": { + "version": "2.0.3", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "dev": true + }, + "err-code": { + "version": "2.0.3", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.20.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "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-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.11.tgz", + "integrity": "sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.11", + "@esbuild/android-arm64": "0.17.11", + "@esbuild/android-x64": "0.17.11", + "@esbuild/darwin-arm64": "0.17.11", + "@esbuild/darwin-x64": "0.17.11", + "@esbuild/freebsd-arm64": "0.17.11", + "@esbuild/freebsd-x64": "0.17.11", + "@esbuild/linux-arm": "0.17.11", + "@esbuild/linux-arm64": "0.17.11", + "@esbuild/linux-ia32": "0.17.11", + "@esbuild/linux-loong64": "0.17.11", + "@esbuild/linux-mips64el": "0.17.11", + "@esbuild/linux-ppc64": "0.17.11", + "@esbuild/linux-riscv64": "0.17.11", + "@esbuild/linux-s390x": "0.17.11", + "@esbuild/linux-x64": "0.17.11", + "@esbuild/netbsd-x64": "0.17.11", + "@esbuild/openbsd-x64": "0.17.11", + "@esbuild/sunos-x64": "0.17.11", + "@esbuild/win32-arm64": "0.17.11", + "@esbuild/win32-ia32": "0.17.11", + "@esbuild/win32-x64": "0.17.11" + } + }, + "escalade": { + "version": "3.1.1" + }, + "escape-string-regexp": { + "version": "4.0.0" + }, + "eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.39.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "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.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.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", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "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.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "argparse": { + "version": "2.0.1", + "dev": true + }, + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "requires": {} + }, + "eslint-formatter-codeframe": { + "version": "7.32.1", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "chalk": "^4.0.0" + } + }, + "eslint-formatter-pretty": { + "version": "4.1.0", + "dev": true, + "requires": { + "@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" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.7", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.4", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-es": { + "version": "3.0.1", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.27.5", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "semver": { + "version": "6.3.0", + "dev": true + } + } + }, + "eslint-plugin-local": { + "version": "1.0.0", + "dev": true + }, + "eslint-plugin-mocha": { + "version": "10.1.0", + "dev": true, + "requires": { + "eslint-utils": "^3.0.0", + "rambda": "^7.1.0" + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "dev": true + }, + "semver": { + "version": "6.3.0", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "4.2.1", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-tsdoc": { + "version": "0.2.17", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2" + } + }, + "eslint-plugin-unused-imports": { + "version": "2.0.0", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-rule-composer": { + "version": "0.3.0", + "dev": true + }, + "eslint-rule-docs": { + "version": "1.1.235", + "dev": true + }, + "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, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true + }, + "espree": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.0" + } + }, + "esprima": { + "version": "4.0.1", + "dev": true + }, + "esquery": { + "version": "1.5.0", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "dev": true + } + } + }, + "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, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "dev": true + }, + "execa": { + "version": "5.1.1", + "dev": true, + "requires": { + "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" + } + }, + "expect": { + "version": "29.4.3", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.4.3", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.4.3", + "jest-message-util": "^29.4.3", + "jest-util": "^29.4.3" + } + }, + "external-editor": { + "version": "3.1.0", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, + "extract-zip": { + "version": "2.0.1", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "fast-deep-equal": { + "version": "3.1.3" + }, + "fast-diff": { + "version": "1.2.0", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "dev": true, + "requires": { + "@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" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fd-slicer": { + "version": "1.1.0", + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "3.2.0", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "dev": true + } + } + }, + "file-entry-cache": { + "version": "6.0.1", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2" + }, + "flat-cache": { + "version": "3.0.4", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "dev": true + }, + "foreground-child": { + "version": "2.0.0", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "fs-constants": { + "version": "1.0.0" + }, + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-minipass": { + "version": "3.0.1", + "dev": true, + "requires": { + "minipass": "^4.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0" + }, + "function-bind": { + "version": "1.1.1", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "dev": true + }, + "gauge": { + "version": "4.0.4", + "dev": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "get-caller-file": { + "version": "2.0.5" + }, + "get-intrinsic": { + "version": "1.1.3", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-stream": { + "version": "6.0.1", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "get-tsconfig": { + "version": "4.4.0", + "dev": true + }, + "git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "requires": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "glob-parent": { + "version": "6.0.2", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "dev": true, + "requires": { + "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" + } + }, + "graceful-fs": { + "version": "4.2.10", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "dev": true + }, + "gts": { + "version": "4.0.0", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "chalk": "^4.1.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^8.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "~2.7.0", + "rimraf": "^3.0.2", + "write-file-atomic": "^4.0.0" + }, + "dependencies": { + "meow": { + "version": "9.0.0", + "dev": true, + "requires": { + "@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" + } + }, + "prettier": { + "version": "2.7.1", + "dev": true + }, + "type-fest": { + "version": "0.18.1", + "dev": true + } + } + }, + "hard-rejection": { + "version": "2.1.0", + "dev": true + }, + "has": { + "version": "1.0.3", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "dev": true + }, + "has-flag": { + "version": "4.0.0" + }, + "has-property-descriptors": { + "version": "1.0.0", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "has-unicode": { + "version": "2.0.1", + "dev": true + }, + "he": { + "version": "1.2.0" + }, + "hosted-git-info": { + "version": "4.1.0", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "html-escaper": { + "version": "2.0.2", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.1", + "dev": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "dev": true + }, + "humanize-ms": { + "version": "1.2.1", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "husky": { + "version": "8.0.3", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1" + }, + "ignore": { + "version": "5.2.0", + "dev": true + }, + "ignore-walk": { + "version": "6.0.1", + "dev": true, + "requires": { + "minimatch": "^6.1.6" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "6.2.0", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "import-fresh": { + "version": "3.3.0", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0" + } + } + }, + "import-lazy": { + "version": "4.0.0", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "inquirer": { + "version": "7.3.3", + "dev": true, + "requires": { + "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" + } + }, + "internal-slot": { + "version": "1.0.3", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "ip": { + "version": "2.0.0", + "dev": true + }, + "irregular-plurals": { + "version": "3.3.0", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1" + }, + "is-bigint": { + "version": "1.0.4", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-builtin-module": { + "version": "3.2.0", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + } + }, + "is-callable": { + "version": "1.2.7", + "dev": true + }, + "is-core-module": { + "version": "2.11.0", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "dev": true + }, + "is-extglob": { + "version": "2.1.1" + }, + "is-fullwidth-code-point": { + "version": "3.0.0" + }, + "is-glob": { + "version": "4.0.3", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0" + }, + "is-lambda": { + "version": "1.0.1", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.2", + "dev": true + }, + "is-number": { + "version": "7.0.0" + }, + "is-number-object": { + "version": "1.0.7", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "is-regex": { + "version": "1.1.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "requires": { + "text-extensions": "^1.0.0" + } + }, + "is-unicode-supported": { + "version": "0.1.0" + }, + "is-weakref": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-wsl": { + "version": "2.2.0", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "0.0.1", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-reports": { + "version": "3.1.5", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest-diff": { + "version": "29.4.3", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.4.3" + } + }, + "jest-get-type": { + "version": "29.4.3", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.4.3", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.4.3" + } + }, + "jest-message-util": { + "version": "29.4.3", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.4.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.4.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + } + } + }, + "jest-util": { + "version": "29.4.3", + "dev": true, + "requires": { + "@jest/types": "^29.4.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jju": { + "version": "1.4.0", + "dev": true + }, + "jpeg-js": { + "version": "0.4.4", + "dev": true + }, + "js-sdsl": { + "version": "4.1.5", + "dev": true + }, + "js-tokens": { + "version": "4.0.0" + }, + "js-yaml": { + "version": "3.13.1", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1" + }, + "json-schema-traverse": { + "version": "1.0.0" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true + }, + "json5": { + "version": "2.2.3", + "dev": true + }, + "jsonc-parser": { + "version": "3.2.0" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonparse": { + "version": "1.3.1", + "dev": true + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "just-extend": { + "version": "4.2.1", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "dev": true + }, + "levn": { + "version": "0.4.1", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "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, + "requires": { + "color-convert": "^1.9.0" + } + }, + "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, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "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, + "requires": { + "color-name": "1.1.3" + } + }, + "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 + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "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 + }, + "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 + }, + "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, + "requires": { + "minimist": "^1.2.6" + } + }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "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, + "requires": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "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, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "lines-and-columns": { + "version": "1.2.4" + }, + "load-json-file": { + "version": "4.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "locate-path": { + "version": "6.0.0", + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "dev": true + }, + "lodash.isfunction": { + "version": "3.0.9", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.27.0", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "make-dir": { + "version": "3.1.0", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-fetch-happen": { + "version": "10.2.1", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "dependencies": { + "@npmcli/fs": { + "version": "2.1.2", + "dev": true, + "requires": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + } + }, + "cacache": { + "version": "16.1.3", + "dev": true, + "requires": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + } + }, + "chownr": { + "version": "2.0.0", + "dev": true + }, + "fs-minipass": { + "version": "2.1.0", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ssri": { + "version": "9.0.1", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "unique-filename": { + "version": "2.0.1", + "dev": true, + "requires": { + "unique-slug": "^3.0.0" + } + }, + "unique-slug": { + "version": "3.0.0", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + } + } + }, + "map-obj": { + "version": "4.3.0", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "dev": true + }, + "memorystream": { + "version": "0.3.1", + "dev": true + }, + "meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "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" + }, + "dependencies": { + "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 + } + } + }, + "merge-stream": { + "version": "2.0.0", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "3.0.0" + }, + "mimic-fn": { + "version": "2.1.0" + }, + "min-indent": { + "version": "1.0.1", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "dev": true + }, + "minimist-options": { + "version": "4.1.0", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, + "minipass": { + "version": "4.2.4", + "dev": true + }, + "minipass-collect": { + "version": "1.0.2", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-fetch": { + "version": "2.1.2", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-flush": { + "version": "1.0.5", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-sized": { + "version": "1.0.3", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minizlib": { + "version": "2.1.2", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "mitt": { + "version": "3.0.0" + }, + "mkdirp": { + "version": "1.0.4", + "dev": true + }, + "mkdirp-classic": { + "version": "0.5.3" + }, + "mocha": { + "version": "10.2.0", + "requires": { + "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" + }, + "dependencies": { + "argparse": { + "version": "2.0.1" + }, + "cliui": { + "version": "7.0.4", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "diff": { + "version": "5.0.0" + }, + "glob": { + "version": "7.2.0", + "requires": { + "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" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "js-yaml": { + "version": "4.1.0", + "requires": { + "argparse": "^2.0.1" + } + }, + "minimatch": { + "version": "5.0.1", + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.1.3" + }, + "supports-color": { + "version": "8.1.1", + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "requires": { + "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" + } + }, + "yargs-parser": { + "version": "20.2.4" + } + } + }, + "ms": { + "version": "2.1.2" + }, + "mute-stream": { + "version": "0.0.8", + "dev": true + }, + "nanoid": { + "version": "3.3.3" + }, + "natural-compare": { + "version": "1.4.0", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "dev": true + }, + "ncp": { + "version": "2.0.0", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "dev": true + }, + "nise": { + "version": "5.1.3", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "7.1.2", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "1.8.6", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + } + } + }, + "node-fetch": { + "version": "2.6.7", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-gyp": { + "version": "9.3.1", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "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" + } + } + } + }, + "nopt": { + "version": "6.0.0", + "dev": true, + "requires": { + "abbrev": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "3.0.3", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0" + }, + "npm-bundled": { + "version": "3.0.0", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^3.0.0" + } + }, + "npm-install-checks": { + "version": "6.0.0", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "3.0.0", + "dev": true + }, + "npm-package-arg": { + "version": "10.1.0", + "dev": true, + "requires": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "6.1.1", + "dev": true, + "requires": { + "lru-cache": "^7.5.1" + } + }, + "lru-cache": { + "version": "7.18.3", + "dev": true + } + } + }, + "npm-packlist": { + "version": "7.0.4", + "dev": true, + "requires": { + "ignore-walk": "^6.0.0" + } + }, + "npm-pick-manifest": { + "version": "8.0.1", + "dev": true, + "requires": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + } + }, + "npm-registry-fetch": { + "version": "14.0.3", + "dev": true, + "requires": { + "make-fetch-happen": "^11.0.0", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.1", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "npm-run-all": { + "version": "4.1.5", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "dev": true + }, + "semver": { + "version": "5.7.1", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "npm-run-path": { + "version": "4.0.1", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "npmlog": { + "version": "6.0.2", + "dev": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "object-inspect": { + "version": "1.12.2", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.values": { + "version": "1.1.6", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "once": { + "version": "1.4.0", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.1", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.1", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "ora": { + "version": "5.4.1", + "requires": { + "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" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-map": { + "version": "4.0.0", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "dev": true + }, + "pacote": { + "version": "15.1.0", + "dev": true, + "requires": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + } + }, + "parent-module": { + "version": "1.0.1", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "requires": { + "@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" + } + }, + "parsel-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.0.tgz", + "integrity": "sha512-+CAY5A3p8b6he3OzlY/naXpeeiLMjEqFUyMwiPrwnemG5yh0/sgygYMKRhtn6/YSriyy4KZwQLnpBfs36GnXUg==", + "dev": true + }, + "path-exists": { + "version": "4.0.0" + }, + "path-is-absolute": { + "version": "1.0.1" + }, + "path-key": { + "version": "3.1.1", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "path-type": { + "version": "4.0.0" + }, + "pend": { + "version": "1.2.0" + }, + "picomatch": { + "version": "2.3.1" + }, + "pidtree": { + "version": "0.3.1", + "dev": true + }, + "pify": { + "version": "3.0.0", + "dev": true + }, + "pixelmatch": { + "version": "5.3.0", + "dev": true, + "requires": { + "pngjs": "^6.0.0" + }, + "dependencies": { + "pngjs": { + "version": "6.0.0", + "dev": true + } + } + }, + "plur": { + "version": "4.0.0", + "dev": true, + "requires": { + "irregular-plurals": "^3.2.0" + } + }, + "pngjs": { + "version": "7.0.0", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "dev": true + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "29.4.3", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "dev": true + } + } + }, + "proc-log": { + "version": "3.0.0", + "dev": true + }, + "progress": { + "version": "2.0.3" + }, + "promise-inflight": { + "version": "1.0.1", + "dev": true + }, + "promise-retry": { + "version": "2.0.1", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proper-lockfile": { + "version": "4.1.2", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "proxy-from-env": { + "version": "1.1.0" + }, + "pump": { + "version": "3.0.0", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1" + }, + "puppeteer": { + "version": "file:packages/puppeteer", + "requires": { + "@puppeteer/browsers": "1.0.0", + "cosmiconfig": "8.1.3", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "20.1.0" + } + }, + "puppeteer-core": { + "version": "file:packages/puppeteer-core", + "requires": { + "@puppeteer/browsers": "1.0.0", + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1120988", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "mitt": "3.0.0", + "parsel-js": "1.1.0", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0" + } + }, + "q": { + "version": "1.5.1", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "dev": true + }, + "quick-lru": { + "version": "4.0.1", + "dev": true + }, + "rambda": { + "version": "7.3.0", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "react-is": { + "version": "18.2.0", + "dev": true + }, + "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, + "requires": { + "debuglog": "^1.0.1", + "graceful-fs": "^4.1.2", + "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" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "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" + } + }, + "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 + }, + "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, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "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 + }, + "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, + "requires": { + "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" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "read-package-json": { + "version": "6.0.0", + "dev": true, + "requires": { + "glob": "^8.0.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "6.1.1", + "dev": true, + "requires": { + "lru-cache": "^7.5.1" + } + }, + "json-parse-even-better-errors": { + "version": "3.0.0", + "dev": true + }, + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "normalize-package-data": { + "version": "5.0.0", + "dev": true, + "requires": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + } + } + } + }, + "read-package-json-fast": { + "version": "3.0.2", + "dev": true, + "requires": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "dependencies": { + "json-parse-even-better-errors": { + "version": "3.0.0", + "dev": true + } + } + }, + "read-pkg": { + "version": "3.0.0", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "semver": { + "version": "5.7.1", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "2.3.0", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "dev": true + } + } + }, + "semver": { + "version": "5.7.1", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "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==", + "dev": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "requires": { + "picomatch": "^2.2.1" + } + }, + "redent": { + "version": "3.0.0", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "dev": true + }, + "require-directory": { + "version": "2.1.1" + }, + "require-from-string": { + "version": "2.0.2" + }, + "resolve": { + "version": "1.22.1", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "requires": { + "global-dirs": "^0.1.1" + } + }, + "restore-cursor": { + "version": "3.1.0", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "retry": { + "version": "0.12.0", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "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" + } + } + } + }, + "rollup": { + "version": "3.18.0", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-async": { + "version": "2.4.1", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.7", + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.1" + }, + "safe-regex-test": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2", + "devOptional": true + }, + "semver": { + "version": "7.3.8", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.0", + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "dev": true + }, + "shell-quote": { + "version": "1.7.4", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7" + }, + "sigstore": { + "version": "1.0.0", + "dev": true, + "requires": { + "make-fetch-happen": "^11.0.1", + "tuf-js": "^1.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.1", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "sinon": { + "version": "15.0.1", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "10.0.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + } + }, + "slash": { + "version": "3.0.0", + "dev": true + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "dev": true + }, + "socks": { + "version": "2.7.1", + "dev": true, + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "7.0.0", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + } + }, + "source-map": { + "version": "0.6.1", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "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, + "requires": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.12", + "dev": true + }, + "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 + }, + "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, + "requires": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "requires": { + "readable-stream": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "dev": true + }, + "ssri": { + "version": "10.0.1", + "dev": true, + "requires": { + "minipass": "^4.0.0" + } + }, + "stack-utils": { + "version": "2.0.6", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "dev": true + } + } + }, + "string_decoder": { + "version": "1.3.0", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-argv": { + "version": "0.3.1", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.padend": { + "version": "3.1.3", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "string.prototype.repeat": { + "version": "0.2.0", + "dev": true + }, + "string.prototype.trimend": { + "version": "1.0.5", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1" + }, + "supports-color": { + "version": "7.2.0", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.3.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true + }, + "symbol-observable": { + "version": "4.0.0", + "dev": true + }, + "tar": { + "version": "6.1.13", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "dev": true + }, + "fs-minipass": { + "version": "2.1.0", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + } + } + }, + "tar-fs": { + "version": "2.1.1", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "test-exclude": { + "version": "6.0.0", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "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" + } + } + } + }, + "text-diff": { + "version": "1.0.1", + "dev": true + }, + "text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "dev": true + }, + "through": { + "version": "2.3.8" + }, + "through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "requires": { + "readable-stream": "3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "requires": { + "is-number": "^7.0.0" + } + }, + "tr46": { + "version": "0.0.3" + }, + "treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true + }, + "trim-newlines": { + "version": "3.0.1", + "dev": true + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tsconfig-paths": { + "version": "3.14.1", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "tsd": { + "version": "0.26.0", + "dev": true, + "requires": { + "@tsd/typescript": "~4.9.5", + "eslint-formatter-pretty": "^4.1.0", + "globby": "^11.0.1", + "meow": "^9.0.0", + "path-exists": "^4.0.0", + "read-pkg-up": "^7.0.0" + }, + "dependencies": { + "meow": { + "version": "9.0.0", + "dev": true, + "requires": { + "@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" + } + }, + "type-fest": { + "version": "0.18.1", + "dev": true + } + } + }, + "tslib": { + "version": "1.14.1" + }, + "tsutils": { + "version": "3.21.0", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tsx": { + "version": "3.12.3", + "dev": true, + "requires": { + "@esbuild-kit/cjs-loader": "^2.4.2", + "@esbuild-kit/core-utils": "^3.0.0", + "@esbuild-kit/esm-loader": "^2.5.5", + "fsevents": "~2.3.2" + } + }, + "tuf-js": { + "version": "1.1.1", + "dev": true, + "requires": { + "@tufjs/models": "1.0.0", + "make-fetch-happen": "^11.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.1", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "tunnel": { + "version": "0.0.6", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "dev": true + }, + "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 + }, + "typescript": { + "version": "4.9.5", + "devOptional": true + }, + "unbox-primitive": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unbzip2-stream": { + "version": "1.4.3", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "unique-filename": { + "version": "3.0.0", + "dev": true, + "requires": { + "unique-slug": "^4.0.0" + } + }, + "unique-slug": { + "version": "4.0.0", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2" + }, + "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 + }, + "uuid": { + "version": "8.3.2", + "dev": true + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "v8-to-istanbul": { + "version": "9.0.1", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.16", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "requires": { + "builtins": "^5.0.0" + } + }, + "validator": { + "version": "13.9.0", + "dev": true + }, + "wcwidth": { + "version": "1.0.1", + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "3.0.1" + }, + "whatwg-url": { + "version": "5.0.0", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "requires": { + "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" + } + }, + "wide-align": { + "version": "1.1.5", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wireit": { + "version": "0.9.5", + "dev": true, + "requires": { + "braces": "^3.0.2", + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "jsonc-parser": "^3.0.0", + "proper-lockfile": "^4.1.2" + } + }, + "word-wrap": { + "version": "1.2.3", + "dev": true + }, + "workerpool": { + "version": "6.2.1" + }, + "wrap-ansi": { + "version": "7.0.0", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2" + }, + "write-file-atomic": { + "version": "4.0.2", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "requires": {} + }, + "y18n": { + "version": "5.0.8" + }, + "yallist": { + "version": "4.0.0", + "dev": true + }, + "yargs": { + "version": "17.7.1", + "requires": { + "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" + }, + "dependencies": { + "yargs-parser": { + "version": "21.1.1" + } + } + }, + "yargs-parser": { + "version": "20.2.9", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0" + }, + "decamelize": { + "version": "4.0.0" + }, + "is-plain-obj": { + "version": "2.1.0" + } + } + }, + "yauzl": { + "version": "2.10.0", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0" + }, + "z-schema": { + "version": "5.0.5", + "dev": true, + "requires": { + "commander": "^9.4.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + } + }, + "zod": { + "version": "3.21.2", + "dev": true + } + } +} diff --git a/remote/test/puppeteer/package.json b/remote/test/puppeteer/package.json new file mode 100644 index 0000000000..cf57cb328d --- /dev/null +++ b/remote/test/puppeteer/package.json @@ -0,0 +1,193 @@ +{ + "name": "puppeteer-repo", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer" + }, + "scripts": { + "build": "wireit", + "build:docs": "wireit", + "check:pinned-deps": "tsx tools/ensure-pinned-deps", + "check": "npm run check --workspaces --if-present && run-p check:*", + "clean": "rimraf -g \"./**/.wireit\" && npm run clean --workspaces --if-present", + "commitlint": "commitlint --from=HEAD~1", + "debug": "mocha --inspect-brk", + "docs": "run-s build:docs generate:markdown", + "format:eslint": "eslint --ext js --ext ts --fix .", + "format:prettier": "prettier --write .", + "format:expectations": "node tools/sort-test-expectations.js", + "format": "run-s format:*", + "generate:markdown": "tsx tools/generate_docs.ts", + "lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)", + "lint:prettier": "prettier --check .", + "lint": "run-s lint:prettier lint:eslint", + "postinstall": "npm run postinstall --workspaces --if-present", + "prepare": "husky install", + "test-install": "npm run test --workspace @puppeteer-test/installation", + "test-types": "tsd -t packages/puppeteer", + "test:chrome:headful": "wireit", + "test:chrome:new-headless": "wireit", + "test:chrome:headless": "wireit", + "test:chrome:bidi": "wireit", + "test:chrome": "wireit", + "test:firefox:bidi": "wireit", + "test:firefox:headful": "wireit", + "test:firefox:headless": "wireit", + "test:firefox": "wireit", + "test": "cross-env PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 node tools/mochaRunner/lib/main.js", + "validate-licenses": "tsx tools/third_party/validate-licenses.ts" + }, + "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:docs": { + "dependencies": [ + "./packages/browsers:build:docs", + "./packages/puppeteer:build:docs", + "./packages/puppeteer-core:build:docs" + ] + }, + "test:chrome:headful": { + "command": "npm test -- --test-suite chrome-headful", + "dependencies": [ + "./test:build" + ] + }, + "test:chrome:headless": { + "command": "npm test -- --test-suite chrome-headless", + "dependencies": [ + "./test:build" + ] + }, + "test:chrome:new-headless": { + "command": "npm test -- --test-suite chrome-new-headless", + "dependencies": [ + "./test:build" + ] + }, + "test:chrome:bidi": { + "command": "npm test -- --test-suite chrome-bidi", + "dependencies": [ + "./test:build" + ] + }, + "test:firefox:headful": { + "command": "npm test -- --test-suite firefox-headful", + "dependencies": [ + "./test:build" + ] + }, + "test:firefox:headless": { + "command": "npm test -- --test-suite firefox-headless", + "dependencies": [ + "./test:build" + ] + }, + "test:firefox:bidi": { + "command": "npm test -- --test-suite firefox-bidi", + "dependencies": [ + "./test:build" + ] + }, + "test:chrome": { + "dependencies": [ + "test:chrome:headful", + "test:chrome:headless", + "test:chrome:new-headless", + "test:chrome:bidi" + ] + }, + "test:firefox": { + "dependencies": [ + "test:firefox:headful", + "test:firefox:headless", + "test:firefox:bidi" + ] + } + }, + "devDependencies": { + "@actions/core": "1.10.0", + "@commitlint/cli": "17.6.1", + "@commitlint/config-conventional": "17.6.1", + "@microsoft/api-documenter": "7.21.7", + "@microsoft/api-extractor": "7.34.4", + "@microsoft/api-extractor-model": "7.26.4", + "@pptr/testserver": "file:packages/testserver", + "@rollup/plugin-commonjs": "24.0.1", + "@rollup/plugin-node-resolve": "15.0.1", + "@types/debug": "4.1.7", + "@types/diff": "5.0.2", + "@types/mime": "3.0.1", + "@types/mocha": "10.0.1", + "@types/node": "18.14.6", + "@types/pixelmatch": "5.2.4", + "@types/pngjs": "6.0.1", + "@types/progress": "2.0.5", + "@types/proxy-from-env": "1.0.1", + "@types/semver": "7.3.13", + "@types/sinon": "10.0.13", + "@types/tar-fs": "2.0.1", + "@types/unbzip2-stream": "1.4.0", + "@types/ws": "8.5.4", + "@typescript-eslint/eslint-plugin": "5.59.1", + "@typescript-eslint/parser": "5.59.1", + "c8": "7.13.0", + "commitlint": "17.6.1", + "commonmark": "0.30.0", + "cross-env": "7.0.3", + "diff": "5.1.0", + "esbuild": "^0.17.11", + "eslint": "8.39.0", + "eslint-config-prettier": "8.8.0", + "eslint-formatter-codeframe": "7.32.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-local": "1.0.0", + "eslint-plugin-mocha": "10.1.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-tsdoc": "0.2.17", + "eslint-plugin-unused-imports": "2.0.0", + "esprima": "4.0.1", + "expect": "29.4.3", + "glob": "8.1.0", + "gts": "4.0.0", + "husky": "8.0.3", + "jpeg-js": "0.4.4", + "license-checker": "25.0.1", + "mime": "3.0.0", + "minimist": "1.2.8", + "mocha": "10.2.0", + "ncp": "2.0.0", + "npm-run-all": "4.1.5", + "pixelmatch": "5.3.0", + "pngjs": "7.0.0", + "prettier": "2.8.8", + "puppeteer": "file:packages/puppeteer", + "rimraf": "3.0.2", + "rollup": "3.18.0", + "semver": "7.3.8", + "sinon": "15.0.1", + "source-map-support": "0.5.21", + "spdx-satisfies": "5.0.1", + "text-diff": "1.0.1", + "tsd": "0.26.0", + "tsx": "3.12.3", + "typescript": "4.9.5", + "wireit": "0.9.5", + "zod": "3.21.2" + }, + "workspaces": [ + "packages/*", + "test", + "test/installation" + ] +} diff --git a/remote/test/puppeteer/packages/browsers/.gitignore b/remote/test/puppeteer/packages/browsers/.gitignore new file mode 100644 index 0000000000..23b2baa7ca --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/.gitignore @@ -0,0 +1 @@ +test/cache \ No newline at end of file diff --git a/remote/test/puppeteer/packages/browsers/.mocharc.cjs b/remote/test/puppeteer/packages/browsers/.mocharc.cjs new file mode 100644 index 0000000000..4cabbc3232 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/.mocharc.cjs @@ -0,0 +1,7 @@ +module.exports = { + logLevel: 'debug', + spec: 'test/build/**/*.spec.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..9da7ab8980 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/CHANGELOG.md @@ -0,0 +1,131 @@ +# Changelog + +## [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..4c40b78e3f --- /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 and Chrome/Chromium. The `latest` keyword only works during the installation. 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": "/lib/esm/main.d.ts", + + "extends": "./api-extractor.json", + + "dtsRollup": { + "enabled": false + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "/../../docs/.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": "/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..c28787e197 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/package.json @@ -0,0 +1,123 @@ +{ + "name": "@puppeteer/browsers", + "version": "1.0.0", + "description": "Download and launch browsers", + "scripts": { + "build:docs": "wireit", + "build": "wireit", + "build:test": "wireit", + "clean": "tsc --build --clean && rm -rf lib", + "test": "wireit" + }, + "bin": { + "@puppeteer/browsers": "lib/cjs/main-cli.js" + }, + "main": "./lib/cjs/main.js", + "module": "./lib/esm/main.js", + "type": "commonjs", + "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 && cross-env DEBUG=puppeteer:* 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.0.0" + }, + "files": [ + "lib", + "!*.tsbuildinfo" + ], + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "devDependencies": { + "@types/node": "^14.15.0", + "@types/yargs": "17.0.22" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } +} 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..6767002269 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/CLI.ts @@ -0,0 +1,313 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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, + Browser, + BrowserPlatform, + 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'; + +type InstallArgs = { + browser: { + name: Browser; + buildId: string; + }; + path?: string; + platform?: BrowserPlatform; + baseUrl?: string; +}; + +type LaunchArgs = { + browser: { + name: Browser; + buildId: string; + }; + path?: string; + platform?: BrowserPlatform; + detached: boolean; + system: boolean; +}; + +type ClearArgs = { + path?: string; +}; + +/** + * @public + */ +export class CLI { + #cachePath; + #rl?: readline.Interface; + + constructor(cachePath = process.cwd(), rl?: readline.Interface) { + this.#cachePath = cachePath; + this.#rl = rl; + } + + #defineBrowserParameter(yargs: Yargs.Argv): void { + yargs.positional('browser', { + description: + 'Which browser to install [@]. `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): 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, required = false): void { + 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 { + const yargsInstance = yargs(hideBin(argv)); + await yargsInstance + .scriptName('@puppeteer/browsers') + .command( + 'install ', + '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: @ ).', + 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 latest 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 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.' + ); + 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`); + } + 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 ', + '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@1083080', + 'Launch the Chrome browser identified by the revision 1083080.' + ); + yargs.example( + '$0 launch firefox@112.0a1', + 'Launch the Firefox browser identified by the milestone 112.0a1.' + ); + yargs.example( + '$0 launch chrome@1083080 --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', + 'Removes all installed browsers from the specified cache directory', + 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() + .wrap(Math.min(120, yargsInstance.terminalWidth())) + .parse(); + } + + #parseBrowser(version: string): Browser { + return version.split('@').shift() as Browser; + } + + #parseBuildId(version: string): string { + return version.split('@').pop() ?? '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..142bceb08e --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/Cache.ts @@ -0,0 +1,119 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import fs from 'fs'; +import path from 'path'; + +import {Browser, BrowserPlatform} from './browser-data/browser-data.js'; + +/** + * @public + */ +export type InstalledBrowser = { + path: string; + browser: Browser; + buildId: string; + platform: BrowserPlatform; +}; + +/** + * The cache used by Puppeteer relies on the following structure: + * + * - rootDir + * -- | browserRoot(browser1) + * ---- - | installationDir() + * ------ the browser-platform-buildId + * ------ specific structure. + * -- | browserRoot(browser2) + * ---- - | installationDir() + * ------ the browser-platform-buildId + * ------ specific structure. + * @internal + */ +export class Cache { + #rootDir: string; + + constructor(rootDir: string) { + this.#rootDir = 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, + }); + } + + 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 { + path: path.join(this.browserRoot(browser), file), + browser, + platform: result.platform, + buildId: result.buildId, + }; + }) + .filter((item): item is InstalledBrowser => { + return item !== null; + }); + }); + } +} + +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..413435453a --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts @@ -0,0 +1,124 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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, + ProfileOptions, +} from './types.js'; + +export {ProfileOptions}; + +export const downloadUrls = { + [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl, + [Browser.CHROME]: chrome.resolveDownloadUrl, + [Browser.CHROMIUM]: chromium.resolveDownloadUrl, + [Browser.FIREFOX]: firefox.resolveDownloadUrl, +}; + +export const downloadPaths = { + [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadPath, + [Browser.CHROME]: chrome.resolveDownloadPath, + [Browser.CHROMIUM]: chromium.resolveDownloadPath, + [Browser.FIREFOX]: firefox.resolveDownloadPath, +}; + +export const executablePathByBrowser = { + [Browser.CHROMEDRIVER]: chromedriver.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 { + switch (browser) { + case Browser.FIREFOX: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + return await firefox.resolveBuildId('FIREFOX_NIGHTLY'); + } + case Browser.CHROME: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + // In CfT beta is the latest version. + return await chrome.resolveBuildId(platform, 'beta'); + } + case Browser.CHROMEDRIVER: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + return await chromedriver.resolveBuildId('latest'); + } + case Browser.CHROMIUM: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + return await chromium.resolveBuildId(platform, 'latest'); + } + } + // 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 { + 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.FIREFOX: + throw new Error( + `System browser detection is not supported for ${browser} yet.` + ); + case Browser.CHROME: + return chromium.resolveSystemExecutablePath(platform, channel); + case Browser.CHROMIUM: + return chrome.resolveSystemExecutablePath(platform, channel); + } +} 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..1fbf8c9647 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts @@ -0,0 +1,169 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import path from 'path'; + +import {httpRequest} 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'; + } +} + +function chromiumDashPlatform(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'linux'; + case BrowserPlatform.MAC_ARM: + return 'mac'; + case BrowserPlatform.MAC: + return 'mac'; + case BrowserPlatform.WIN32: + return 'win'; + 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 resolveBuildId( + platform: BrowserPlatform, + channel: 'beta' | 'stable' = 'beta' +): Promise { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL( + `https://chromiumdash.appspot.com/fetch_releases?platform=${chromiumDashPlatform( + platform + )}&channel=${channel}` + ), + '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 { + const response = JSON.parse(String(data)); + return resolve(response[0].version); + } catch { + return reject(new Error('Chrome version not found')); + } + }); + }, + false + ); + request.on('error', err => { + reject(err); + }); + }); +} + +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..39894d2e86 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import {httpRequest} from '../httpUtil.js'; + +import {BrowserPlatform} from './types.js'; + +function archive(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'chromedriver_linux64'; + case BrowserPlatform.MAC_ARM: + return 'chromedriver_mac_arm64'; + case BrowserPlatform.MAC: + return 'chromedriver_mac64'; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return 'chromedriver_win32'; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + buildId: string, + baseUrl = 'https://chromedriver.storage.googleapis.com' +): string { + return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; +} + +export function resolveDownloadPath( + platform: BrowserPlatform, + buildId: string +): string[] { + return [buildId, `${archive(platform)}.zip`]; +} + +export function relativeExecutablePath( + platform: BrowserPlatform, + _buildId: string +): string { + switch (platform) { + case BrowserPlatform.MAC: + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.LINUX: + return 'chromedriver'; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return 'chromedriver.exe'; + } +} +export async function resolveBuildId( + _channel: 'latest' = 'latest' +): Promise { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL(`https://chromedriver.storage.googleapis.com/LATEST_RELEASE`), + '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/browser-data/chromium.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts new file mode 100644 index 0000000000..71fa003e0a --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts @@ -0,0 +1,125 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import path from 'path'; + +import {httpRequest} from '../httpUtil.js'; + +import {BrowserPlatform} from './types.js'; + +export {resolveSystemExecutablePath} from './chrome.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, + // We will need it for other channels/keywords. + _channel: 'latest' = 'latest' +): Promise { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL( + `https://storage.googleapis.com/chromium-browser-snapshots/${folder( + platform + )}/LAST_CHANGE` + ), + '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/browser-data/firefox.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts new file mode 100644 index 0000000000..c3f337ee5a --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts @@ -0,0 +1,355 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import fs from 'fs'; +import path from 'path'; + +import {httpRequest} from '../httpUtil.js'; + +import {BrowserPlatform, 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 { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL('https://product-details.mozilla.org/1.0/firefox_versions.json'), + '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 { + const versions = JSON.parse(data); + return resolve(versions[channel]); + } catch { + return reject(new Error('Firefox version not found')); + } + }); + }, + false + ); + request.on('error', err => { + reject(err); + }); + }); +} + +export async function createProfile(options: ProfileOptions): Promise { + 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 +): Record { + 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.passwords.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, + + // Disable page translations, which can cause issues with tests. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1836093. + 'browser.translations.enable': 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`, + + // Temporarily force disable BFCache in parent (https://bit.ly/bug-1732263) + 'fission.bfcacheInParent': false, + + // Force all web content to use a single content process + 'fission.webContentIsolationStrategy': 0, + + // 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, + // 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 { + 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..f88d2ca098 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/types.ts @@ -0,0 +1,75 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import * as chrome from './chrome.js'; +import * as firefox from './firefox.js'; + +/** + * Supported browsers. + * + * @public + */ +export enum Browser { + CHROME = 'chrome', + CHROMIUM = 'chromium', + FIREFOX = 'firefox', + CHROMEDRIVER = 'chromedriver', +} + +/** + * Platform names used to identify a OS platfrom 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', +} + +export const downloadUrls = { + [Browser.CHROME]: chrome.resolveDownloadUrl, + [Browser.CHROMIUM]: chrome.resolveDownloadUrl, + [Browser.FIREFOX]: firefox.resolveDownloadUrl, +}; + +/** + * @public + */ +export enum BrowserTag { + LATEST = 'latest', +} + +/** + * @public + */ +export interface ProfileOptions { + preferences: Record; + 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..eee0a347e8 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/debug.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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..fed8c2e2ea --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/detectPlatform.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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..6139accd49 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/fileUtil.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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 { + 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 { + 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 { + 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..5b6150f734 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/httpUtil.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import {createWriteStream} from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import {URL} from 'url'; + +import createHttpsProxyAgent from 'https-proxy-agent'; +import {getProxyForUrl} from 'proxy-from-env'; + +export function headHttpRequest(url: URL): Promise { + return new Promise(resolve => { + const request = httpRequest( + url, + 'HEAD', + response => { + 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, + }; + + const proxyURL = getProxyForUrl(url.toString()); + if (proxyURL) { + const proxy = new URL(proxyURL); + if (proxy.protocol === 'http:') { + options.path = url.href; + options.hostname = proxy.hostname; + options.protocol = proxy.protocol; + options.port = proxy.port; + options.headers ??= {}; + options.headers['Host'] ||= url.host; + } else { + options.agent = createHttpsProxyAgent({ + host: proxy.host, + path: proxy.pathname, + port: proxy.port, + secureProxy: proxy.protocol === 'https:', + headers: options.headers, + }); + } + } + + 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 { + return new Promise((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); + }); + }); +} 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..054e048420 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/install.ts @@ -0,0 +1,218 @@ +/** + * Copyright 2017 Google Inc. 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. + */ + +import assert from 'assert'; +import {existsSync} from 'fs'; +import {mkdir, unlink} from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import { + Browser, + 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(); +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 dowloand. 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://storage.googleapis.com/chromium-browser-snapshots 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 async function install( + options: InstallOptions +): Promise { + 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 structure = new Cache(options.cacheDir); + const browserRoot = structure.browserRoot(options.browser); + const archivePath = path.join(browserRoot, fileName); + if (!existsSync(browserRoot)) { + await mkdir(browserRoot, {recursive: true}); + } + + if (!options.unpack) { + if (existsSync(archivePath)) { + return { + path: archivePath, + browser: options.browser, + platform: options.platform, + buildId: options.buildId, + }; + } + debugInstall(`Downloading binary from ${url}`); + debugTime('download'); + await downloadFile(url, archivePath, options.downloadProgressCallback); + debugTimeEnd('download'); + return { + path: archivePath, + browser: options.browser, + platform: options.platform, + buildId: options.buildId, + }; + } + + const outputPath = structure.installationDir( + options.browser, + options.platform, + options.buildId + ); + if (existsSync(outputPath)) { + return { + path: outputPath, + browser: options.browser, + platform: options.platform, + buildId: options.buildId, + }; + } + 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 { + path: outputPath, + browser: options.browser, + platform: options.platform, + buildId: options.buildId, + }; +} + +/** + * @public + */ +export async function canDownload(options: InstallOptions): Promise { + 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..9f8c8f20ed --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/launch.ts @@ -0,0 +1,494 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import childProcess from 'child_process'; +import {accessSync} from 'fs'; +import os from 'os'; +import path from 'path'; +import readline from 'readline'; + +import { + Browser, + BrowserPlatform, + executablePathByBrowser, + resolveSystemExecutablePath, + 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 { + options.platform ??= detectBrowserPlatform(); + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } + const installationDir = new Cache(options.cacheDir).installationDir( + options.browser, + options.platform, + options.buildId + ); + return path.join( + installationDir, + executablePathByBrowser[options.browser](options.platform, options.buildId) + ); +} + +/** + * @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 type LaunchOptions = { + executablePath: string; + pipe?: boolean; + dumpio?: boolean; + args?: string[]; + env?: Record; + handleSIGINT?: boolean; + handleSIGTERM?: boolean; + handleSIGHUP?: boolean; + detached?: boolean; + onExit?: () => Promise; +}; + +/** + * @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; + + 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, + }); + + debugLaunch(`Launching ${this.#executablePath} ${this.#args.join(' ')}`, { + detached: opts.detached, + env: opts.env, + stdio, + }); + + this.#browserProcess = childProcess.spawn( + this.#executablePath, + this.#args, + { + detached: opts.detached, + env: opts.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 { + await this.#runHooks(); + if (!this.#exited) { + this.kill(); + } + return this.#browserProcessExiting; + } + + hasClosed(): Promise { + 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?: number): Promise { + 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 ? setTimeout(onTimeout, timeout) : 0; + + 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..a086c1c3b9 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/main-cli.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +/** + * Copyright 2023 Google Inc. 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. + */ + +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..14ca6a838a --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/main.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +export { + launch, + computeExecutablePath, + computeSystemExecutablePath, + TimeoutError, + LaunchOptions, + ComputeExecutablePathOptions as Options, + SystemOptions, + CDP_WEBSOCKET_ENDPOINT_REGEX, + WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX, + Process, +} from './launch.js'; +export {install, canDownload, InstallOptions} from './install.js'; +export {detectBrowserPlatform} from './detectPlatform.js'; +export { + resolveBuildId, + Browser, + BrowserPlatform, + ChromeReleaseChannel, + createProfile, + ProfileOptions, +} 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..ef01b990b7 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "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/chrome-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts new file mode 100644 index 0000000000..c0404baad8 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import assert from 'assert'; +import path from 'path'; + +import { + BrowserPlatform, + ChromeReleaseChannel, +} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + resolveDownloadUrl, + relativeExecutablePath, + resolveSystemExecutablePath, +} 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.`)); + }); +}); 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..a9a08c9d62 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts @@ -0,0 +1,104 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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..7dc3e7ebaf --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts @@ -0,0 +1,239 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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]); + }); + + 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 { + 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..cef690a6bb --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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..fb4134a663 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import assert from 'assert'; + +import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + resolveDownloadUrl, + relativeExecutablePath, +} from '../../../lib/cjs/browser-data/chromedriver.js'; + +describe('ChromeDriver', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_linux64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_mac64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_mac_arm64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_win32.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_win32.zip' + ); + }); + + it('should resolve executable paths', () => { + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.LINUX, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN32, '12372323'), + 'chromedriver.exe' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN64, '12372323'), + '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..52c23d22c2 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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' + ) + ) + ); + + await new CLI(tmpDir, createMockedReadlineInterface('no')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chromedriver', + `linux-${testChromeDriverBuildId}`, + '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..fb725de010 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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)); + }); +}); 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..5dae0d0d29 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts @@ -0,0 +1,108 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import assert from 'assert'; +import path from 'path'; + +import { + BrowserPlatform, + ChromeReleaseChannel, +} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + resolveDownloadUrl, + relativeExecutablePath, + resolveSystemExecutablePath, +} 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') + ); + }); + + 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.`)); + }); +}); 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..7fade9e52d --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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..ec93e0c353 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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, + 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()}`, + ]); + }); + + 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 () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `firefox@latest`, + `--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..cad4ef5fa3 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts @@ -0,0 +1,107 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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..29d5974c73 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/launch.ts new file mode 100644 index 0000000000..88388d8d6d --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/launch.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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/tsconfig.json b/remote/test/puppeteer/packages/browsers/test/src/tsconfig.json new file mode 100644 index 0000000000..63dd3f1eeb --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "../build" + }, + "references": [{"path": "../../tsconfig.json"}] +} 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..9afb1bb763 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/utils.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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 = {}; + +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..606827fe3c --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/versions.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +export const testChromeBuildId = '113.0.5672.0'; +export const testChromiumBuildId = '1083080'; +// TODO: We can add a Cron job to auto-update on change. +// Firefox keeps only `latest` version of Nightly builds. +export const testFirefoxBuildId = '114.0a1'; +export const testChromeDriverBuildId = '112.0.5615.49'; 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..9ee861f878 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs @@ -0,0 +1,81 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +/** + * Downloads test browser binaries to test/cache/server folder that + * mirrors the structure of the download server. + */ + +import {BrowserPlatform, install} from '@puppeteer/browsers'; +import path from 'path'; +import fs from 'fs'; + +import * as versions from '../test/build/versions.js'; +import {downloadPaths} from '../lib/esm/browser-data/browser-data.js'; + +function getBrowser(str) { + const regex = /test(.+)BuildId/; + const match = str.match(regex); + + if (match && match[1]) { + return match[1].toLowerCase(); + } else { + return null; + } +} + +const cacheDir = path.normalize(path.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 = path.join( + cacheDir, + 'server', + ...downloadPaths[browser](platform, buildId) + ); + + if (fs.existsSync(targetPath)) { + continue; + } + + const result = await install({ + browser, + buildId, + platform, + cacheDir: path.join(cacheDir, 'tmp'), + unpack: false, + }); + + fs.mkdirSync(path.dirname(targetPath), { + recursive: true, + }); + fs.copyFileSync(result.path, targetPath); + } +} + +fs.rmSync(path.join(cacheDir, 'tmp'), { + recursive: true, + force: true, + maxRetries: 10, +}); diff --git a/remote/test/puppeteer/packages/browsers/tsconfig.json b/remote/test/puppeteer/packages/browsers/tsconfig.json new file mode 100644 index 0000000000..a219f8b704 --- /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/ng-schematics/.eslintignore b/remote/test/puppeteer/packages/ng-schematics/.eslintignore new file mode 100644 index 0000000000..69132ca336 --- /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/ \ No newline at end of file diff --git a/remote/test/puppeteer/packages/ng-schematics/.gitignore b/remote/test/puppeteer/packages/ng-schematics/.gitignore new file mode 100644 index 0000000000..93d9982369 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/.gitignore @@ -0,0 +1,3 @@ + +# Sandbox +sandbox/ \ No newline at end of file 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..3cc843df1e --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## [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..3efc89f17c --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/README.md @@ -0,0 +1,67 @@ +# Puppeteer Angular Schematic + +Adds Puppeteer-based e2e tests to your Angular project. + +## Usage + +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 frameworks: + +- **Jasmine** [https://jasmine.github.io/] +- **Jest** [https://jestjs.io/] +- **Mocha** [https://mochajs.org/] +- **Node Test Runner** _(Experimental)_ [https://nodejs.org/api/test.html] + +With the schematics installed you can run E2E tests: + +```bash +ng e2e +``` + +> Note: Command spawns it's own server on the same port `ng serve` does. + +## Options + +When adding schematics to your project you can to provide following options: + +| Option | Description | Value | Required | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- | +| `--isDefaultTester` | When true, replaces default `ng e2e` command. | `boolean` | `true` | +| `--exportConfig` | When true, creates an empty [Puppeteer configuration](https://pptr.dev/guides/configuration) file. (`.puppeteerrc.cjs`) | `boolean` | `true` | +| `--testingFramework` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` | + +## 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 + +For easier development we provide a script to auto-generate the Angular project to test against. Simply run: + +```bash +npm run sandbox -- --init +``` + +After that to run `@puppeteer/ng-schematics` against the Sandbox Angular project run: + +```bash +npm run sandbox +# or to auto-build and then run schematics +npm run sandbox -- --build +``` + +### 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 +``` diff --git a/remote/test/puppeteer/packages/ng-schematics/package-lock.json b/remote/test/puppeteer/packages/ng-schematics/package-lock.json new file mode 100644 index 0000000000..11dd119c2b --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/package-lock.json @@ -0,0 +1,1098 @@ +{ + "name": "angular", + "version": "0.2.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "angular", + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "^14.2.6", + "@angular-devkit/schematics": "^14.2.6", + "typescript": "~4.7.2" + }, + "devDependencies": { + "@types/jasmine": "~4.0.0", + "@types/node": "^14.15.0", + "jasmine": "^4.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.6.tgz", + "integrity": "sha512-qtRSdRm/h7C3ya04PJTDgQXV6mM8Y4RakANX1GTSXetCf9AVSxg74NJX76DWUgiHT4JiPYnJgJU6Hr/L0H6JOQ==", + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.6.tgz", + "integrity": "sha512-mSFtc4M49mWrYsgJx/P6bA6SzXb8SeZqmppKRMoEQxiXI1bwFdGLNWzAmzEsGvS96h/nPIaOfcX5cKJSp++4FA==", + "dependencies": { + "@angular-devkit/core": "14.2.6", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@types/jasmine": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", + "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.18.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.32.tgz", + "integrity": "sha512-Y6S38pFr04yb13qqHf8uk1nHE3lXgQ30WZbv1mLliV9pt0NjvqdWttLcrOYLnXbOafknVYRHZGoMSpR9UwfYow==", + "dev": true + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "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": { + "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/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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "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/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": "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/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/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/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.7.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "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/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/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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/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/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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "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/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-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/jasmine": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.4.0.tgz", + "integrity": "sha512-xrbOyYkkCvgduNw7CKktDtNb+BwwBv/zvQeHpTkbxqQ37AJL5V4sY3jHoMIJPP/hTc3QxLVwOyxc87AqA+kw5g==", + "dev": true, + "dependencies": { + "glob": "^7.1.6", + "jasmine-core": "^4.4.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.4.0.tgz", + "integrity": "sha512-+l482uImx5BVd6brJYlaHe2UwfKoZBqQfNp20ZmdNfsjGFTemGfqHLsXjKEW23w9R/m8WYeFc9JmIgjj6dUtAA==", + "dev": true + }, + "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/jsonc-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==" + }, + "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/magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "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/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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "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/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/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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/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/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "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/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/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "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/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/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/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.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/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/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + }, + "dependencies": { + "@angular-devkit/core": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.6.tgz", + "integrity": "sha512-qtRSdRm/h7C3ya04PJTDgQXV6mM8Y4RakANX1GTSXetCf9AVSxg74NJX76DWUgiHT4JiPYnJgJU6Hr/L0H6JOQ==", + "requires": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + } + }, + "@angular-devkit/schematics": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.6.tgz", + "integrity": "sha512-mSFtc4M49mWrYsgJx/P6bA6SzXb8SeZqmppKRMoEQxiXI1bwFdGLNWzAmzEsGvS96h/nPIaOfcX5cKJSp++4FA==", + "requires": { + "@angular-devkit/core": "14.2.6", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + } + }, + "@types/jasmine": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", + "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "dev": true + }, + "@types/node": { + "version": "14.18.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.32.tgz", + "integrity": "sha512-Y6S38pFr04yb13qqHf8uk1nHE3lXgQ30WZbv1mLliV9pt0NjvqdWttLcrOYLnXbOafknVYRHZGoMSpR9UwfYow==", + "dev": true + }, + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "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, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==" + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "requires": { + "clone": "^1.0.2" + } + }, + "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==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "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" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==" + }, + "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==" + }, + "jasmine": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.4.0.tgz", + "integrity": "sha512-xrbOyYkkCvgduNw7CKktDtNb+BwwBv/zvQeHpTkbxqQ37AJL5V4sY3jHoMIJPP/hTc3QxLVwOyxc87AqA+kw5g==", + "dev": true, + "requires": { + "glob": "^7.1.6", + "jasmine-core": "^4.4.0" + } + }, + "jasmine-core": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.4.0.tgz", + "integrity": "sha512-+l482uImx5BVd6brJYlaHe2UwfKoZBqQfNp20ZmdNfsjGFTemGfqHLsXjKEW23w9R/m8WYeFc9JmIgjj6dUtAA==", + "dev": true + }, + "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==" + }, + "jsonc-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==" + }, + "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==", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "requires": { + "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" + } + }, + "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==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "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==" + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "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==" + }, + "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==" + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "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==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "requires": { + "defaults": "^1.0.3" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + } +} 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..e7f645d4d3 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/package.json @@ -0,0 +1,68 @@ +{ + "name": "@puppeteer/ng-schematics", + "version": "0.2.0", + "description": "Puppeteer Angular schematics", + "scripts": { + "build": "wireit", + "clean": "tsc -b --clean && rm -rf lib && rm -rf test/build", + "dev:test": "npm run test --watch", + "dev": "npm run build --watch", + "test": "wireit", + "sandbox": "node tools/sandbox.js" + }, + "wireit": { + "build": { + "command": "tsc -b && node tools/copySchemaFiles.js", + "clean": "if-file-deleted", + "files": [ + "tsconfig.json", + "tsconfig.spec.json", + "src/**", + "test/src/**" + ], + "output": [ + "lib/**", + "test/build/**" + ] + }, + "test": { + "command": "mocha", + "dependencies": [ + "build" + ] + } + }, + "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.0.0" + }, + "dependencies": { + "@angular-devkit/architect": "^0.1502.7", + "@angular-devkit/core": "^15.2.7", + "@angular-devkit/schematics": "^15.2.7" + }, + "devDependencies": { + "@types/node": "^14.15.0", + "@schematics/angular": "^14.2.8", + "@angular/cli": "^15.2.2" + }, + "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..45aec95152 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts @@ -0,0 +1,138 @@ +import {spawn} from 'child_process'; + +import { + createBuilder, + BuilderContext, + BuilderOutput, + targetFromTargetString, + BuilderRun, +} from '@angular-devkit/architect'; +import {JsonObject} from '@angular-devkit/core'; + +import {PuppeteerBuilderOptions} from './types.js'; + +const terminalStyles = { + blue: '\u001b[34m', + green: '\u001b[32m', + bold: '\u001b[1m', + reverse: '\u001b[7m', + clear: '\u001b[0m', +}; + +function getError(executable: string, args: string[]) { + return ( + `Puppeteer E2E tests failed!` + + '\n' + + `Error running '${executable}' with arguments '${args.join(' ')}'.` + + `\n` + + 'Please look at the output above to determine the issue!' + ); +} + +function getExecutable(command: string[]) { + const executable = command.shift()!; + const error = getError(executable, command); + + if (executable === 'node') { + return { + executable: executable, + args: command, + error, + }; + } + + return { + executable: `./node_modules/.bin/${executable}`, + args: command, + error, + }; +} + +async function executeCommand(context: BuilderContext, command: string[]) { + await new Promise((resolve, reject) => { + context.logger.debug(`Trying to execute command - ${command.join(' ')}.`); + const {executable, args, error} = getExecutable(command); + + const child = spawn(executable, args, { + cwd: context.workspaceRoot, + stdio: 'inherit', + }); + + child.on('error', message => { + 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' = 'info' +): void { + const color = type === 'info' ? terminalStyles.blue : terminalStyles.green; + context.logger.info( + `${terminalStyles.bold}${terminalStyles.reverse}${color}${message}${terminalStyles.clear}` + ); +} + +async function startServer( + options: PuppeteerBuilderOptions, + context: BuilderContext +): Promise { + 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: 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 { + let server: BuilderRun | null = null; + try { + server = await startServer(options, context); + + message('\nRunning tests...\n', context); + for (const command of options.commands) { + await executeCommand(context, command); + } + + message('\nTest ran successfully!', context, 'success'); + return {success: true}; + } catch (error) { + if (error instanceof Error) { + return {success: false, error: error.message}; + } + return {success: false, error: error as any}; + } finally { + if (server) { + await server.stop(); + } + } +} + +export default createBuilder(executeE2ETest) as any; 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..42e80f46d0 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json @@ -0,0 +1,22 @@ +{ + "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." + } + }, + "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..b25d51b5f1 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {JsonObject} from '@angular-devkit/core'; + +type Command = [string, ...string[]]; + +export interface PuppeteerBuilderOptions extends JsonObject { + commands: Command[]; + devServerTarget: string; +} 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..0cf38b799b --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json @@ -0,0 +1,10 @@ +{ + "$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" + } + } +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/.puppeteerrc.cjs.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/.puppeteerrc.cjs.template new file mode 100644 index 0000000000..04f3f0d832 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/.puppeteerrc.cjs.template @@ -0,0 +1,4 @@ +/** + * @type {import("puppeteer").Configuration} + */ +module.exports = {}; diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.e2e.ts.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.e2e.ts.template new file mode 100644 index 0000000000..2f98ef7d46 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.e2e.ts.template @@ -0,0 +1,59 @@ +import * as puppeteer from 'puppeteer'; +<% if(testingFramework == 'node') { %> +import { + describe, + it, + before, + beforeEach, + after, + afterEach, +} from 'node:test'; +<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %> +import * as assert from 'assert'; +<% } %> + +describe('App test', function () { + let browser: puppeteer.Browser; + let page: puppeteer.Page; + +<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %> + beforeAll(async () => { + browser = await puppeteer.launch(); + }); +<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %> + before(async () => { + browser = await puppeteer.launch(); + }); +<% } %> + + beforeEach(async () => { + page = await browser.newPage(); + await page.goto('<%= baseUrl %>'); + }); + + afterEach(async () => { + await page.close(); + }); + +<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %> + afterAll(async () => { + await browser.close(); + }); +<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %> + after(async () => { + await browser.close(); + }); +<% } %> + + it('is running', async function () { + const element = await page.waitForSelector( + 'text/<%= project %> app is running!' + ); + +<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %> + expect(element).not.toBeNull(); +<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %> + assert.ok(element); +<% } %> + }); +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tsconfig.json.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tsconfig.json.template new file mode 100644 index 0000000000..438d04725f --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tsconfig.json.template @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../tsconfig.json", + "compilerOptions": { +<% if(testingFramework == 'jest') { %> + "esModuleInterop": true, +<% } %><% if(testingFramework == 'node') { %> + "module": "CommonJS", + "rootDir": "tests/", + "outDir": "test/", +<% } %> + "types": ["<%= testingFramework %>"] + }, + "include": ["tests/**/*.e2e.ts"] +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/helpers/babel.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/helpers/babel.js new file mode 100644 index 0000000000..06259b39c8 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/helpers/babel.js @@ -0,0 +1,4 @@ +require('@babel/register')({ + extensions: ['.js', '.ts'], + presets: ['@babel/preset-env', '@babel/preset-typescript'], +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/support/jasmine.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/support/jasmine.json new file mode 100644 index 0000000000..7100e102f1 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/support/jasmine.json @@ -0,0 +1,9 @@ +{ + "spec_dir": "e2e", + "spec_files": ["**/*[eE]2[eE].ts"], + "helpers": ["helpers/babel.js", "helpers/**/*.{js|ts}"], + "env": { + "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..99cc594c97 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js @@ -0,0 +1,11 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + testMatch: ['/tests/**/?(*.)+(e2e).[tj]s?(x)'], + preset: 'ts-jest', + 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..63ca85e3eb --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js @@ -0,0 +1,4 @@ +module.exports = { + file: ['e2e/babel.js'], + spec: './e2e/tests/**/*.e2e.ts', +}; diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/babel.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/babel.js new file mode 100644 index 0000000000..06259b39c8 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/babel.js @@ -0,0 +1,4 @@ +require('@babel/register')({ + extensions: ['.js', '.ts'], + presets: ['@babel/preset-env', '@babel/preset-typescript'], +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/node/e2e/.gitignore.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/node/e2e/.gitignore.template new file mode 100644 index 0000000000..35250fac0e --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/node/e2e/.gitignore.template @@ -0,0 +1,3 @@ +# Compiled e2e tests output Node auto resolves files in folders named 'test' + +test/ \ No newline at end of file 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..49c56bfc2f --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts @@ -0,0 +1,127 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; +import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks'; +import {of} from 'rxjs'; +import {concatMap, map, scan} from 'rxjs/operators'; + +import { + addBaseFiles, + addFrameworkFiles, + getNgCommandName, +} from '../utils/files.js'; +import {getAngularConfig} from '../utils/json.js'; +import { + addPackageJsonDependencies, + addPackageJsonScripts, + getDependenciesFromOptions, + getPackageLatestNpmVersion, + DependencyType, + type NodePackage, + updateAngularJsonScripts, +} from '../utils/packages.js'; +import {type SchematicsOptions} 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 ngAdd(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext) => { + return chain([ + addDependencies(options), + addPuppeteerFiles(options), + addOtherFiles(options), + updateScripts(options), + 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()); + + return tree; + }) + ); + }; +} + +function updateScripts(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext): Tree => { + context.logger.debug('Updating "package.json" scripts'); + const angularJson = getAngularConfig(tree); + const projects = Object.keys(angularJson['projects']); + + if (projects.length === 1) { + const name = getNgCommandName(options); + const prefix = options.isDefaultTester ? '' : `run ${projects[0]}:`; + return addPackageJsonScripts(tree, [ + { + name, + script: `ng ${prefix}${name}`, + }, + ]); + } + return tree; + }; +} + +function addPuppeteerFiles(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext) => { + context.logger.debug('Adding Puppeteer base files.'); + const {projects} = getAngularConfig(tree); + + return addBaseFiles(tree, context, { + projects, + options, + }); + }; +} + +function addOtherFiles(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext) => { + context.logger.debug('Adding Puppeteer additional files.'); + const {projects} = getAngularConfig(tree); + + return addFrameworkFiles(tree, context, { + projects, + options, + }); + }; +} + +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..77cb5bdd48 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "Puppeteer", + "title": "Puppeteer Install Schema", + "type": "object", + "properties": { + "isDefaultTester": { + "description": "", + "type": "boolean", + "default": true, + "x-prompt": "Use Puppeteer as default `ng e2e` command?" + }, + "exportConfig": { + "description": "", + "type": "boolean", + "default": false, + "x-prompt": "Export default Puppeteer config file?" + }, + "testingFramework": { + "description": "", + "type": "string", + "enum": ["jasmine", "jest", "mocha", "node"], + "default": "jasmine", + "x-prompt": { + "message": "With what Testing Library do you wish to integrate?", + "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 (Experimental: Node v18) [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..9518e1d996 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts @@ -0,0 +1,162 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {relative, resolve} from 'path'; + +import {getSystemPath, normalize, strings} from '@angular-devkit/core'; +import { + SchematicContext, + Tree, + apply, + applyTemplates, + chain, + filter, + mergeWith, + move, + url, +} from '@angular-devkit/schematics'; + +import {SchematicsOptions, TestingFramework} from './types.js'; + +export interface FilesOptions { + projects: any; + options: SchematicsOptions; + applyPath: string; + relativeToWorkspacePath: string; + movePath?: string; + filterPredicate?: (path: string) => boolean; +} + +const PUPPETEER_CONFIG_TEMPLATE = '.puppeteerrc.cjs.template'; + +export function addFiles( + tree: Tree, + context: SchematicContext, + { + projects, + options, + applyPath, + movePath, + relativeToWorkspacePath, + filterPredicate, + }: FilesOptions +): any { + return chain( + Object.keys(projects).map(name => { + const project = projects[name]; + const projectPath = resolve(getSystemPath(normalize(project.root))); + const workspacePath = resolve(getSystemPath(normalize(''))); + + const relativeToWorkspace = relative( + `${projectPath}${relativeToWorkspacePath}`, + workspacePath + ); + + const baseUrl = getProjectBaseUrl(project); + + return mergeWith( + apply(url(applyPath), [ + filter( + filterPredicate ?? + (() => { + return true; + }) + ), + move(movePath ? `${project.root}${movePath}` : project.root), + applyTemplates({ + ...options, + ...strings, + root: project.root ? `${project.root}/` : project.root, + baseUrl, + project: name, + relativeToWorkspace, + }), + ]) + ); + }) + )(tree, context); +} + +function getProjectBaseUrl(project: any): string { + let options = {protocol: 'http', port: 4200, host: 'localhost'}; + + if (project.architect?.serve?.options) { + const projectOptions = project.architect?.serve?.options; + + options = {...options, ...projectOptions}; + options.protocol = projectOptions.ssl ? 'https' : 'http'; + } + + return `${options.protocol}://${options.host}:${options.port}`; +} + +export function addBaseFiles( + tree: Tree, + context: SchematicContext, + filesOptions: Omit +): any { + const options: FilesOptions = { + ...filesOptions, + applyPath: './files/base', + relativeToWorkspacePath: `/`, + filterPredicate: path => { + return path.includes(PUPPETEER_CONFIG_TEMPLATE) && + !filesOptions.options.exportConfig + ? false + : true; + }, + }; + + return addFiles(tree, context, options); +} + +export function addFrameworkFiles( + tree: Tree, + context: SchematicContext, + filesOptions: Omit +): any { + const testingFramework = filesOptions.options.testingFramework; + const options: FilesOptions = { + ...filesOptions, + applyPath: `./files/${testingFramework}`, + relativeToWorkspacePath: `/`, + }; + + return addFiles(tree, context, options); +} + +export function getScriptFromOptions(options: SchematicsOptions): string[][] { + switch (options.testingFramework) { + case TestingFramework.Jasmine: + return [[`jasmine`, '--config=./e2e/support/jasmine.json']]; + case TestingFramework.Jest: + return [[`jest`, '-c', 'e2e/jest.config.js']]; + case TestingFramework.Mocha: + return [[`mocha`, '--config=./e2e/.mocharc.js']]; + case TestingFramework.Node: + return [ + [`tsc`, '-p', 'e2e/tsconfig.json'], + ['node', '--test', 'e2e/'], + ]; + } +} + +export function getNgCommandName(options: SchematicsOptions): string { + if (options.isDefaultTester) { + 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..f48eab399e --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {SchematicsException, Tree} from '@angular-devkit/schematics'; + +export function getJsonFileAsObject( + tree: Tree, + path: string +): Record { + 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 { + return JSON.stringify(object, null, 2); +} + +export function getAngularConfig(tree: Tree): Record { + return getJsonFileAsObject(tree, './angular.json'); +} 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..b3724be90f --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts @@ -0,0 +1,204 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {get} from 'https'; + +import {Tree} from '@angular-devkit/schematics'; + +import {getNgCommandName, getScriptFromOptions} from './files.js'; +import { + getAngularConfig, + getJsonFileAsObject, + getObjectAsJson, +} from './json.js'; +import {SchematicsOptions, TestingFramework} 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 { + return new Promise(resolve => { + let version = 'latest'; + + return get(`https://registry.npmjs.org/${name}`, res => { + let data = ''; + + res.on('data', (chunk: any) => { + 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, + 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']; + const babelPackages = [ + '@babel/core', + '@babel/register', + '@babel/preset-env', + '@babel/preset-typescript', + ]; + + switch (options.testingFramework) { + case TestingFramework.Jasmine: + dependencies.push('jasmine', ...babelPackages); + break; + case TestingFramework.Jest: + dependencies.push('jest', '@types/jest', 'ts-jest'); + break; + case TestingFramework.Mocha: + dependencies.push('mocha', '@types/mocha', ...babelPackages); + break; + case TestingFramework.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 commands = getScriptFromOptions(options); + const name = getNgCommandName(options); + + Object.keys(angularJson['projects']).forEach(project => { + const e2eScript = [ + { + name, + value: { + builder: '@puppeteer/ng-schematics:puppeteer', + options: { + commands, + devServerTarget: `${project}:serve`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }, + }, + ]; + + updateJsonValues( + angularJson['projects'][project], + 'architect', + e2eScript, + overwrite + ); + }); + + tree.overwrite('./angular.json', getObjectAsJson(angularJson)); + + 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..c59f90e69d --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +export enum TestingFramework { + Jasmine = 'jasmine', + Jest = 'jest', + Mocha = 'mocha', + Node = 'node', +} + +export interface SchematicsOptions { + isDefaultTester: boolean; + exportConfig: boolean; + testingFramework: TestingFramework; +} diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/index.spec.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/index.spec.ts new file mode 100644 index 0000000000..59636dbb38 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/test/src/index.spec.ts @@ -0,0 +1,212 @@ +import https from 'https'; +import {join} from 'path'; + +import {JsonObject} from '@angular-devkit/core'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing/schematic-test-runner'; +import expect from 'expect'; +import sinon from 'sinon'; + +const WORKSPACE_OPTIONS = { + name: 'workspace', + newProjectRoot: 'projects', + version: '14.0.0', +}; + +const APPLICATION_OPTIONS = { + name: 'sandbox', +}; + +function getProjectFile(file: string): string { + return `/${WORKSPACE_OPTIONS.newProjectRoot}/${APPLICATION_OPTIONS.name}/${file}`; +} + +function getAngularJsonScripts( + tree: UnitTestTree, + isDefault = true +): { + builder: string; + configurations: Record; + options: Record; +} { + const angularJson = tree.readJson('angular.json') as any; + const e2eScript = isDefault ? 'e2e' : 'puppeteer'; + return angularJson['projects']?.[APPLICATION_OPTIONS.name]?.['architect'][ + e2eScript + ]; +} + +function getPackageJson(tree: UnitTestTree): { + scripts: Record; + devDependencies: string[]; +} { + const packageJson = tree.readJson('package.json') as JsonObject; + return { + scripts: packageJson['scripts'] as any, + devDependencies: Object.keys( + packageJson['devDependencies'] as Record + ), + }; +} + +async function buildTestingTree(userOptions?: Record) { + const runner = new SchematicTestRunner( + 'schematics', + join(__dirname, '../../lib/schematics/collection.json') + ); + const options = { + isDefaultTester: true, + exportConfig: false, + testingFramework: 'jasmine', + ...userOptions, + }; + let workingTree: UnitTestTree; + + // Build workspace + workingTree = await runner + .runExternalSchematicAsync( + '@schematics/angular', + 'workspace', + WORKSPACE_OPTIONS + ) + .toPromise(); + // Build dummy application + workingTree = await runner + .runExternalSchematicAsync( + '@schematics/angular', + 'application', + APPLICATION_OPTIONS, + workingTree + ) + .toPromise(); + + return await runner + .runSchematicAsync('ng-add', options, workingTree) + .toPromise(); +} + +describe('@puppeteer/ng-schematics: ng-add', () => { + // Stop outgoing Request for version fetching + before(() => { + const httpsGetStub = sinon.stub(https, 'get'); + httpsGetStub.returns({ + on: (_: any, callback: () => void) => { + callback(); + }, + } as any); + }); + + after(() => { + sinon.restore(); + }); + + it('should create base files and update to "package.json"', async () => { + const tree = await buildTestingTree(); + const {devDependencies, scripts} = getPackageJson(tree); + const {builder, configurations} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/tsconfig.json')); + expect(tree.files).toContain(getProjectFile('e2e/tests/app.e2e.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', + }, + }); + }); + + it('should update create proper "ng" command for non default tester', async () => { + const tree = await buildTestingTree({ + isDefaultTester: false, + }); + const {scripts} = getPackageJson(tree); + const {builder} = getAngularJsonScripts(tree, false); + + expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer'); + expect(builder).toBe('@puppeteer/ng-schematics:puppeteer'); + }); + + it('should create Puppeteer config', async () => { + const {files} = await buildTestingTree({ + exportConfig: true, + }); + + expect(files).toContain(getProjectFile('.puppeteerrc.cjs')); + }); + + it('should not create Puppeteer config', async () => { + const {files} = await buildTestingTree({ + exportConfig: false, + }); + + expect(files).not.toContain(getProjectFile('.puppeteerrc.cjs')); + }); + + it('should create Jasmine files and update "package.json"', async () => { + const tree = await buildTestingTree({ + testingFramework: 'jasmine', + }); + const {devDependencies} = getPackageJson(tree); + const {options} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/support/jasmine.json')); + expect(tree.files).toContain(getProjectFile('e2e/helpers/babel.js')); + expect(devDependencies).toContain('jasmine'); + expect(devDependencies).toContain('@babel/core'); + expect(devDependencies).toContain('@babel/register'); + expect(devDependencies).toContain('@babel/preset-typescript'); + expect(options['commands']).toEqual([ + [`jasmine`, '--config=./e2e/support/jasmine.json'], + ]); + }); + + it('should create Jest files and update "package.json"', async () => { + const tree = await buildTestingTree({ + testingFramework: 'jest', + }); + const {devDependencies} = getPackageJson(tree); + const {options} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/jest.config.js')); + expect(devDependencies).toContain('jest'); + expect(devDependencies).toContain('@types/jest'); + expect(devDependencies).toContain('ts-jest'); + expect(options['commands']).toEqual([[`jest`, '-c', 'e2e/jest.config.js']]); + }); + + it('should create Mocha files and update "package.json"', async () => { + const tree = await buildTestingTree({ + testingFramework: 'mocha', + }); + const {devDependencies} = getPackageJson(tree); + const {options} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/.mocharc.js')); + expect(tree.files).toContain(getProjectFile('e2e/babel.js')); + expect(devDependencies).toContain('mocha'); + expect(devDependencies).toContain('@types/mocha'); + expect(devDependencies).toContain('@babel/core'); + expect(devDependencies).toContain('@babel/register'); + expect(devDependencies).toContain('@babel/preset-typescript'); + expect(options['commands']).toEqual([ + [`mocha`, '--config=./e2e/.mocharc.js'], + ]); + }); + + it('should create Node files"', async () => { + const tree = await buildTestingTree({ + testingFramework: 'node', + }); + const {options} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/.gitignore')); + expect(options['commands']).toEqual([ + [`tsc`, '-p', 'e2e/tsconfig.json'], + ['node', '--test', 'e2e/'], + ]); + }); +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.js b/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.js new file mode 100644 index 0000000000..bca61b8101 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.js @@ -0,0 +1,72 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +const fs = require('fs/promises'); +const {join} = require('path'); +const path = require('path'); + +/** + * + * @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 = join(__dirname, '..', 'src'); + const outputDir = 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/sandbox.js b/remote/test/puppeteer/packages/ng-schematics/tools/sandbox.js new file mode 100644 index 0000000000..44f131bfac --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/tools/sandbox.js @@ -0,0 +1,104 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +const {spawn} = require('child_process'); +const {readFile, writeFile} = require('fs/promises'); +const {join} = require('path'); +const {cwd} = require('process'); + +const isInit = process.argv.indexOf('--init') !== -1; +const isBuild = process.argv.indexOf('--build') !== -1; +const commands = { + build: ['npm run build'], + createSandbox: ['npx ng new sandbox --defaults'], + runSchematics: [ + { + command: 'npm run schematics', + options: { + cwd: join(cwd(), '/sandbox/'), + }, + }, + ], +}; +const scripts = { + // 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: 'npm run delete:file && schematics ../:ng-add --dry-run=false', +}; +/** + * + * @param {string | object} toExecute + * @returns {Promise} + */ +async function executeCommand(commands) { + for (const toExecute of commands) { + let executable; + let args; + let options = {}; + if (typeof toExecute === 'string') { + [executable, ...args] = toExecute.split(' '); + } else { + [executable, ...args] = toExecute.command.split(' '); + options = toExecute.options ?? {}; + } + + await new Promise((resolve, reject) => { + const createProcess = spawn(executable, args, { + stdio: 'inherit', + shell: true, + ...options, + }); + + createProcess.on('error', message => { + console.error(message); + reject(message); + }); + + createProcess.on('exit', code => { + if (code === 0) { + resolve(true); + } else { + reject(); + } + }); + }); + } +} + +async function main() { + if (isInit) { + await executeCommand(commands.createSandbox); + + const packageJsonFile = join(cwd(), '/sandbox/package.json'); + const packageJson = JSON.parse(await readFile(packageJsonFile)); + packageJson['scripts'] = { + ...packageJson['scripts'], + ...scripts, + }; + await writeFile(packageJsonFile, JSON.stringify(packageJson, null, 2)); + } else { + if (isBuild) { + await executeCommand(commands.build); + } + await executeCommand(commands.runSchematics); + } +} + +main().catch(() => { + console.log('\n'); +}); 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..115c833159 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "tsconfig", + "lib": ["ES2018"], + "module": "CommonJS", + "noEmitOnError": true, + "rootDir": "src/", + "outDir": "lib/", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "types": ["node"], + "target": "ES6" + }, + "include": ["src/**/*"], + "exclude": ["src/**/files/**/*"], + "references": [{"path": "./tsconfig.spec.json"}] +} diff --git a/remote/test/puppeteer/packages/ng-schematics/tsconfig.spec.json b/remote/test/puppeteer/packages/ng-schematics/tsconfig.spec.json new file mode 100644 index 0000000000..8afa532cb1 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "test/src/", + "outDir": "test/build/", + "types": ["node", "mocha"] + }, + "include": ["test/src/**/*"], + "exclude": ["test/build/**/*"] +} 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..41457fd4d4 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md @@ -0,0 +1,1322 @@ +# 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. + +## [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 \ ([#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/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": "/lib/esm/puppeteer/puppeteer-core.d.ts", + + "extends": "./api-extractor.json", + + "dtsRollup": { + "enabled": false + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "/../../docs/.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": "/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..27306ad882 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/package.json @@ -0,0 +1,159 @@ +{ + "name": "puppeteer-core", + "version": "20.1.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.0.0" + }, + "scripts": { + "build:docs": "wireit", + "build:tsc": "wireit", + "build:types": "wireit", + "build": "wireit", + "check": "tsx tools/ensure-correct-devtools-protocol-package", + "clean": "tsc -b --clean && rm -rf lib src/generated", + "generate:package-json": "wireit", + "generate:sources": "wireit", + "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:sources": { + "command": "tsx tools/generate_sources.ts", + "clean": "if-file-deleted", + "files": [ + "../../versions.js", + "src/{injected,templates}/**", + "tools/generate_sources.ts" + ], + "output": [ + "src/generated/*.ts" + ] + }, + "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 && rollup --config rollup.third_party.config.mjs", + "clean": "if-file-deleted", + "dependencies": [ + "generate:package-json", + "generate:sources", + "../browsers:build" + ], + "files": [ + "{compat,src,third_party}/**", + "rollup.third_party.config.mjs" + ], + "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", + "!*.tsbuildinfo" + ], + "author": "The Chromium Authors", + "license": "Apache-2.0", + "dependencies": { + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1120988", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0", + "@puppeteer/browsers": "1.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "devDependencies": { + "mitt": "3.0.0", + "parsel-js": "1.1.0" + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/rollup.third_party.config.mjs b/remote/test/puppeteer/packages/puppeteer-core/rollup.third_party.config.mjs new file mode 100644 index 0000000000..8b40906cbf --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/rollup.third_party.config.mjs @@ -0,0 +1,35 @@ +/** + * Copyright 2022 Google Inc. 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. + */ +import commonjs from '@rollup/plugin-commonjs'; +import {nodeResolve} from '@rollup/plugin-node-resolve'; +import glob from 'glob'; + +export default ['cjs', 'esm'].flatMap(outputType => { + const configs = []; + // Note we don't use path.join here. We cannot since `glob` does not support + // the backslash path separator. + for (const file of glob.sync(`lib/${outputType}/third_party/**/*.js`)) { + configs.push({ + input: file, + output: { + file, + format: outputType, + }, + plugins: [commonjs(), nodeResolve()], + }); + } + return configs; +}); 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..5a08f6ec17 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts @@ -0,0 +1,473 @@ +/** + * Copyright 2017 Google Inc. 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. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import {ChildProcess} from 'child_process'; + +import {Protocol} from 'devtools-protocol'; + +import {EventEmitter} from '../common/EventEmitter.js'; +import type {Target} from '../common/Target.js'; // TODO: move to ./api + +import type {BrowserContext} from './BrowserContext.js'; +import type {Page} from './Page.js'; // TODO: move to ./api + +/** + * BrowserContext options. + * + * @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; + +/** + * @public + */ +export type TargetFilterCallback = ( + target: Protocol.Target.TargetInfo +) => boolean; + +/** + * @internal + */ +export type IsPageTargetCallback = ( + target: Protocol.Target.TargetInfo +) => 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'], + ['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' + | '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 BrowserEmittedEvents { + /** + * Emitted when Puppeteer gets disconnected from the browser instance. This + * might happen because of one of the following: + * + * - browser is closed or crashed + * + * - The {@link Browser.disconnect | browser.disconnect } method 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', +} + +/** + * A Browser is created when Puppeteer connects to a browser instance, either through + * {@link PuppeteerNode.launch} or {@link Puppeteer.connect}. + * + * @remarks + * + * The Browser class extends from Puppeteer's {@link EventEmitter} class and will + * emit various events which are documented in the {@link BrowserEmittedEvents} enum. + * + * @example + * An example of using a {@link Browser} to create a {@link Page}: + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * await browser.close(); + * })(); + * ``` + * + * @example + * An example of disconnecting from and reconnecting to a {@link Browser}: + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * 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. + * browser.disconnect(); + * + * // Use the endpoint to reestablish a connection + * const browser2 = await puppeteer.connect({browserWSEndpoint}); + * // Close the browser. + * await browser2.close(); + * })(); + * ``` + * + * @public + */ +export class Browser extends EventEmitter { + /** + * @internal + */ + constructor() { + super(); + } + + /** + * @internal + */ + _attach(): Promise { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _detach(): void { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + get _targets(): Map { + throw new Error('Not implemented'); + } + + /** + * The spawned browser process. Returns `null` if the browser instance was created with + * {@link Puppeteer.connect}. + */ + process(): ChildProcess | null { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _getIsPageTargetCallback(): IsPageTargetCallback | undefined { + throw new Error('Not implemented'); + } + + /** + * Creates a new incognito browser context. This won't share cookies/cache with other + * browser contexts. + * + * @example + * + * ```ts + * (async () => { + * 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'); + * })(); + * ``` + */ + createIncognitoBrowserContext( + options?: BrowserContextOptions + ): Promise; + createIncognitoBrowserContext(): Promise { + throw new Error('Not implemented'); + } + + /** + * Returns an array of all open browser contexts. In a newly created browser, this will + * return a single instance of {@link BrowserContext}. + */ + browserContexts(): BrowserContext[] { + throw new Error('Not implemented'); + } + + /** + * Returns the default browser context. The default browser context cannot be closed. + */ + defaultBrowserContext(): BrowserContext { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _disposeContext(contextId?: string): Promise; + _disposeContext(): Promise { + throw new Error('Not implemented'); + } + + /** + * The browser websocket endpoint which can be used as an argument to + * {@link Puppeteer.connect}. + * + * @returns The Browser websocket url. + * + * @remarks + * + * The format is `ws://${host}:${port}/devtools/browser/`. + * + * You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`. + * Learn more about the + * {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and + * the {@link + * https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target + * | browser endpoint}. + */ + wsEndpoint(): string { + throw new Error('Not implemented'); + } + + /** + * Promise which resolves to a new {@link Page} object. The Page is created in + * a default browser context. + */ + newPage(): Promise { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _createPageInContext(contextId?: string): Promise; + _createPageInContext(): Promise { + throw new Error('Not implemented'); + } + + /** + * All active targets inside the Browser. In case of multiple browser contexts, returns + * an array with all the targets in all browser contexts. + */ + targets(): Target[] { + throw new Error('Not implemented'); + } + + /** + * The target associated with the browser. + */ + target(): Target { + throw new Error('Not implemented'); + } + + /** + * Searches for a target in all browser contexts. + * + * @param predicate - A function to be run for every target. + * @returns The first target found that matches the `predicate` function. + * + * @example + * + * An example of 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/' + * ); + * ``` + */ + waitForTarget( + predicate: (x: Target) => boolean | Promise, + options?: WaitForTargetOptions + ): Promise; + waitForTarget(): Promise { + throw new Error('Not implemented'); + } + + /** + * An array of all open pages inside the Browser. + * + * @remarks + * + * In case of multiple browser contexts, returns an array with all the pages in all + * browser contexts. Non-visible pages, such as `"background_page"`, will not be listed + * here. You can find them using {@link Target.page}. + */ + pages(): Promise { + throw new Error('Not implemented'); + } + + /** + * A string representing the browser name and version. + * + * @remarks + * + * For headless browser, this is similar to `HeadlessChrome/61.0.3153.0`. For + * non-headless, this is similar to `Chrome/61.0.3153.0`. + * + * The format of browser.version() might change with future releases of browsers. + */ + version(): Promise { + throw new Error('Not implemented'); + } + + /** + * The browser's original user agent. Pages can override the browser user agent with + * {@link Page.setUserAgent}. + */ + userAgent(): Promise { + throw new Error('Not implemented'); + } + + /** + * Closes the browser and all of its pages (if any were opened). The + * {@link Browser} object itself is considered to be disposed and cannot be + * used anymore. + */ + close(): Promise { + throw new Error('Not implemented'); + } + + /** + * Disconnects Puppeteer from the browser, but leaves the browser process running. + * After calling `disconnect`, the {@link Browser} object is considered disposed and + * cannot be used anymore. + */ + disconnect(): void { + throw new Error('Not implemented'); + } + + /** + * Indicates that the browser is connected. + */ + isConnected(): boolean { + throw new Error('Not implemented'); + } +} +/** + * @public + */ +export const enum BrowserContextEmittedEvents { + /** + * 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', +} 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..77fb9b1987 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts @@ -0,0 +1,186 @@ +/** + * Copyright 2017 Google Inc. 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. + */ + +import {EventEmitter} from '../common/EventEmitter.js'; +import {Target} from '../common/Target.js'; + +import type {Permission, Browser} from './Browser.js'; +import {Page} from './Page.js'; + +/** + * BrowserContexts provide a way to operate multiple independent browser + * sessions. When a browser is launched, it has a single BrowserContext used by + * default. The method {@link Browser.newPage | Browser.newPage} creates a page + * in the default browser context. + * + * @remarks + * + * The Browser class extends from Puppeteer's {@link EventEmitter} class and + * will emit various events which are documented in the + * {@link BrowserContextEmittedEvents} enum. + * + * If a page opens another page, e.g. with a `window.open` call, the popup will + * belong to the parent page's browser context. + * + * Puppeteer allows creation of "incognito" browser contexts with + * {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext} + * method. "Incognito" browser contexts don't write any browsing data to disk. + * + * @example + * + * ```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 class BrowserContext extends EventEmitter { + /** + * @internal + */ + constructor() { + super(); + } + + /** + * An array of all active targets inside the browser context. + */ + targets(): Target[] { + throw new Error('Not implemented'); + } + + /** + * This searches for a target in this specific browser context. + * + * @example + * An example of 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/' + * ); + * ``` + * + * @param predicate - A function to be run for every target + * @param options - An object of options. Accepts a timeout, + * which is the maximum wait time in milliseconds. + * Pass `0` to disable the timeout. Defaults to 30 seconds. + * @returns Promise which resolves to the first target found + * that matches the `predicate` function. + */ + waitForTarget( + predicate: (x: Target) => boolean | Promise, + options?: {timeout?: number} + ): Promise; + waitForTarget(): Promise { + throw new Error('Not implemented'); + } + + /** + * An array of all pages inside the browser context. + * + * @returns Promise which resolves to an array of all open pages. + * Non visible pages, such as `"background_page"`, will not be listed here. + * You can find them using {@link Target.page | the target page}. + */ + pages(): Promise { + throw new Error('Not implemented'); + } + + /** + * Returns whether BrowserContext is incognito. + * The default browser context is the only non-incognito browser context. + * + * @remarks + * The default browser context cannot be closed. + */ + isIncognito(): boolean { + throw new Error('Not implemented'); + } + + /** + * @example + * + * ```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. + */ + overridePermissions(origin: string, permissions: Permission[]): Promise; + overridePermissions(): Promise { + throw new Error('Not implemented'); + } + + /** + * Clears all permission overrides for the browser context. + * + * @example + * + * ```ts + * const context = browser.defaultBrowserContext(); + * context.overridePermissions('https://example.com', ['clipboard-read']); + * // do stuff .. + * context.clearPermissionOverrides(); + * ``` + */ + clearPermissionOverrides(): Promise { + throw new Error('Not implemented'); + } + + /** + * Creates a new page in the browser context. + */ + newPage(): Promise { + throw new Error('Not implemented'); + } + + /** + * The browser this browser context belongs to. + */ + browser(): Browser { + throw new Error('Not implemented'); + } + + /** + * Closes the browser context. All the targets that belong to the browser context + * will be closed. + * + * @remarks + * Only incognito browser contexts can be closed. + */ + close(): Promise { + throw new Error('Not implemented'); + } + + get id(): string | undefined { + return undefined; + } +} 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..09c409736e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts @@ -0,0 +1,917 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +import {Protocol} from 'devtools-protocol'; + +import {CDPSession} from '../common/Connection.js'; +import {ExecutionContext} from '../common/ExecutionContext.js'; +import {Frame} from '../common/Frame.js'; +import {MouseClickOptions} from '../common/Input.js'; +import {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; +import { + ElementFor, + EvaluateFuncWith, + HandleFor, + HandleOr, + NodeFor, +} from '../common/types.js'; +import {KeyInput} from '../common/USKeyboardLayout.js'; + +import {JSHandle} from './JSHandle.js'; +import {ScreenshotOptions} from './Page.js'; + +/** + * @public + */ +export interface BoxModel { + content: Point[]; + padding: Point[]; + border: Point[]; + margin: Point[]; + 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 PressOptions { + /** + * Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. + */ + delay?: number; + /** + * If specified, generates an input event with this text. + */ + text?: string; +} + +/** + * @public + */ +export interface Point { + x: number; + y: number; +} + +/** + * 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 `` 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 ` + + + 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..bc376a5045 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/drag-and-drop.html @@ -0,0 +1,46 @@ + + + + Drag-and-drop test + + + +
drag me
+
+ + + 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 @@ + + + + File upload test + + + + + \ 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..fd962c7518 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/keyboard.html @@ -0,0 +1,42 @@ + + + + Keyboard test + + + + + + \ 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 @@ + + + + Rotated button test + + + + + + + + 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 @@ + + + + Scrollable test + + + + + + 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 @@ + + + + Selection Test + + + + + + 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..6d77f3106d --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/textarea.html @@ -0,0 +1,15 @@ + + + + Textarea test + + + + + + + diff --git a/remote/test/puppeteer/test/assets/input/touches-move.html b/remote/test/puppeteer/test/assets/input/touches-move.html new file mode 100644 index 0000000000..212330d46c --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/touches-move.html @@ -0,0 +1,65 @@ + + + + + Drag-and-drop test + + + + +
touch me
+ + + + diff --git a/remote/test/puppeteer/test/assets/input/touches.html b/remote/test/puppeteer/test/assets/input/touches.html new file mode 100644 index 0000000000..4392cfacbd --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/touches.html @@ -0,0 +1,35 @@ + + + + Touch test + + + + + + + \ No newline at end of file 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 @@ + + + + + + Element: wheel event - Scaling_an_element_via_the_wheel - code sample + + +
Scale me with your mouse wheel.
+ + + 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 @@ + 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 @@ + 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 @@ + + 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 @@ + 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 @@ + 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 @@ + 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 @@ + 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..7c259b6673 --- /dev/null +++ b/remote/test/puppeteer/test/assets/lazy-oopif-frame.html @@ -0,0 +1,3 @@ + +
+ \ No newline at end of file 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 @@ + 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 @@ + 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 + 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(); + 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 @@ + + + + + + + + + + + + + + + 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 @@ + +
hello, world!
diff --git a/remote/test/puppeteer/test/assets/oopif.html b/remote/test/puppeteer/test/assets/oopif.html new file mode 100644 index 0000000000..0761e8ab11 --- /dev/null +++ b/remote/test/puppeteer/test/assets/oopif.html @@ -0,0 +1,2 @@ +Navigate within document + \ No newline at end of file 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..52b8ca9020 --- /dev/null +++ b/remote/test/puppeteer/test/assets/p-selectors.html @@ -0,0 +1,13 @@ +
hello + +
+ +
+
\ 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 @@ + + + + + + PDF + + +
PDF Content
+ + 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 @@ + + + + Playground + + + + +
First div
+
+ Second div + Inner span +
+ + \ 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 @@ + + + + Popup + + + I am a popup + + 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 @@ + + + + Popup test + + + + + 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 Binary files /dev/null and b/remote/test/puppeteer/test/assets/pptr.png differ 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 @@ + 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 @@ + 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 @@ + 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 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 @@ + + 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 @@ + 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": [""], + "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 @@ + \ 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 @@ +Woof-Woof 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 @@ + + + + Worker test + + + + + \ 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 @@ + +
+ 123321 +
+ 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..189ae05f97 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt @@ -0,0 +1,16 @@ +[ + { + "url": "http://localhost:/csscoverage/involved.html", + "ranges": [ + { + "start": 149, + "end": 297 + }, + { + "start": 306, + "end": 435 + } + ], + "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/grid-cell-0.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png new file mode 100644 index 0000000000..ff282e989b Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png 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:/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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png 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..52e2a0f6d3 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png 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..e503f801ec Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/transparent.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-chrome/white.jpg 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..4677bdbc4f Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png 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..532dc8db65 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png 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..8e86dc9017 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png 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..d713d27943 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png 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..7a74457869 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png 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..f4e059c300 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png 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..d1431bd91d Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png 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..6d28cddcea Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png 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..2b72c7528b Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png 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..2b72c7528b Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png 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 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png 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..846b810386 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png 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..07890a04b3 Binary files /dev/null and b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png 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..f84e852fd8 --- /dev/null +++ b/remote/test/puppeteer/test/installation/.mocharc.cjs @@ -0,0 +1,21 @@ +// Copyright 2022 Google Inc. 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. + +/** + * @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..0446da820a --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js @@ -0,0 +1,19 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +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..c635de59c4 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js @@ -0,0 +1,32 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +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..a82551b8d1 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs @@ -0,0 +1,19 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +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..475adc5248 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js @@ -0,0 +1,25 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +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/configuration/.puppeteerrc.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs new file mode 100644 index 0000000000..8be5af0037 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs @@ -0,0 +1,8 @@ +const {join} = require('path'); + +/** + * @type {import("puppeteer").PuppeteerConfiguration} + */ +module.exports = { + cacheDirectory: join(__dirname, '.cache', 'puppeteer'), +}; 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..7416522ba3 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js @@ -0,0 +1,20 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +import 'puppeteer'; + +// Should still be reachable. +import 'puppeteer-core/internal/revisions.js'; 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..e44f5aeabd --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs @@ -0,0 +1,20 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +require('puppeteer'); + +// Should still be reachable. +require('puppeteer-core/internal/revisions.js'); 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..63ab015ef8 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js @@ -0,0 +1,25 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +export default { + mode: 'production', + entry: './index.js', + target: 'node', + 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..3176dd7053 --- /dev/null +++ b/remote/test/puppeteer/test/installation/package.json @@ -0,0 +1,54 @@ +{ + "name": "@puppeteer-test/installation", + "version": "latest", + "type": "module", + "private": true, + "scripts": { + "build": "wireit", + "build:packages": "wireit", + "clean": "tsc -b --clean && rm -rf build", + "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": "8.1.0", + "mocha": "10.2.0" + }, + "devDependencies": { + "@types/glob": "8.1.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..83111f3ad3 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/browsers.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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', + }); + assert.strictEqual(result.status, 0); + assert.ok( + result.stdout + .toString('utf-8') + .startsWith('@puppeteer/browsers ') + ); + }); +}); 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..e78c93b159 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/constants.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +import {dirname, join, resolve} from 'path'; +import {fileURLToPath} from 'url'; + +import glob from 'glob'; + +export const PUPPETEER_CORE_PACKAGE_PATH = resolve( + glob.sync('puppeteer-core-*.tgz')[0]! +); +export const PUPPETEER_BROWSERS_PACKAGE_PATH = resolve( + glob.sync('puppeteer-browsers-[0-9]*.tgz')[0]! +); +export const PUPPETEER_PACKAGE_PATH = resolve( + glob.sync('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-configuration.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts new file mode 100644 index 0000000000..f74922844d --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +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', () => { + 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, 1); + assert.equal(files[0], 'chrome'); + + 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..03a57d5437 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +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..edefbd3afe --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +import assert from 'assert'; +import {readdir} from 'fs/promises'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +describe('`puppeteer` with Firefox', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + PUPPETEER_PRODUCT: 'firefox', + }; + }, + }); + + 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'); + }); +}); 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..d0b97bf75f --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +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..6633bdb2e2 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +import assert from 'assert'; +import {readdir} from 'fs/promises'; +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, 1); + assert.equal(files[0], 'chrome'); + 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'); + }); +}); 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..7bb9dda194 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/sandbox.ts @@ -0,0 +1,137 @@ +/** + * Copyright 2022 Google Inc. 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. + */ +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 + ): void; + (title: string, getScriptContent: (cwd: string) => Promise): void; +} + +export interface SandboxOptions { + dependencies?: string[]; + devDependencies?: string[]; + /** + * This should be idempotent. + */ + env?: ((cwd: string) => NodeJS.ProcessEnv) | NodeJS.ProcessEnv; + before?: (cwd: string) => Promise; +} + +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; + } +} + +/** + * Configures mocha before/after hooks to create a temp folder and install + * specified dependencies. + */ +export const configureSandbox = (options: SandboxOptions): void => { + before(async function (): Promise { + 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}); + }; + }); + + after(async function () { + if (!process.env['KEEP_SANDBOX']) { + await rm(this.sandbox, {recursive: true, force: true, maxRetries: 5}); + } else { + console.log('sandbox saved in', this.sandbox); + } + }); +}; 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..404691840a --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/util.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +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 => { + 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..f749dd8a07 --- /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/package.json b/remote/test/puppeteer/test/package.json new file mode 100644 index 0000000000..276b0cddc8 --- /dev/null +++ b/remote/test/puppeteer/test/package.json @@ -0,0 +1,30 @@ +{ + "name": "@puppeteer-test/test", + "version": "latest", + "private": true, + "scripts": { + "build": "wireit", + "clean": "tsc -b --clean && rm -rf build" + }, + "wireit": { + "build": { + "command": "tsc -b", + "clean": "if-file-deleted", + "dependencies": [ + "../packages/puppeteer:build", + "../packages/testserver:build" + ], + "files": [ + "../tools/mochaRunner/**", + "src/**" + ], + "output": [ + "build/**", + "tsconfig.tsbuildinfo" + ] + } + }, + "dependencies": { + "puppeteer": "file:../packages/puppeteer" + } +} diff --git a/remote/test/puppeteer/test/src/CDPSession.spec.ts b/remote/test/puppeteer/test/src/CDPSession.spec.ts new file mode 100644 index 0000000000..b059e72723 --- /dev/null +++ b/remote/test/puppeteer/test/src/CDPSession.spec.ts @@ -0,0 +1,139 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; +import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Target.createCDPSession', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page} = getTestState(); + + const client = await page.target().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} = getTestState(); + let called = 0; + browser.browserContexts()[0]!.on('targetcreated', async target => { + called++; + if (called > 1) { + throw new Error('Too many targets created'); + } + await target.createCDPSession(); + }); + await browser.newPage(); + }); + + it('should send events', async () => { + const {page, server} = getTestState(); + + const client = await page.target().createCDPSession(); + await client.send('Network.enable'); + const events: unknown[] = []; + client.on('Network.requestWillBeSent', event => { + return 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} = getTestState(); + + const client = await page.target().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} = getTestState(); + + const client = await page.target().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} = getTestState(); + + const client = await page.target().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 expose the underlying connection', async () => { + const {page} = getTestState(); + + const client = await page.target().createCDPSession(); + expect(client.connection()).toBeTruthy(); + }); +}); diff --git a/remote/test/puppeteer/test/src/DeviceRequestPrompt.spec.ts b/remote/test/puppeteer/test/src/DeviceRequestPrompt.spec.ts new file mode 100644 index 0000000000..b898b2090f --- /dev/null +++ b/remote/test/puppeteer/test/src/DeviceRequestPrompt.spec.ts @@ -0,0 +1,457 @@ +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import { + DeviceRequestPrompt, + DeviceRequestPromptDevice, + DeviceRequestPromptManager, +} from 'puppeteer-core/internal/common/DeviceRequestPrompt.js'; +import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import {TimeoutSettings} from 'puppeteer-core/internal/common/TimeoutSettings.js'; + +class MockCDPSession extends EventEmitter { + async send(): Promise {} + connection() { + return undefined; + } + async detach() {} + id() { + return '1'; + } +} + +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/test/src/EventEmitter.spec.ts b/remote/test/puppeteer/test/src/EventEmitter.spec.ts new file mode 100644 index 0000000000..ef5a6cc065 --- /dev/null +++ b/remote/test/puppeteer/test/src/EventEmitter.spec.ts @@ -0,0 +1,170 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +import expect from 'expect'; +import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import sinon from 'sinon'; + +describe('EventEmitter', () => { + let emitter: EventEmitter; + + 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'); + 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'); + expect(listener.callCount).toEqual(1); + emitter.off('foo', listener); + emitter.emit('foo'); + 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'); + expect(listener.callCount).toEqual(1); + emitter.emit('foo'); + 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'); + + 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')).toBe(true); + }); + + it('returns false if the event has listeners', () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + expect(emitter.emit('notFoo')).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')).toBe(false); + expect(emitter.emit('bar')).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')).toBe(true); + expect(emitter.emit('bar')).toBe(false); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/NetworkManager.spec.ts b/remote/test/puppeteer/test/src/NetworkManager.spec.ts new file mode 100644 index 0000000000..db73f6e601 --- /dev/null +++ b/remote/test/puppeteer/test/src/NetworkManager.spec.ts @@ -0,0 +1,1537 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +import expect from 'expect'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; +import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import {Frame} from 'puppeteer-core/internal/common/Frame.js'; +import { + NetworkManager, + NetworkManagerEmittedEvents, +} from 'puppeteer-core/internal/common/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 { + async send(): Promise {} + connection() { + return undefined; + } + async detach() {} + id() { + return '1'; + } +} + +describe('NetworkManager', () => { + it('should process extra info on multiple redirects', async () => { + const mockCDPSession = new MockCDPSession(); + new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + 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: { + 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: { + 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: { + 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: { + 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(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + manager.setRequestInterception(true); + + const requests: HTTPRequest[] = []; + manager.on(NetworkManagerEmittedEvents.Request, (request: HTTPRequest) => { + request.continue(); + requests.push(request); + }); + + /** + * 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: [Object], + 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: [Object], + 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: [Object], + 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(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const requests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.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: { + 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, + shouldReportCorbBlocking: false, + }); + + expect(requests).toHaveLength(1); + }); + + it(`should resolve the response once the late responseReceivedExtraInfo event arrives`, async () => { + const mockCDPSession = new MockCDPSession(); + const manager = new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const finishedRequests: HTTPRequest[] = []; + const pendingRequests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.RequestFinished, + (request: HTTPRequest) => { + finishedRequests.push(request); + } + ); + + manager.on(NetworkManagerEmittedEvents.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: { + 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, + shouldReportCorbBlocking: false, + }); + + 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(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const responses: HTTPResponse[] = []; + const requests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.Response, + (response: HTTPResponse) => { + responses.push(response); + } + ); + + manager.on(NetworkManagerEmittedEvents.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: { + 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(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const responses: HTTPResponse[] = []; + const requests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.Response, + (response: HTTPResponse) => { + responses.push(response); + } + ); + + manager.on(NetworkManagerEmittedEvents.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: { + 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, + shouldReportCorbBlocking: false, + }); + 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(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const responses: HTTPResponse[] = []; + const requests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.Response, + (response: HTTPResponse) => { + responses.push(response); + } + ); + + manager.on(NetworkManagerEmittedEvents.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: { + 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, + shouldReportCorbBlocking: false, + }); + + 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: { + 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: { + 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, + shouldReportCorbBlocking: false, + }); + expect( + responses.map(r => { + return r.status(); + }) + ).toEqual([200, 302, 200]); + }); +}); diff --git a/remote/test/puppeteer/test/src/TargetManager.spec.ts b/remote/test/puppeteer/test/src/TargetManager.spec.ts new file mode 100644 index 0000000000..282086eda8 --- /dev/null +++ b/remote/test/puppeteer/test/src/TargetManager.spec.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +import expect from 'expect'; +import { + CDPBrowser, + CDPBrowserContext, +} from 'puppeteer-core/internal/common/Browser.js'; + +import {getTestState} from './mocha-utils'; // eslint-disable-line import/extensions +import {attachFrame} from './utils.js'; + +describe('TargetManager', () => { + /* We use a special browser for this test as we need the --site-per-process flag */ + let browser: CDPBrowser; + let context: CDPBrowserContext; + + before(async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + browser = (await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat([ + '--site-per-process', + '--remote-debugging-port=21222', + '--host-rules=MAP * 127.0.0.1', + ]), + }) + )) as CDPBrowser; + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + }); + + afterEach(async () => { + await context.close(); + }); + + after(async () => { + await browser.close(); + }); + + it('should handle targets', async () => { + const {server} = getTestState(); + + const targetManager = browser._targetManager(); + expect(targetManager.getAvailableTargets().size).toBe(2); + + expect(await context.pages()).toHaveLength(0); + expect(targetManager.getAvailableTargets().size).toBe(2); + + const page = await context.newPage(); + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(3); + + await page.goto(server.EMPTY_PAGE); + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(3); + + // 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(3); + 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(4); + 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(5); + expect(page.frames()).toHaveLength(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..3ab518e703 --- /dev/null +++ b/remote/test/puppeteer/test/src/accessibility.spec.ts @@ -0,0 +1,582 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import assert from 'assert'; + +import expect from 'expect'; +import {SerializedAXNode} from 'puppeteer-core/internal/common/Accessibility.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Accessibility', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + + Accessibility Test + + +
Hello World
+

Inputs

+ + + + + + + + + + `); + + 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()).toEqual(golden); + }); + it('should report uninteresting nodes', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(``); + 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}) + ) + ).toEqual(golden); + }); + it('get snapshots while the tree is re-calculated', async () => { + // see https://github.com/puppeteer/puppeteer/issues/9404 + const {page} = getTestState(); + + await page.setContent( + ` + + + + + + Accessible name + aria-expanded puppeteer bug + + + + +

Some content

+ + + ` + ); + async function getAccessibleName(page: any, element: any) { + return (await page.accessibility.snapshot({root: element})).name; + } + const button = await page.$('button'); + expect(await getAccessibleName(page, button)).toEqual('Show'); + await button?.click(); + await page.waitForSelector('aria/Hide'); + }); + it('roledescription', async () => { + const {page} = getTestState(); + + await page.setContent( + '
Hi
' + ); + 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} = getTestState(); + + await page.setContent( + '11' + ); + 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} = getTestState(); + + await page.setContent(''); + 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} = getTestState(); + + await page.setContent( + '
hey
' + ); + 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} = getTestState(); + + await page.setContent( + '
hey
' + ); + 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} = getTestState(); + + await page.setContent(` +
+
Tab1
+
Tab2
+
`); + 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} = getTestState(); + + await page.setContent(` +
+ Edit this image: my fake image +
`); + 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: 'img', + name: 'my fake image', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('rich text editable fields with role should have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` +
+ Edit this image: my fake image +
`); + // 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]).toEqual(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} = getTestState(); + + await page.setContent(` +
Edit this image:my fake image
`); + 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} = getTestState(); + + await page.setContent(` +
+ this is the inner content + yo +
`); + 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} = getTestState(); + + await page.setContent(` +
+ this is the inner content + yo +
`); + 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} = getTestState(); + + await page.setContent(` +
+ this is the inner content + yo +
`); + 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} = getTestState(); + + await page.setContent(``); + + const 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} = getTestState(); + + await page.setContent(``); + + const 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} = getTestState(); + + await page.setContent(` +
+
First Item
+
Second Item
+
Third Item
+
+ `); + + const 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} = getTestState(); + + await page.setContent(``); + const 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} = getTestState(); + + await page.setContent(`
`); + const div = (await page.$('div'))!; + expect(await page.accessibility.snapshot({root: div})).toEqual(null); + expect( + await page.accessibility.snapshot({ + root: div, + interestingOnly: false, + }) + ).toEqual({ + 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..52fe99997b --- /dev/null +++ b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts @@ -0,0 +1,698 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +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, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, detachFrame} from './utils.js'; + +describe('AriaQueryHandler', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('parseAriaSelector', () => { + beforeEach(async () => { + const {page} = getTestState(); + await page.setContent( + '' + ); + }); + it('should find button', async () => { + const {page} = getTestState(); + const expectFound = async (button: ElementHandle | null) => { + assert(button); + const id = await button.evaluate((button: Element) => { + return button.id; + }); + expect(id).toBe('btn'); + }; + let button = await page.$( + 'aria/Submit button and some spaces[role="button"]' + ); + await expectFound(button); + button = await page.$( + "aria/Submit button and some spaces[role='button']" + ); + await expectFound(button); + button = await page.$( + 'aria/ Submit button and some spaces[role="button"]' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button and some spaces [role="button"]' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button and some spaces [ role = "button" ] ' + ); + await expectFound(button); + button = await page.$( + 'aria/[role="button"]Submit button and some spaces' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button [role="button"]and some spaces' + ); + await expectFound(button); + button = await page.$( + 'aria/[name=" Submit button and some spaces"][role="button"]' + ); + await expectFound(button); + button = await page.$( + "aria/[name=' Submit button and some spaces'][role='button']" + ); + await expectFound(button); + 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} = getTestState(); + await page.setContent( + '
' + ); + const button = (await page.$( + 'aria/[role="button"]' + )) as ElementHandle; + const id = await button!.evaluate(button => { + return button.id; + }); + expect(id).toBe('btn'); + }); + + it('should find button by name and role', async () => { + const {page} = getTestState(); + await page.setContent( + '
' + ); + const button = (await page.$( + 'aria/Submit[role="button"]' + )) as ElementHandle; + const id = await button!.evaluate(button => { + return button.id; + }); + expect(id).toBe('btn'); + }); + + it('should find first matching element', async () => { + const {page} = getTestState(); + await page.setContent( + ` + + + ` + ); + const div = (await page.$( + 'aria/menu div' + )) as ElementHandle; + const id = await div!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu1'); + }); + + it('should find by name', async () => { + const {page} = getTestState(); + await page.setContent( + ` + + + ` + ); + const menu = (await page.$( + 'aria/menu-label1' + )) as ElementHandle; + const id = await menu!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu1'); + }); + + it('should find by name', async () => { + const {page} = getTestState(); + await page.setContent( + ` + + + ` + ); + const menu = (await page.$( + 'aria/menu-label2' + )) as ElementHandle; + const id = await menu!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu2'); + }); + }); + + describe('queryAll', () => { + it('should find menu by name', async () => { + const {page} = getTestState(); + await page.setContent( + ` + + + ` + ); + const divs = (await page.$$('aria/menu div')) as Array< + ElementHandle + >; + 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(25_000); + + const {page} = 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} = 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} = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return (document.body.innerHTML = `
`); + }); + const element = (await page.$('div'))!; + await element!.waitForSelector('aria/test'); + }); + + it('should persist query handler bindings across reloads', async () => { + const {page, server} = 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} = 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} = 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} = 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(`

anything

`), + ]); + 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} = 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'); + const 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} = 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 = + '

'); + }); + await watchdog; + }); + + it('Page.waitForSelector is shortcut for main frame', async () => { + const {page, server} = 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'); + const elementHandle = await watchdog; + expect(elementHandle!.frame).toBe(page.mainFrame()); + }); + + it('should run in specified frame', async () => { + const {page, server} = 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'); + const elementHandle = await waitForSelectorPromise; + expect(elementHandle!.frame).toBe(frame2); + }); + + it('should throw when frame is detached', async () => { + const {page, server} = 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} = getTestState(); + + let imgFound = false; + const waitForSelector = page + .waitForSelector('aria/[role="img"]') + .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} = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('aria/name', {visible: true}) + .then(() => { + return (divFound = true); + }); + await page.setContent( + `
1
` + ); + 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} = getTestState(); + + let divVisible = false; + const waitForSelector = page + .waitForSelector('aria/inner', {visible: true}) + .then(() => { + return (divVisible = true); + }); + await page.setContent( + `
hi
` + ); + 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} = getTestState(); + + let divHidden = false; + await page.setContent( + `
text
` + ); + const waitForSelector = page + .waitForSelector('aria/[role="button"]', {hidden: true}) + .then(() => { + return (divHidden = true); + }); + 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} = getTestState(); + + let divHidden = false; + await page.setContent( + `
text
` + ); + const waitForSelector = page + .waitForSelector('aria/[role="main"]', {hidden: true}) + .then(() => { + return (divHidden = true); + }); + 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} = getTestState(); + + await page.setContent(`
text
`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('aria/[role="main"]', {hidden: true}) + .then(() => { + return (divRemoved = true); + }); + 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} = getTestState(); + + const handle = await page.waitForSelector('aria/non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + + it('should respect timeout', async () => { + const {page} = 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} = getTestState(); + + await page.setContent(`
text
`); + 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} = getTestState(); + + let divFound = false; + const waitForSelector = page.waitForSelector('aria/zombo').then(() => { + return (divFound = true); + }); + await page.setContent(`
`); + 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} = getTestState(); + + const waitForSelector = page.waitForSelector('aria/zombo'); + await page.setContent(`
anything
`); + expect( + await page.evaluate(x => { + return x?.textContent; + }, await waitForSelector) + ).toBe('anything'); + }); + + it('should have correct stack trace for timeout', async () => { + const {page} = 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 () => { + beforeEach(async () => { + const {page} = getTestState(); + await page.setContent( + ` +

title

+ +
+
+
+
+
+
+ + +
+
+ +

text content

+ +

text content

+ + + Accessible Name + + + + +
+
+
+
item1
+
item2
+
+
item3
+
+ +
+ ` + ); + }); + const getIds = async (elements: ElementHandle[]) => { + return Promise.all( + elements.map(element => { + return element.evaluate((element: Element) => { + return element.id; + }); + }) + ); + }; + it('should find by name "foo"', async () => { + const {page} = getTestState(); + 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} = getTestState(); + 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} = getTestState(); + 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} = getTestState(); + const found = (await page.$$('aria/[role="button"]')) as Array< + ElementHandle + >; + const ids = await getIds(found); + expect(ids).toEqual([ + 'node5', + 'node6', + 'node7', + 'node8', + 'node10', + 'node21', + ]); + }); + it('should find by role "heading"', async () => { + const {page} = getTestState(); + 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} = getTestState(); + const found = await page.$$('aria/title'); + const ids = await getIds(found); + expect(ids).toEqual(['shown']); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/bidi/Connection.spec.ts b/remote/test/puppeteer/test/src/bidi/Connection.spec.ts new file mode 100644 index 0000000000..e5cd9de0a1 --- /dev/null +++ b/remote/test/puppeteer/test/src/bidi/Connection.spec.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +import expect from 'expect'; +import {Connection} from 'puppeteer-core/internal/common/bidi/Connection.js'; +import {ConnectionTransport} from 'puppeteer-core/internal/common/ConnectionTransport.js'; + +describe('WebDriver BiDi', () => { + describe('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 Connection(transport); + const responsePromise = connection.send('session.new', { + capabilities: { + proxy: {}, + }, + }); + expect(transport.sent).toEqual([ + `{"id":1,"method":"session.new","params":{"capabilities":{"proxy":{}}}}`, + ]); + const id = JSON.parse(transport.sent[0]!).id; + const rawResponse = { + id, + 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/test/src/browser.spec.ts b/remote/test/puppeteer/test/src/browser.spec.ts new file mode 100644 index 0000000000..2023da0f59 --- /dev/null +++ b/remote/test/puppeteer/test/src/browser.spec.ts @@ -0,0 +1,91 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Browser specs', function () { + setupTestBrowserHooks(); + + describe('Browser.version', function () { + it('should return whether we are in headless', async () => { + const {browser, isHeadless, headless} = getTestState(); + + const version = await browser.version(); + expect(version.length).toBeGreaterThan(0); + expect(version.startsWith('Headless')).toBe( + isHeadless && headless !== 'new' + ); + }); + }); + + describe('Browser.userAgent', function () { + it('should include WebKit', async () => { + const {browser, isChrome} = 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} = getTestState(); + + const target = browser.target(); + expect(target.type()).toBe('browser'); + }); + }); + + describe('Browser.process', function () { + it('should return child_process instance', async () => { + const {browser} = getTestState(); + + const process = await browser.process(); + expect(process!.pid).toBeGreaterThan(0); + }); + it('should not return child_process for remote browser', async () => { + const {browser, puppeteer} = getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint, + }); + expect(remoteBrowser.process()).toBe(null); + remoteBrowser.disconnect(); + }); + }); + + describe('Browser.isConnected', () => { + it('should set the browser connected state', async () => { + const {browser, puppeteer} = getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const newBrowser = await puppeteer.connect({ + browserWSEndpoint, + }); + expect(newBrowser.isConnected()).toBe(true); + 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..d17d77457e --- /dev/null +++ b/remote/test/puppeteer/test/src/browsercontext.spec.ts @@ -0,0 +1,243 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('BrowserContext', function () { + setupTestBrowserHooks(); + it('should have default context', async () => { + const {browser} = getTestState(); + 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} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + 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); + await context.close(); + }); + it('should fire target events', async () => { + const {browser, server} = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const events: any[] = []; + context.on('targetcreated', target => { + return events.push('CREATED: ' + target.url()); + }); + context.on('targetchanged', target => { + return events.push('CHANGED: ' + target.url()); + }); + context.on('targetdestroyed', target => { + return 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}`, + ]); + await context.close(); + }); + it('should wait for a target', async () => { + const {browser, server} = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + 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; + } + } + await context.close(); + }); + + it('should timeout waiting for a non-existent target', async () => { + const {browser, server} = 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} = getTestState(); + + // 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} = getTestState(); + + expect(browser.browserContexts()).toHaveLength(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts()).toHaveLength(2); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts).toHaveLength(2); + remoteBrowser.disconnect(); + await context.close(); + }); + + it('should provide a context id', async () => { + const {browser} = getTestState(); + + 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(); + }); +}); 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..8ddcfe3302 --- /dev/null +++ b/remote/test/puppeteer/test/src/chromiumonly.spec.ts @@ -0,0 +1,172 @@ +/** + * Copyright 2019 Google Inc. 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. + */ +import {IncomingMessage} from 'http'; + +import expect from 'expect'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +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 {defaultBrowserOptions, puppeteer} = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + 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); + browser1.disconnect(); + + const browser2 = await puppeteer.connect({ + browserURL: browserURL + '/', + }); + const page2 = await browser2.newPage(); + expect( + await page2.evaluate(() => { + return 8 * 7; + }) + ).toBe(56); + browser2.disconnect(); + await originalBrowser.close(); + }); + it('should throw when using both browserWSEndpoint and browserURL', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + const browserURL = 'http://127.0.0.1:21222'; + + let error!: Error; + await puppeteer + .connect({ + browserURL, + browserWSEndpoint: originalBrowser.wsEndpoint(), + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'Exactly one of browserWSEndpoint, browserURL or transport' + ); + + await originalBrowser.close(); + }); + it('should throw when trying to connect to non-existing browser', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + 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' + ); + await originalBrowser.close(); + }); + }); + + describe('Puppeteer.launch |pipe| option', function () { + it('should support the pipe option', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign({pipe: true}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + 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(); + await browser.close(); + }); + it('should support the pipe argument', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions); + options.args = ['--remote-debugging-pipe'].concat(options.args || []); + const browser = await puppeteer.launch(options); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should fire "disconnected" when closing with pipe', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign({pipe: true}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const disconnectedEventPromise = waitEvent(browser, 'disconnected'); + // Emulate user exiting browser. + browser.process()!.kill(); + await disconnectedEventPromise; + }); + }); +}); + +describe('Chromium-Specific Page Tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.setRequestInterception should work with intervention headers', async () => { + const {server, page} = getTestState(); + + server.setRoute('/intervention', (_req, res) => { + return res.end(` + + `); + }); + 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..8e253f3085 --- /dev/null +++ b/remote/test/puppeteer/test/src/click.spec.ts @@ -0,0 +1,492 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; +import {KnownDevices} from 'puppeteer'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('Page.click', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should click the button', async () => { + const {page, server} = 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} = getTestState(); + + await page.setContent(` + + + + `); + 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} = 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} = getTestState(); + + await page.setContent(` + + + `); + 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} = 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} = 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} = 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} = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto(server.PREFIX + '/wrappedlink.html'); + const 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} = getTestState(); + + await page.setContent(` + + woofdoggo + `); + 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} = 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} = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + const messages: any[] = []; + 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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; + }); + }); + const 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} = 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} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.setContent(`empty.html`); + // This await should not hang. + await page.click('a'); + }); + it('should click the button inside an iframe', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('
spacer
'); + await attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + const 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setViewport({width: 500, height: 500}); + await page.setContent( + '
spacer
' + ); + 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} = getTestState(); + + await page.setViewport({width: 400, height: 400, deviceScaleFactor: 5}); + expect( + await page.evaluate(() => { + return window.devicePixelRatio; + }) + ).toBe(5); + await page.setContent('
spacer
'); + await attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + const 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..2a048bd008 --- /dev/null +++ b/remote/test/puppeteer/test/src/cookies.spec.ts @@ -0,0 +1,570 @@ +/** + * Copyright 2018 Google Inc. 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. + */ +import expect from 'expect'; + +import { + expectCookieEquals, + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Cookie specs', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.cookies', function () { + it('should return no cookies in pristine browser context', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + expectCookieEquals(await page.cookies(), []); + }); + it('should get a cookie', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + + 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} = 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} = 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} = 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} = 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); + }); + 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} = 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); + }); + 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + const cookies = await page.cookies(); + 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} = getTestState(); + + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({ + name: 'gridcookie', + value: 'GRID', + path: '/grid.html', + }); + 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); + 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} = 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} = 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} = 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} = 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} = 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} = 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(''); + expectCookieEquals(await page.cookies(), []); + 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} = 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(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(''); + + 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', + }, + ]); + + 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, puppeteer, defaultBrowserOptions} = getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + ignoreHTTPSErrors: true, + }); + + const page = await browser.newPage(); + + try { + await page.goto(httpsServer.PREFIX + '/grid.html'); + await page.evaluate(src => { + let fulfill!: () => void; + const promise = new Promise(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' + ); + 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 page.close(); + await browser.close(); + } + }); + }); + + describe('Page.deleteCookie', function () { + it('should work', async () => { + const {page, server} = 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..7c93fc73ba --- /dev/null +++ b/remote/test/puppeteer/test/src/coverage.spec.ts @@ -0,0 +1,356 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; + +import { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, +} from './mocha-utils.js'; + +describe('Coverage specs', function () { + describe('JSCoverage', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page, server} = getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + 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} = 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} = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + }); + it("shouldn't ignore eval() scripts if reportAnonymousScripts is true", async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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} = 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} = 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, ':/') + ).toBeGolden('jscoverage-involved.txt'); + }); + // @see https://crbug.com/990945 + it.skip('should not hang when there is a debugger statement', async () => { + const {page, server} = 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} = getTestState(); + + await page.coverage.startJSCoverage({resetOnNavigation: false}); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + 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} = 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} = getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + 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} = getTestState(); + await page.coverage.startJSCoverage({ + includeRawScriptCoverage: true, + }); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + 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} = 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 () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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: 40}]); + }); + it('should work with complicated usecases', async () => { + const {page, server} = 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, ':/') + ).toBeGolden('csscoverage-involved.txt'); + }); + it('should work with empty stylesheets', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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/defaultbrowsercontext.spec.ts b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts new file mode 100644 index 0000000000..73c31d73c4 --- /dev/null +++ b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts @@ -0,0 +1,115 @@ +/** + * Copyright 2017 Google Inc. 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. + */ +import expect from 'expect'; + +import { + expectCookieEquals, + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('DefaultBrowserContext', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('page.cookies() should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + 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} = 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'); + 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} = 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'); + 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/dialog.spec.ts b/remote/test/puppeteer/test/src/dialog.spec.ts new file mode 100644 index 0000000000..e4489004cd --- /dev/null +++ b/remote/test/puppeteer/test/src/dialog.spec.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2018 Google Inc. 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. + */ +import expect from 'expect'; +import sinon from 'sinon'; + +import { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, +} from './mocha-utils.js'; + +describe('Page.Events.Dialog', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should fire', async () => { + const {page} = 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} = 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} = getTestState(); + + page.on('dialog', dialog => { + 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..cb46b5e60c --- /dev/null +++ b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts @@ -0,0 +1,197 @@ +/** + * Copyright 2021 Google Inc. 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. + */ + +import expect from 'expect'; + +import { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, +} from './mocha-utils.js'; + +describe('Input.drag', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should throw an exception if not enabled before usage', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + const draggable = (await page.$('#drag'))!; + + try { + await draggable!.drag({x: 1, y: 1}); + } catch (error) { + expect((error as Error).message).toContain( + 'Drag Interception is not enabled!' + ); + } + }); + it('should emit a dragIntercepted event when dragged', async () => { + const {page, server} = 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); + const draggable = (await page.$('#drag'))!; + const data = await draggable.drag({x: 1, y: 1}); + + expect(data.items).toHaveLength(1); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + }); + it('should emit a dragEnter', async () => { + const {page, server} = 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); + const draggable = (await page.$('#drag'))!; + const data = await draggable.drag({x: 1, y: 1}); + const dropzone = (await page.$('#drop'))!; + await dropzone.dragEnter(data); + + expect( + await page.evaluate(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragEnter; + }) + ).toBe(true); + }); + it('should emit a dragOver event', async () => { + const {page, server} = 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); + const draggable = (await page.$('#drag'))!; + const data = await draggable.drag({x: 1, y: 1}); + const dropzone = (await page.$('#drop'))!; + await dropzone.dragEnter(data); + await dropzone.dragOver(data); + + expect( + await page.evaluate(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragEnter; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragOver; + }) + ).toBe(true); + }); + it('can be dropped', async () => { + const {page, server} = 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); + const draggable = (await page.$('#drag'))!; + const dropzone = (await page.$('#drop'))!; + const data = await draggable.drag({x: 1, y: 1}); + await dropzone.dragEnter(data); + await dropzone.dragOver(data); + await dropzone.drop(data); + + expect( + await page.evaluate(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragEnter; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragOver; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDrop; + }) + ).toBe(true); + }); + it('can be dragged and dropped with a single function', async () => { + const {page, server} = 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); + const draggable = (await page.$('#drag'))!; + const dropzone = (await page.$('#drop'))!; + await draggable.dragAndDrop(dropzone); + + expect( + await page.evaluate(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragEnter; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragOver; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDrop; + }) + ).toBe(true); + }); + it('can be disabled', async () => { + const {page, server} = 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); + const draggable = (await page.$('#drag'))!; + await draggable.drag({x: 1, y: 1}); + await page.setDragInterception(false); + + try { + await draggable.drag({x: 1, y: 1}); + } catch (error) { + expect((error as Error).message).toContain( + 'Drag Interception is not enabled!' + ); + } + }); +}); 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..704c262d8c --- /dev/null +++ b/remote/test/puppeteer/test/src/elementhandle.spec.ts @@ -0,0 +1,802 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; +import {Puppeteer} from 'puppeteer'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; +import sinon from 'sinon'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + shortWaitForArrayToHaveAtLeastNElements, +} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('ElementHandle specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('ElementHandle.boundingBox', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const 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} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const nestedFrame = page.frames()[1]!.childFrames()[1]!; + const 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} = getTestState(); + + await page.setContent('
hi
'); + const element = (await page.$('div'))!; + expect(await element.boundingBox()).toBe(null); + }); + it('should force a layout', async () => { + const {page} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent( + '
hello
' + ); + const 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} = getTestState(); + + await page.setContent(` + + + + `); + const element = (await page.$( + '#therect' + )) as ElementHandle; + 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} = 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('#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]!; + const 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 + dif.paddingLeft + y: 2 + 5, + }); + }); + + it('should return null for invisible elements', async () => { + const {page} = getTestState(); + + await page.setContent('
hi
'); + const element = (await page.$('div'))!; + expect(await element.boxModel()).toBe(null); + }); + }); + + describe('ElementHandle.contentFrame', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const 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} = getTestState(); + await page.setContent('
text
'); + const 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} = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const 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} = 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 = ` +
+ `; + document.body.addEventListener('click', e => { + (window as any).reportClick(e.clientX, e.clientY); + }); + }); + + const 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} = getTestState(); + + await page.goto(server.PREFIX + '/shadow.html'); + const 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} = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const 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).toBe('Node is not of type HTMLElement'); + }); + it('should throw for detached nodes', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const 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).toBe('Node is detached from document'); + }); + it('should throw for hidden nodes', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const 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).toBe( + 'Node is either not clickable or not an HTMLElement' + ); + }); + it('should throw for recursively hidden nodes', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const 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).toBe( + 'Node is either not clickable or not an HTMLElement' + ); + }); + it('should throw for
elements', async () => { + const {page} = getTestState(); + + await page.setContent('hello
goodbye'); + const br = (await page.$('br'))!; + const error = await br.click().catch(error_ => { + return error_; + }); + expect(error.message).toBe( + 'Node is either not clickable or not an HTMLElement' + ); + }); + }); + + describe('ElementHandle.clickablePoint', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + document.body.style.padding = '0'; + document.body.style.margin = '0'; + document.body.innerHTML = ` +
+ `; + }); + await page.evaluate(async () => { + return new Promise(resolve => { + return window.requestAnimationFrame(resolve); + }); + }); + const 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 work for iframes', async () => { + const {page} = getTestState(); + await page.evaluate(() => { + document.body.style.padding = '10px'; + document.body.style.margin = '10px'; + document.body.innerHTML = ` + + `; + }); + await page.evaluate(async () => { + return new Promise(resolve => { + return window.requestAnimationFrame(resolve); + }); + }); + const frame = page.frames()[1]!; + const 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} = getTestState(); + const waitFor = page.waitForSelector('.foo') as Promise< + ElementHandle + >; + // Set the page content after the waitFor has been started. + await page.setContent( + '
bar2
Foo1
' + ); + let element = (await waitFor)!; + expect(element).toBeDefined(); + + const innerWaitFor = element.waitForSelector('.bar') as Promise< + ElementHandle + >; + await element.evaluate(el => { + el.innerHTML = '
bar1
'; + }); + element = (await innerWaitFor)!; + expect(element).toBeDefined(); + expect( + await element.evaluate(el => { + return (el as HTMLElement).innerText; + }) + ).toStrictEqual('bar1'); + }); + }); + + describe('Element.waitForXPath', () => { + it('should wait correctly with waitForXPath on an element', async () => { + const {page} = getTestState(); + // Set the page content after the waitFor has been started. + await page.setContent( + `
+ el1 +
+ el2 +
+
+
+ el3 +
` + ); + + const el1 = (await page.waitForSelector( + '#el1' + )) as ElementHandle; + + for (const path of ['//div', './/div']) { + const e = (await el1.waitForXPath( + path + )) as ElementHandle; + expect( + await e.evaluate(el => { + return el.id; + }) + ).toStrictEqual('el2'); + } + }); + }); + + describe('ElementHandle.hover', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + const 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} = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + for (let i = 0; i < 11; ++i) { + const button = (await page.$('#btn' + i))!; + // All but last button are visible. + const visible = i < 10; + expect(await button.isIntersectingViewport()).toBe(visible); + } + }); + it('should work with threshold', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + // a button almost cannot be seen + // sometimes we expect to return false by isIntersectingViewport1 + const button = (await page.$('#btn11'))!; + expect( + await button.isIntersectingViewport({ + threshold: 0.001, + }) + ).toBe(false); + }); + it('should work with threshold of 1', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + // a button almost cannot be seen + // sometimes we expect to return false by isIntersectingViewport1 + const button = (await page.$('#btn0'))!; + expect( + await button.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(true); + }); + it('should work with svg elements', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/inline-svg.html'); + const visibleCircle = await page.$('circle'); + const visibleSvg = await page.$('svg'); + expect( + await visibleCircle!.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(true); + expect( + await visibleCircle!.isIntersectingViewport({ + threshold: 0, + }) + ).toBe(true); + expect( + await visibleSvg!.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(true); + expect( + await visibleSvg!.isIntersectingViewport({ + threshold: 0, + }) + ).toBe(true); + + const invisibleCircle = await page.$('div circle'); + const invisibleSvg = await page.$('div svg'); + expect( + await invisibleCircle!.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(false); + expect( + await invisibleCircle!.isIntersectingViewport({ + threshold: 0, + }) + ).toBe(false); + expect( + await invisibleSvg!.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(false); + expect( + await invisibleSvg!.isIntersectingViewport({ + threshold: 0, + }) + ).toBe(false); + }); + }); + + describe('Custom queries', function () { + afterEach(() => { + Puppeteer.clearCustomQueryHandlers(); + }); + it('should register and unregister', async () => { + const {page} = getTestState(); + await page.setContent('
'); + + // Register. + Puppeteer.registerCustomQueryHandler('getById', { + queryOne: (_element, selector) => { + return document.querySelector(`[id="${selector}"]`); + }, + }); + const element = (await page.$( + 'getById/foo' + )) as ElementHandle; + 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', () => { + 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} = getTestState(); + await page.setContent( + '
Foo1
Foo2
' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (_element, selector) => { + return [...document.querySelectorAll(`.${selector}`)]; + }, + }); + const elements = (await page.$$('getByClass/foo')) as Array< + ElementHandle + >; + 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} = getTestState(); + await page.setContent( + '
Foo1
Foo2
' + ); + 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} = getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '
Foo1
' + ); + const element = await waitFor; + + expect(element).toBeDefined(); + }); + + it('should wait correctly with waitForSelector on an element', async () => { + const {page} = getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo') as Promise< + ElementHandle + >; + + // Set the page content after the waitFor has been started. + await page.setContent( + '
bar2
Foo1
' + ); + let element = (await waitFor)!; + expect(element).toBeDefined(); + + const innerWaitFor = element.waitForSelector('getByClass/bar') as Promise< + ElementHandle + >; + + await element.evaluate(el => { + el.innerHTML = '
bar1
'; + }); + + element = (await innerWaitFor)!; + expect(element).toBeDefined(); + expect( + await element.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} = getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '
Foo1
' + ); + const element = await waitFor; + + expect(element).toBeDefined(); + }); + it('should work when both queryOne and queryAll are registered', async () => { + const {page} = getTestState(); + await page.setContent( + '
Foo2
' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + queryAll: (element, selector) => { + return [...(element as Element).querySelectorAll(`.${selector}`)]; + }, + }); + + const 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} = getTestState(); + await page.setContent( + '
text
content
' + ); + 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} = getTestState(); + await page.setContent('
'); + + Puppeteer.registerCustomQueryHandler('getById', { + // This is a function shorthand + queryOne(_element, selector) { + return document.querySelector(`[id="${selector}"]`); + }, + }); + + const element = (await page.$( + 'getById/foo' + )) as ElementHandle; + expect( + await page.evaluate(element => { + return element.id; + }, element) + ).toBe('foo'); + }); + }); + + describe('Element.toElement', () => { + it('should work', async () => { + const {page} = getTestState(); + await page.setContent('
Foo1
'); + const element = await page.$('.foo'); + const div = await element?.toElement('div'); + expect(div).toBeDefined(); + }); + }); +}); 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..43ea9bf59c --- /dev/null +++ b/remote/test/puppeteer/test/src/emulation.spec.ts @@ -0,0 +1,516 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; +import {KnownDevices, PredefinedNetworkConditions} from 'puppeteer'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +const iPhone = KnownDevices['iPhone 6']; +const iPhoneLandscape = KnownDevices['iPhone 6 landscape']; + +describe('Emulation', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.viewport', function () { + it('should get the proper viewport size', async () => { + const {page} = 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} = 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} = 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} = 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} = 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} = 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'); + }); + }); + + describe('Page.emulate', function () { + it('should work', async () => { + const {page, server} = 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} = getTestState(); + + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + const 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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..f8992b051e --- /dev/null +++ b/remote/test/puppeteer/test/src/evaluation.spec.ts @@ -0,0 +1,600 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('Evaluation specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluate', function () { + it('should work', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return 7 * 3; + }); + expect(result).toBe(21); + }); + it('should transfer BigInt', async () => { + const {page} = getTestState(); + + const result = await page.evaluate((a: bigint) => { + return a; + }, BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should transfer NaN', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(a => { + return a; + }, NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should transfer -0', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(a => { + return a; + }, -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should transfer Infinity', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(a => { + return a; + }, Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should transfer -Infinity', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(a => { + return a; + }, -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should transfer arrays', async () => { + const {page} = 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} = 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} = 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} = getTestState(); + + await page.goto(server.PREFIX + '/global-var.html'); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it('should return undefined for objects with symbols', async () => { + const {page} = getTestState(); + + expect( + await page.evaluate(() => { + return [Symbol('foo4')]; + }) + ).toBe(undefined); + }); + it('should work with function shorthands', async () => { + const {page} = 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} = getTestState(); + + const result = await page.evaluate( + a => { + return a['中文字符']; + }, + { + 中文字符: 42, + } + ); + expect(result).toBe(42); + }); + it('should throw when evaluation triggers reload', async () => { + const {page} = 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} = 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} = 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} = 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 await (globalThis as any).callController(9, 3); + }); + expect(result).toBe(27); + }); + it('should reject promise with exception', async () => { + const {page} = 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} = getTestState(); + + let error!: Error; + await page + .evaluate(() => { + throw 'qwerty'; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain('qwerty'); + }); + it('should support thrown numbers as error messages', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .evaluate(() => { + throw 100500; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain('100500'); + }); + it('should return complex objects', async () => { + const {page} = 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} = getTestState(); + + const result = await page.evaluate(() => { + return BigInt(42); + }); + expect(result).toBe(BigInt(42)); + }); + it('should return NaN', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return NaN; + }); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should return -0', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return -0; + }); + expect(Object.is(result, -0)).toBe(true); + }); + it('should return Infinity', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return Infinity; + }); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should return -Infinity', async () => { + const {page} = 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} = 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} = getTestState(); + + expect( + await page.evaluate(() => { + return {a: undefined}; + }) + ).toEqual({}); + }); + it('should return undefined for non-serializable objects', async () => { + const {page} = getTestState(); + + expect( + await page.evaluate(() => { + return window; + }) + ).toBe(undefined); + }); + it('should return promise as empty object', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return { + promise: new Promise(resolve => { + setTimeout(resolve, 1000); + }), + }; + }); + expect(result).toEqual({ + promise: {}, + }); + }); + it('should fail for circular object', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + const a: {[x: string]: any} = {}; + const b = {a}; + a['b'] = b; + return a; + }); + expect(result).toBe(undefined); + }); + it('should be able to throw a tricky error', async () => { + const {page} = getTestState(); + + const windowHandle = await page.evaluateHandle(() => { + return window; + }); + const errorText = await windowHandle.jsonValue().catch(error_ => { + return error_.message; + }); + const error = await page + .evaluate(errorText => { + throw new Error(errorText); + }, errorText) + .catch(error_ => { + return error_; + }); + expect(error.message).toContain(errorText); + }); + it('should accept a string', async () => { + const {page} = getTestState(); + + const result = await page.evaluate('1 + 2'); + expect(result).toBe(3); + }); + it('should accept a string with semi colons', async () => { + const {page} = getTestState(); + + const result = await page.evaluate('1 + 5;'); + expect(result).toBe(6); + }); + it('should accept a string with comments', async () => { + const {page} = 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} = getTestState(); + + await page.setContent('
42
'); + const 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} = getTestState(); + + await page.setContent('
39
'); + const element = (await page.$('section'))!; + expect(element).toBeTruthy(); + 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} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const 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} = 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 throw a nice error after a navigation', async () => { + const {page} = getTestState(); + + const executionContext = await page.mainFrame().executionContext(); + + await Promise.all([ + page.waitForNavigation(), + executionContext.evaluate(() => { + return window.location.reload(); + }), + ]); + const error = await executionContext + .evaluate(() => { + return null; + }) + .catch(error_ => { + return error_; + }); + expect((error as Error).message).toContain('navigation'); + }); + it('should not throw an error when evaluation does a navigation', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/one-style.html'); + const result = await page.evaluate(() => { + (window as any).location = '/empty.html'; + return [42]; + }); + expect(result).toEqual([42]); + }); + it('should transfer 100Mb of data from page to node.js', async function () { + this.timeout(25_000); + const {page} = 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} = 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'); + }); + }); + + describe('Page.evaluateOnNewDocument', function () { + it('should evaluate before anything else on the page', async () => { + const {page, server} = 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} = 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('Frame.evaluate', function () { + it('should have different execution contexts', async () => { + const {page, server} = 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} = 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} = 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..4065b867fc --- /dev/null +++ b/remote/test/puppeteer/test/src/fixtures.spec.ts @@ -0,0 +1,112 @@ +/** + * Copyright 2019 Google Inc. 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. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ + +import path from 'path'; + +import expect from 'expect'; + +import {getTestState} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Fixtures', function () { + it('dumpio option should work with pipe option', async () => { + const {defaultBrowserOptions, puppeteerPath, headless} = getTestState(); + if (headless === 'new') { + // This test only works in the old headless mode. + return; + } + + let dumpioData = ''; + const {spawn} = await import('child_process'); + 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 => { + return (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} = getTestState(); + + let dumpioData = ''; + const {spawn} = await import('child_process'); + 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 => { + return (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} = getTestState(); + + const {spawn, execSync} = await import('child_process'); + 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 wsEndPointCallback: (value: string) => void; + const wsEndPointPromise = new Promise(x => { + return (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 => { + return res.on('close', resolve); + }), + ]; + if (process.platform === 'win32') { + execSync(`taskkill /pid ${res.pid} /T /F`); + } else { + process.kill(res.pid!); + } + await Promise.all(promises); + }); +}); 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..4d4d05cce6 --- /dev/null +++ b/remote/test/puppeteer/test/src/frame.spec.ts @@ -0,0 +1,350 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; +import {CDPSession} from 'puppeteer-core/internal/common/Connection.js'; +import {Frame} from 'puppeteer-core/internal/common/Frame.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import { + attachFrame, + detachFrame, + dumpFrames, + navigateFrame, + waitEvent, +} from './utils.js'; + +describe('Frame specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Frame.executionContext', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames()).toHaveLength(2); + const [frame1, frame2] = page.frames(); + const context1 = await frame1!.executionContext(); + const context2 = await frame2!.executionContext(); + expect(context1).toBeTruthy(); + expect(context2).toBeTruthy(); + expect(context1 !== context2).toBeTruthy(); + expect(context1._world?.frame()).toBe(frame1); + expect(context2._world?.frame()).toBe(frame2); + + await Promise.all([ + context1.evaluate(() => { + return ((globalThis as any).a = 1); + }), + context2.evaluate(() => { + return ((globalThis as any).a = 2); + }), + ]); + const [a1, a2] = await Promise.all([ + context1.evaluate(() => { + return (globalThis as any).a; + }), + context2.evaluate(() => { + return (globalThis as any).a; + }), + ]); + expect(a1).toBe(1); + expect(a2).toBe(2); + }); + }); + + describe('Frame.evaluateHandle', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + const windowHandle = await mainFrame.evaluateHandle(() => { + return window; + }); + expect(windowHandle).toBeTruthy(); + }); + }); + + describe('Frame.evaluate', function () { + it('should throw for detached frames', async () => { + const {page, server} = getTestState(); + + const frame1 = (await attachFrame(page, 'frame1', server.EMPTY_PAGE))!; + await detachFrame(page, 'frame1'); + let error!: Error; + await frame1 + .evaluate(() => { + return 7 * 8; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'Execution context is not available in detached frame' + ); + }); + + it('allows readonly array to be an argument', async () => { + const {page, server} = 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} = 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} = getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(dumpFrames(page.mainFrame())).toEqual([ + 'http://localhost:/frames/nested-frames.html', + ' http://localhost:/frames/two-frames.html (2frames)', + ' http://localhost:/frames/frame.html (uno)', + ' http://localhost:/frames/frame.html (dos)', + ' http://localhost:/frames/frame.html (aframe)', + ]); + }); + it('should send events when frames are manipulated dynamically', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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..3ebe534aae --- /dev/null +++ b/remote/test/puppeteer/test/src/golden-utils.ts @@ -0,0 +1,173 @@ +/** + * Copyright 2017 Google Inc. 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. + */ +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 + ? `${change.value}` + : change.removed + ? `${change.value}` + : change.value; + return text; + }, ``); + 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..f75654f1e1 --- /dev/null +++ b/remote/test/puppeteer/test/src/headful.spec.ts @@ -0,0 +1,438 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import {mkdtemp} from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import expect from 'expect'; +import { + PuppeteerLaunchOptions, + PuppeteerNode, +} from 'puppeteer-core/internal/node/PuppeteerNode.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; + +import {getTestState} from './mocha-utils.js'; + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +const extensionPath = path.join(__dirname, '../assets', 'simple-extension'); +const serviceWorkerExtensionPath = path.join( + __dirname, + '..', + 'assets', + 'serviceworkers', + 'extension' +); + +describe('headful tests', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20 * 1000); + + let headfulOptions: PuppeteerLaunchOptions | undefined; + let headlessOptions: PuppeteerLaunchOptions & {headless: boolean}; + let extensionOptions: PuppeteerLaunchOptions & { + headless: boolean; + args: string[]; + }; + let forcedOopifOptions: PuppeteerLaunchOptions & { + headless: boolean; + devtools: boolean; + args: string[]; + }; + let devtoolsOptions: PuppeteerLaunchOptions & { + headless: boolean; + devtools: boolean; + }; + const browsers: any[] = []; + + beforeEach(() => { + const {server, defaultBrowserOptions} = getTestState(); + headfulOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + }); + headlessOptions = Object.assign({}, defaultBrowserOptions, { + headless: true, + }); + + extensionOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + ], + }); + + forcedOopifOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + devtools: true, + args: [ + `--host-rules=MAP oopifdomain 127.0.0.1`, + `--isolate-origins=${server.PREFIX.replace( + 'localhost', + 'oopifdomain' + )}`, + ], + }); + + devtoolsOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + devtools: true, + }); + }); + + async function launchBrowser(puppeteer: PuppeteerNode, options: any) { + const browser = await puppeteer.launch(options); + browsers.push(browser); + return browser; + } + + afterEach(() => { + for (const i in browsers) { + const browser = browsers[i]; + if (browser.isConnected()) { + browser.close(); + } + delete browsers[i]; + } + }); + + describe('HEADFUL', function () { + it('background_page target type should be available', async () => { + const {puppeteer} = getTestState(); + const browserWithExtension = await launchBrowser( + puppeteer, + 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 {puppeteer, defaultBrowserOptions} = getTestState(); + const browserWithExtension = await launchBrowser(puppeteer, { + ...defaultBrowserOptions, + headless: false, + 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 {puppeteer} = getTestState(); + const browserWithExtension = await launchBrowser( + puppeteer, + 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(); + }); + it('target.page() should return a DevTools page if custom isPageTarget is provided', async function () { + const {puppeteer} = getTestState(); + const originalBrowser = await launchBrowser(puppeteer, devtoolsOptions); + + 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); + await browser.close(); + }); + it('should have default url when launching browser', async function () { + const {puppeteer} = getTestState(); + const browser = await launchBrowser(puppeteer, extensionOptions); + const pages = (await browser.pages()).map((page: {url: () => any}) => { + return page.url(); + }); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + 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, puppeteer} = getTestState(); + + const userDataDir = await mkdtemp(TMP_FOLDER); + // Write a cookie in headful chrome + const headfulBrowser = await launchBrowser( + puppeteer, + 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( + puppeteer, + 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'); + }); + // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548 + it.skip('OOPIF: should report google.com frame', async () => { + const {server, puppeteer} = getTestState(); + + // https://google.com is isolated by default in Chromium embedder. + const browser = await launchBrowser(puppeteer, headfulOptions); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', (r: {respond: (arg0: {body: string}) => any}) => { + 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: {url: () => any}) => { + return frame.url(); + }) + .sort(); + expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']); + await browser.close(); + }); + it('OOPIF: should expose events within OOPIFs', async () => { + const {server, puppeteer} = getTestState(); + + const browser = await launchBrowser(puppeteer, forcedOopifOptions); + const page = await browser.newPage(); + + // Setup our session listeners to observe OOPIF activity. + const session = await page.target().createCDPSession(); + const networkEvents: any[] = []; + const otherSessions: any[] = []; + await session.send('Target.setAutoAttach', { + autoAttach: true, + flatten: true, + waitForDebuggerOnStart: true, + }); + session.on( + 'sessionattached', + async (session: { + on: (arg0: string, arg1: (params: any) => number) => void; + send: (arg0: string) => any; + }) => { + otherSessions.push(session); + + session.on('Network.requestWillBeSent', (params: any) => { + return networkEvents.push(params); + }); + 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, + }); + await browser.close(); + + const requests = networkEvents.map(event => { + return event.request.url; + }); + expect(requests).toContain(`http://oopifdomain:${server.PORT}/fetch`); + }); + it('should close browser with beforeunload page', async () => { + const {server, puppeteer} = getTestState(); + + const browser = await launchBrowser(puppeteer, headfulOptions); + 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'); + await browser.close(); + }); + it('should open devtools when "devtools: true" option is given', async () => { + const {puppeteer} = getTestState(); + + const browser = await launchBrowser( + puppeteer, + Object.assign({devtools: true}, headfulOptions) + ); + const context = await browser.createIncognitoBrowserContext(); + await Promise.all([ + context.newPage(), + browser.waitForTarget((target: {url: () => string | string[]}) => { + return target.url().includes('devtools://'); + }), + ]); + await browser.close(); + }); + }); + + describe('Page.bringToFront', function () { + it('should work', async () => { + const {puppeteer} = getTestState(); + const browser = await launchBrowser(puppeteer, headfulOptions); + 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(); + await browser.close(); + }); + }); + + describe('Page.screenshot', function () { + it('should run in parallel in multiple pages', async () => { + const {server, puppeteer} = getTestState(); + const browser = await puppeteer.launch(headfulOptions); + 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 browser.close(); + }); + }); +}); 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..1b2dbb894a --- /dev/null +++ b/remote/test/puppeteer/test/src/idle_override.spec.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +import expect from 'expect'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Emulate idle state', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + async function getIdleState() { + const {page} = getTestState(); + + const stateElement = (await page.$('#state')) as ElementHandle; + return await page.evaluate(element => { + return element.innerText; + }, stateElement); + } + + async function verifyState(expectedState: string) { + const actualState = await getIdleState(); + expect(actualState).toEqual(expectedState); + } + + it('changing idle state emulation causes change of the IdleDetector state', async () => { + const {page, server, context} = 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(); + + // Emulate Idle states and verify IdleDetector updates state accordingly. + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: false, + }); + await verifyState('Idle state: idle, locked.'); + + await page.emulateIdleState({ + isUserActive: true, + isScreenUnlocked: false, + }); + await verifyState('Idle state: active, locked.'); + + await page.emulateIdleState({ + isUserActive: true, + isScreenUnlocked: true, + }); + await verifyState('Idle state: active, unlocked.'); + + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: true, + }); + await verifyState('Idle state: idle, unlocked.'); + + // Remove Idle emulation and verify IdleDetector is in initial state. + await page.emulateIdleState(); + await verifyState(initialState); + + // Emulate idle state again after removing emulation. + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: false, + }); + await verifyState('Idle state: idle, locked.'); + + // Remove emulation second time. + await page.emulateIdleState(); + await verifyState(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..8217092e22 --- /dev/null +++ b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import {TLSSocket} from 'tls'; + +import expect from 'expect'; +import {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {getTestState} 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 browser!: Browser; + let context: BrowserContext; + let page!: Page; + + before(async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign( + {ignoreHTTPSErrors: true}, + defaultBrowserOptions + ); + browser = await puppeteer.launch(options); + }); + + after(async () => { + await browser.close(); + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + }); + + describe('Response.securityDetails', function () { + it('should work', async () => { + const {httpsServer} = getTestState(); + + 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} = getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.securityDetails()).toBe(null); + }); + it('Network redirects should report SecurityDetails', async () => { + const {httpsServer} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + httpsServer.setRoute('/mixedcontent.html', (_req, res) => { + res.end(``); + }); + 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..836ee479a1 --- /dev/null +++ b/remote/test/puppeteer/test/src/injected.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2022 Google Inc. 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. + */ + +import expect from 'expect'; +import {PUPPETEER_WORLD} from 'puppeteer-core/internal/common/IsolatedWorlds.js'; +import {LazyArg} from 'puppeteer-core/internal/common/LazyArg.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('PuppeteerUtil tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page} = getTestState(); + + const world = page.mainFrame().worlds[PUPPETEER_WORLD]; + 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} = getTestState(); + + const world = page.mainFrame().worlds[PUPPETEER_WORLD]; + 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..5519fca4c1 --- /dev/null +++ b/remote/test/puppeteer/test/src/input.spec.ts @@ -0,0 +1,409 @@ +/** + * Copyright 2017 Google Inc. 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. + */ + +import path from 'path'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} 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(); + setupTestPageAndContextHooks(); + + describe('input', function () { + it('should upload the file', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/fileupload.html'); + const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); + const 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} = getTestState(); + + await page.setContent(``); + 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} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.setContent(``); + 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} = getTestState(); + + await page.setContent( + `` + ); + 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} = getTestState(); + + await page.setContent(``); + 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 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} = getTestState(); + + await page.setContent(``); + 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); + 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} = getTestState(); + + await page.setContent(``); + 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} = getTestState(); + + await page.setContent(``); + 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} = getTestState(); + + await page.setContent(``); + 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 promise.then(() => { + return false; + }); + }) + ).toBeFalsy(); + }); + it('should fail when accepting file chooser twice', async () => { + const {page} = getTestState(); + + await page.setContent(``); + 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} = 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(``); + 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} = getTestState(); + + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLElement).click(); + }), + ]); + await fileChooser.cancel(); + let error!: Error; + + try { + 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} = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(false); + }); + it('should work for "multiple"', async () => { + const {page} = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + it('should work for "webkitdirectory"', async () => { + const {page} = getTestState(); + + await page.setContent(``); + 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..3d307a0523 --- /dev/null +++ b/remote/test/puppeteer/test/src/jshandle.spec.ts @@ -0,0 +1,339 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('JSHandle', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluateHandle', function () { + it('should work', async () => { + const {page} = getTestState(); + + const windowHandle = await page.evaluateHandle(() => { + return window; + }); + expect(windowHandle).toBeTruthy(); + }); + it('should return the RemoteObject', async () => { + const {page} = getTestState(); + + const windowHandle = await page.evaluateHandle(() => { + return window; + }); + expect(windowHandle.remoteObject()).toBeTruthy(); + }); + it('should accept object handle as an argument', async () => { + const {page} = getTestState(); + + const 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} = getTestState(); + + const 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} = 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} = getTestState(); + + const 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} = getTestState(); + + const 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} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return { + one: 1, + two: 2, + three: 3, + }; + }); + const twoHandle = await aHandle.getProperty('two'); + expect(await twoHandle.jsonValue()).toEqual(2); + }); + }); + + describe('JSHandle.jsonValue', function () { + it('should work', async () => { + const {page} = getTestState(); + + const 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} = getTestState(); + + const 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} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return 'foo'; + }); + expect(await aHandle.jsonValue()).toEqual('foo'); + + const bHandle = await page.evaluateHandle(() => { + return undefined; + }); + expect(await bHandle.jsonValue()).toEqual(undefined); + }); + + it('should not work with dates', async () => { + const {page} = getTestState(); + + const dateHandle = await page.evaluateHandle(() => { + return new Date('2017-09-26T00:00:00.000Z'); + }); + const json = await dateHandle.jsonValue(); + expect(json).toEqual({}); + }); + it('should throw for circular objects', async () => { + const {page} = getTestState(); + + const handle = await page.evaluateHandle(() => { + const t: {t?: unknown; g: number} = {g: 1}; + t.t = t; + return t; + }); + let error!: Error; + await handle.jsonValue().catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Could not serialize referenced object'); + }); + }); + + describe('JSHandle.getProperties', function () { + it('should work', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return { + foo: 'bar', + }; + }); + const properties = await aHandle.getProperties(); + const foo = properties.get('foo')!; + expect(foo).toBeTruthy(); + expect(await foo.jsonValue()).toBe('bar'); + }); + it('should return even non-own properties', async () => { + const {page} = getTestState(); + + const 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} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return document.body; + }); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + }); + it('should return null for non-elements', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return 2; + }); + const element = aHandle.asElement(); + expect(element).toBeFalsy(); + }); + it('should return ElementHandle for TextNodes', async () => { + const {page} = getTestState(); + + await page.setContent('
ee!
'); + const aHandle = await page.evaluateHandle(() => { + return document.querySelector('div')!.firstChild; + }); + const 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} = getTestState(); + + const numberHandle = await page.evaluateHandle(() => { + return 2; + }); + expect(numberHandle.toString()).toBe('JSHandle:2'); + const stringHandle = await page.evaluateHandle(() => { + return 'a'; + }); + expect(stringHandle.toString()).toBe('JSHandle:a'); + }); + it('should work for complicated objects', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return window; + }); + expect(aHandle.toString()).toBe('JSHandle@object'); + }); + it('should work with different subtypes', async () => { + const {page} = 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' + ); + }); + }); +}); 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..1ae6db22dd --- /dev/null +++ b/remote/test/puppeteer/test/src/keyboard.spec.ts @@ -0,0 +1,557 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import os from 'os'; + +import expect from 'expect'; +import {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('Keyboard', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should type into a textarea', async () => { + const {page} = 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 press the metaKey', async () => { + const {page, isFirefox} = getTestState(); + + await page.evaluate(() => { + (window as any).keyPromise = new Promise(resolve => { + return document.addEventListener('keydown', event => { + return resolve(event.key); + }); + }); + }); + await page.keyboard.press('Meta'); + expect(await page.evaluate('keyPromise')).toBe( + isFirefox && os.platform() !== 'darwin' ? 'OS' : 'Meta' + ); + }); + it('should move with the arrow keys', async () => { + const {page, server} = 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 (let i = 0; i < 'World!'.length; i++) { + page.keyboard.press('ArrowLeft'); + } + await page.keyboard.type('inserted '); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('Hello inserted World!'); + page.keyboard.down('Shift'); + for (let i = 0; i < 'inserted '.length; i++) { + page.keyboard.press('ArrowLeft'); + } + 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} = 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.up(cmdKey); + await page.keyboard.down(cmdKey); + 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} = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const 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 support |text| option', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = (await page.$('textarea'))!; + await textarea.press('a', {text: 'ё'}); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('ё'); + }); + it('should send a character with sendCharacter', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.keyboard.sendCharacter('嗨'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('嗨'); + await page.evaluate(() => { + return window.addEventListener( + 'keydown', + e => { + return e.preventDefault(); + }, + true + ); + }); + await page.keyboard.sendCharacter('a'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('嗨a'); + }); + it('should report shiftKey', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + const codeForKey = new Map([ + ['Shift', 16], + ['Alt', 18], + ['Control', 17], + ]); + for (const [modifierKey, modifierCode] of codeForKey) { + await keyboard.down(modifierKey); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + 'Keydown: ' + + modifierKey + + ' ' + + modifierKey + + 'Left ' + + modifierCode + + ' [' + + modifierKey + + ']' + ); + await keyboard.down('!'); + // Shift+! will generate a keypress + if (modifierKey === 'Shift') { + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + 'Keydown: ! Digit1 49 [' + + modifierKey + + ']\nKeypress: ! Digit1 33 33 [' + + modifierKey + + ']' + ); + } else { + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']'); + } + + await keyboard.up('!'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: ! Digit1 49 [' + modifierKey + ']'); + await keyboard.up(modifierKey); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + 'Keyup: ' + + modifierKey + + ' ' + + modifierKey + + 'Left ' + + modifierCode + + ' []' + ); + } + }); + it('should report multiple modifiers', async () => { + const {page, server} = 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 17 [Control]'); + await keyboard.down('Alt'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: Alt AltLeft 18 [Alt Control]'); + await keyboard.down(';'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: ; Semicolon 186 [Alt Control]'); + await keyboard.up(';'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: ; Semicolon 186 [Alt Control]'); + await keyboard.up('Control'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: Control ControlLeft 17 [Alt]'); + await keyboard.up('Alt'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: Alt AltLeft 18 []'); + }); + it('should send proper codes while typing', async () => { + const {page, server} = 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 49 []', + 'Keypress: ! Digit1 33 33 []', + 'Keyup: ! Digit1 49 []', + ].join('\n') + ); + await page.keyboard.type('^'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + [ + 'Keydown: ^ Digit6 54 []', + 'Keypress: ^ Digit6 94 94 []', + 'Keyup: ^ Digit6 54 []', + ].join('\n') + ); + }); + it('should send proper codes while typing with shift', async () => { + const {page, server} = 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 16 [Shift]', + 'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode + 'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode + 'Keyup: ~ Backquote 192 [Shift]', + ].join('\n') + ); + await keyboard.up('Shift'); + }); + it('should not type canceled events', async () => { + const {page, server} = 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} = 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} = 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} = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + event => { + return ((globalThis as any).keyLocation = event.location); + }, + true + ); + }); + const 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} = getTestState(); + + let error = await page.keyboard + // @ts-expect-error bad input + .press('NotARealKey') + .catch(error_ => { + return error_; + }); + expect(error.message).toBe('Unknown key: "NotARealKey"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('ё').catch(error_ => { + return error_; + }); + expect(error && error.message).toBe('Unknown key: "ё"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('😊').catch(error_ => { + return error_; + }); + expect(error && error.message).toBe('Unknown key: "😊"'); + }); + it('should type emoji', async () => { + const {page, server} = 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame( + page, + 'emoji-test', + server.PREFIX + '/input/textarea.html' + ); + const frame = page.frames()[1]!; + const 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 () => { + const {page, isFirefox} = 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 + ]; + if (isFirefox && os.platform() !== 'darwin') { + expect(key).toBe('OS'); + } else { + expect(key).toBe('Meta'); + } + + if (isFirefox) { + expect(code).toBe('OSLeft'); + } else { + expect(code).toBe('MetaLeft'); + } + + if (isFirefox && os.platform() !== 'darwin') { + expect(metaKey).toBe(false); + } else { + 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..61249c0e07 --- /dev/null +++ b/remote/test/puppeteer/test/src/launcher.spec.ts @@ -0,0 +1,897 @@ +/** + * Copyright 2017 Google Inc. 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. + */ +import fs from 'fs'; +import {mkdtemp, readFile, writeFile} from 'fs/promises'; +import os from 'os'; +import path from 'path'; +import {TLSSocket} from 'tls'; + +import {Protocol} from 'devtools-protocol'; +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; +import sinon from 'sinon'; + +import {getTestState, itOnlyRegularInstall} from './mocha-utils.js'; +import {dumpFrames, waitEvent} from './utils.js'; + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); +const FIREFOX_TIMEOUT = 30 * 1000; + +describe('Launcher specs', function () { + if (getTestState().isFirefox) { + this.timeout(FIREFOX_TIMEOUT); + } + + describe('Puppeteer', function () { + describe('Browser.disconnect', function () { + it('should reject navigation when browser closes', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + server.setRoute('/one-style.css', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + 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'); + remote.disconnect(); + const error = await navigationPromise; + expect( + [ + 'Navigation failed because browser has disconnected!', + 'Protocol error (Page.navigate): Target closed.', + ].includes(error.message) + ).toBeTruthy(); + await browser.close(); + }); + it('should reject waitForSelector when browser closes', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + server.setRoute('/empty.html', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const page = await remote.newPage(); + const watchdog = page + .waitForSelector('div', {timeout: 60000}) + .catch(error_ => { + return error_; + }); + remote.disconnect(); + const error = await watchdog; + expect(error.message).toContain('Protocol error'); + await browser.close(); + }); + }); + describe('Browser.close', function () { + it('should terminate network waiters', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + 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).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + await browser.close(); + }); + }); + describe('Puppeteer.launch', function () { + it('can launch and close the browser', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + await browser.close(); + }); + it('should reject all promises when browser is closed', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + const page = await browser.newPage(); + let error!: Error; + const neverResolves = page + .evaluate(() => { + return new Promise(() => {}); + }) + .catch(error_ => { + return (error = error_); + }); + await browser.close(); + await neverResolves; + expect(error.message).toContain('Protocol error'); + }); + it('should reject if executable path is invalid', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + let waitError!: Error; + const options = Object.assign({}, defaultBrowserOptions, { + executablePath: 'random-invalid-path', + }); + await puppeteer.launch(options).catch(error => { + return (waitError = error); + }); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const userDataDir = await mkdtemp(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.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 {defaultBrowserOptions, puppeteer} = getTestState(); + + // 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 browser = await puppeteer.launch(defaultBrowserOptions); + + // 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 browser.newPage(); + await browser.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 {defaultBrowserOptions, puppeteer} = getTestState(); + + 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 options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + + expect(await readFile(prefsJSPath, 'utf8')).toBe(prefsJSContent); + + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir argument', async () => { + const {isChrome, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtemp(TMP_FOLDER); + const options = Object.assign({}, defaultBrowserOptions); + if (isChrome) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}`, + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + '-profile', + userDataDir, + ]; + } + const browser = await puppeteer.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.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, puppeteer, defaultBrowserOptions} = getTestState(); + + 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 browser = await puppeteer.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.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 {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtemp(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return (localStorage['hey'] = 'hello'); + }); + await browser.close(); + + const browser2 = await puppeteer.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect( + await page2.evaluate(() => { + return localStorage['hey']; + }) + ).toBe('hello'); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir option should restore cookies', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtemp(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + 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'); + }); + await browser.close(); + + const browser2 = await puppeteer.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect( + await page2.evaluate(() => { + return document.cookie; + }) + ).toBe('doSomethingOnlyOnce=true'); + await browser2.close(); + // 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} = getTestState(); + + 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} = getTestState(); + if (isChrome) { + expect(puppeteer.product).toBe('chrome'); + } else if (isFirefox) { + expect(puppeteer.product).toBe('firefox'); + } + }); + it('should work with no default arguments', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions); + options.ignoreDefaultArgs = true; + const browser = await puppeteer.launch(options); + try { + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + } finally { + await browser.close(); + } + }); + it('should filter out ignored default arguments in Chrome', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + // Make sure we launch with `--enable-automation` by default. + const defaultArgs = puppeteer.defaultArgs(); + const browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + // Ignore first and third default argument. + ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]], + }) + ); + 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); + await browser.close(); + }); + it('should filter out ignored default argument in Firefox', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const defaultArgs = puppeteer.defaultArgs(); + const browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + // Only the first argument is fixed, others are optional. + ignoreDefaultArgs: [defaultArgs[0]!], + }) + ); + 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); + await browser.close(); + }); + it('should have default URL when launching browser', async function () { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + const pages = (await browser.pages()).map(page => { + return page.url(); + }); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it('should have custom URL when launching browser', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions); + options.args = [server.EMPTY_PAGE].concat(options.args || []); + const browser = await puppeteer.launch(options); + 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); + await browser.close(); + }); + it('should pass the timeout parameter to browser.waitForTarget', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + timeout: 1, + }); + let error!: Error; + await puppeteer.launch(options).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should work with timeout = 0', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + timeout: 0, + }); + const browser = await puppeteer.launch(options); + await browser.close(); + }); + it('should set the default viewport', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: { + width: 456, + height: 789, + }, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('window.innerWidth')).toBe(456); + expect(await page.evaluate('window.innerHeight')).toBe(789); + await browser.close(); + }); + it('should disable the default viewport', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(page.viewport()).toBe(null); + await browser.close(); + }); + it('should take fullPage screenshots when defaultViewport is null', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true, + }); + expect(screenshot).toBeInstanceOf(Buffer); + await browser.close(); + }); + it('should set the debugging port', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + debuggingPort: 9999, + }); + const browser = await puppeteer.launch(options); + const url = new URL(browser.wsEndpoint()); + await browser.close(); + expect(url.port).toBe('9999'); + }); + it('should not allow setting debuggingPort and pipe', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + debuggingPort: 9999, + pipe: true, + }); + + let error!: Error; + await puppeteer.launch(options).catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('either pipe or debugging port'); + }); + it('should launch Chrome properly with --no-startup-window and waitForInitialPage=false', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + 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 = await puppeteer.launch(options); + const pages = await browser.pages(); + expect(pages).toHaveLength(0); + await browser.close(); + }); + }); + + describe('Puppeteer.launch', function () { + itOnlyRegularInstall('should be able to launch Chrome', async () => { + const {puppeteer} = getTestState(); + const browser = await puppeteer.launch({product: 'chrome'}); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Chrome'); + }); + + it('should be able to launch Firefox', async function () { + this.timeout(FIREFOX_TIMEOUT); + const {puppeteer} = getTestState(); + const browser = await puppeteer.launch({product: 'firefox'}); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Firefox'); + }); + }); + + describe('Puppeteer.connect', function () { + it('should be able to connect multiple times to the same browser', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const otherBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint(), + }); + const page = await otherBrowser.newPage(); + expect( + await page.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + otherBrowser.disconnect(); + + const secondPage = await originalBrowser.newPage(); + expect( + await secondPage.evaluate(() => { + return 7 * 6; + }) + ).toBe(42); + await originalBrowser.close(); + }); + it('should be able to close remote browser', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint(), + }); + await Promise.all([ + waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should be able to connect to a browser with no page targets', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const pages = await originalBrowser.pages(); + await Promise.all( + pages.map(page => { + return page.close(); + }) + ); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint(), + }); + await Promise.all([ + waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should support ignoreHTTPSErrors option', async () => { + const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + ignoreHTTPSErrors: true, + }); + const page = await browser.newPage(); + let error!: Error; + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }), + ]); + expect(error).toBeUndefined(); + 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 browser.close(); + }); + + it('should support targetFilter option in puppeteer.launch', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + targetFilter: target => { + return target.type !== 'page'; + }, + waitForInitialPage: false, + }); + try { + const targets = browser.targets(); + expect(targets).toHaveLength(1); + expect( + targets.find(target => { + return target.type() === 'page'; + }) + ).toBeUndefined(); + } finally { + await browser.close(); + } + }); + + // @see https://github.com/puppeteer/puppeteer/issues/4197 + it('should support targetFilter option', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const page1 = await originalBrowser.newPage(); + await page1.goto(server.EMPTY_PAGE); + + const page2 = await originalBrowser.newPage(); + await page2.goto(server.EMPTY_PAGE + '?should-be-ignored'); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + targetFilter: (targetInfo: Protocol.Target.TargetInfo) => { + return !targetInfo.url?.includes('should-be-ignored'); + }, + }); + + const pages = await browser.pages(); + + await page2.close(); + await page1.close(); + await browser.disconnect(); + await originalBrowser.close(); + + expect( + pages + .map((p: Page) => { + return p.url(); + }) + .sort() + ).toEqual(['about:blank', server.EMPTY_PAGE]); + }); + it('should be able to reconnect to a disconnected browser', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const page = await originalBrowser.newPage(); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + originalBrowser.disconnect(); + + const browser = await puppeteer.connect({browserWSEndpoint}); + const pages = await browser.pages(); + const restoredPage = pages.find(page => { + return page.url() === server.PREFIX + '/frames/nested-frames.html'; + })!; + expect(dumpFrames(restoredPage.mainFrame())).toEqual([ + 'http://localhost:/frames/nested-frames.html', + ' http://localhost:/frames/two-frames.html (2frames)', + ' http://localhost:/frames/frame.html (uno)', + ' http://localhost:/frames/frame.html (dos)', + ' http://localhost:/frames/frame.html (aframe)', + ]); + expect( + await restoredPage.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + await browser.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, defaultBrowserOptions} = getTestState(); + + const browserOne = await puppeteer.launch(defaultBrowserOptions); + const browserTwo = await puppeteer.connect({ + browserWSEndpoint: browserOne.wsEndpoint(), + }); + const [page1, page2] = await Promise.all([ + new Promise(x => { + return browserOne.once('targetcreated', target => { + return x(target.page()); + }); + }), + browserTwo.newPage(), + ]); + expect( + await page1.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + expect( + await page2.evaluate(() => { + return 7 * 6; + }) + ).toBe(42); + await browserOne.close(); + }); + it('should be able to reconnect', async () => { + const {puppeteer, server, defaultBrowserOptions} = getTestState(); + const browserOne = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = browserOne.wsEndpoint(); + const pageOne = await browserOne.newPage(); + await pageOne.goto(server.EMPTY_PAGE); + browserOne.disconnect(); + + const browserTwo = await puppeteer.connect({ + browserWSEndpoint, + }); + const pages = await browserTwo.pages(); + const pageTwo = pages.find(page => { + return page.url() === server.EMPTY_PAGE; + })!; + await pageTwo.reload(); + const bodyHandle = await pageTwo.waitForSelector('body', { + timeout: 10000, + }); + await bodyHandle!.dispose(); + await browserTwo.close(); + }); + }); + describe('Puppeteer.executablePath', function () { + itOnlyRegularInstall('should work', async () => { + const {puppeteer} = getTestState(); + + const executablePath = puppeteer.executablePath(); + expect(fs.existsSync(executablePath)).toBe(true); + expect(fs.realpathSync(executablePath)).toBe(executablePath); + }); + it('returns executablePath for channel', () => { + const {puppeteer} = getTestState(); + + const executablePath = puppeteer.executablePath('chrome'); + expect(executablePath).toBeTruthy(); + }); + describe('when executable path is configured', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + const {puppeteer} = getTestState(); + sandbox + .stub(puppeteer.configuration, 'executablePath') + .value('SOME_CUSTOM_EXECUTABLE'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('its value is used', async () => { + const {puppeteer} = getTestState(); + try { + puppeteer.executablePath(); + } catch (error) { + expect((error as Error).message).toContain( + 'SOME_CUSTOM_EXECUTABLE' + ); + } + }); + }); + }); + }); + + describe('Browser target events', function () { + it('should work', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const browser = await puppeteer.launch(defaultBrowserOptions); + const events: string[] = []; + browser.on('targetcreated', () => { + return events.push('CREATED'); + }); + browser.on('targetchanged', () => { + return events.push('CHANGED'); + }); + browser.on('targetdestroyed', () => { + return events.push('DESTROYED'); + }); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); + await browser.close(); + }); + }); + + describe('Browser.Events.disconnected', function () { + it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const remoteBrowser1 = await puppeteer.connect({ + browserWSEndpoint, + }); + const remoteBrowser2 = await puppeteer.connect({ + browserWSEndpoint, + }); + + let disconnectedOriginal = 0; + let disconnectedRemote1 = 0; + let disconnectedRemote2 = 0; + originalBrowser.on('disconnected', () => { + return ++disconnectedOriginal; + }); + remoteBrowser1.on('disconnected', () => { + return ++disconnectedRemote1; + }); + remoteBrowser2.on('disconnected', () => { + return ++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(originalBrowser, 'disconnected'), + originalBrowser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + }); + }); +}); 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..70d9ab505e --- /dev/null +++ b/remote/test/puppeteer/test/src/mocha-utils.ts @@ -0,0 +1,330 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +import fs from 'fs'; +import path from 'path'; + +import {TestServer} from '@pptr/testserver'; +import {Protocol} from 'devtools-protocol'; +import expect from 'expect'; +import * as Mocha from 'mocha'; +import puppeteer from 'puppeteer/lib/cjs/puppeteer/puppeteer.js'; +import {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; +import { + setLogCapture, + getCapturedLogs, +} from 'puppeteer-core/internal/common/Debug.js'; +import { + PuppeteerLaunchOptions, + PuppeteerNode, +} from 'puppeteer-core/internal/node/PuppeteerNode.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; +import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; +import sinon from 'sinon'; + +import {extendExpectWithToBeGolden} from './utils.js'; + +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 getTestState = (): PuppeteerTestState => { + return state as PuppeteerTestState; +}; + +const product = + process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome'; + +const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false; + +const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase() as + | 'true' + | 'false' + | 'new'; +const isHeadless = headless === 'true' || headless === 'new'; +const isFirefox = product === 'firefox'; +const isChrome = product === 'chrome'; +const protocol = process.env['PUPPETEER_PROTOCOL'] || 'cdp'; + +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: protocol as 'cdp' | 'webDriverBiDi', + }, + extraLaunchOptions +); + +(async (): Promise => { + 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 setupGoldenAssertions = (): void => { + const suffix = 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(); + +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 = {}; + +export const itOnlyRegularInstall = ( + description: string, + body: Mocha.AsyncFunc +): Mocha.Test => { + if (alternativeInstall || process.env['BINARY']) { + return it.skip(description, body); + } else { + return it(description, body); + } +}; + +if ( + process.env['MOCHA_WORKER_ID'] === undefined || + process.env['MOCHA_WORKER_ID'] === '0' +) { + console.log( + `Running unit tests with: + -> product: ${product} + -> binary: ${ + defaultBrowserOptions.executablePath || + path.relative(process.cwd(), puppeteer.executablePath()) + } + -> mode: ${ + isHeadless + ? headless === 'new' + ? '--headless=new' + : '--headless' + : 'headful' + }` + ); +} + +process.on('unhandledRejection', reason => { + throw reason; +}); + +export const setupTestBrowserHooks = (): void => { + before(async () => { + const browser = await puppeteer.launch(defaultBrowserOptions); + state.browser = browser; + }); + + after(async () => { + await state.browser?.close(); + state.browser = undefined; + }); +}; + +export const setupTestPageAndContextHooks = (): void => { + beforeEach(async () => { + state.context = await state.browser!.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + + afterEach(async () => { + await state.context?.close(); + state.context = undefined; + state.page = undefined; + }); +}; + +export const mochaHooks = { + beforeAll: [ + async (): Promise => { + const {server, httpsServer} = await setupServer(); + + state.puppeteer = puppeteer; + state.defaultBrowserOptions = defaultBrowserOptions; + state.server = server; + state.httpsServer = httpsServer; + state.isFirefox = isFirefox; + state.isChrome = isChrome; + state.isHeadless = isHeadless; + state.headless = headless; + state.puppeteerPath = path.resolve( + path.join(__dirname, '..', '..', 'packages', 'puppeteer') + ); + }, + ], + + beforeEach: async (): Promise => { + state.server!.reset(); + state.httpsServer!.reset(); + }, + + afterAll: [ + async (): Promise => { + await state.server!.stop(); + state.server = undefined; + await state.httpsServer!.stop(); + state.httpsServer = undefined; + }, + ], + + afterEach: (): void => { + sinon.restore(); + }, +}; + +export const expectCookieEquals = ( + cookies: Protocol.Network.Cookie[], + expectedCookies: Array> +): void => { + const {isChrome} = getTestState(); + if (!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.length).toBe(expectedCookies.length); + for (let i = 0; i < cookies.length; i++) { + expect(cookies[i]).toMatchObject(expectedCookies[i]!); + } +}; + +/** + * 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. + */ +export const describeWithDebugLogs = ( + description: string, + body: (this: Mocha.Suite) => void +): Mocha.Suite | void => { + describe(description + '-debug', () => { + beforeEach(() => { + setLogCapture(true); + }); + + afterEach(function () { + 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); + }); + + describe(description, body); + }); +}; + +export const shortWaitForArrayToHaveAtLeastNElements = async ( + data: unknown[], + minLength: number, + attempts = 3, + timeout = 50 +): Promise => { + for (let i = 0; i < attempts; i++) { + if (data.length >= minLength) { + break; + } + await new Promise(resolve => { + return setTimeout(resolve, timeout); + }); + } +}; + +export const createTimeout = ( + n: number, + value?: T +): Promise => { + return new Promise(resolve => { + setTimeout(() => { + return resolve(value); + }, n); + }); +}; 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..6d7aec47ee --- /dev/null +++ b/remote/test/puppeteer/test/src/mouse.spec.ts @@ -0,0 +1,354 @@ +/** + * Copyright 2018 Google Inc. 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. + */ +import os from 'os'; + +import expect from 'expect'; +import {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +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(); + setupTestPageAndContextHooks(); + it('should click the document', async () => { + const {page} = 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} = 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} = 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); + // Firefox needs an extra frame here after typing or it will fail to set the scrollTop + await page.evaluate(() => { + return new Promise(requestAnimationFrame); + }); + await page.evaluate(() => { + return (document.querySelector('textarea')!.scrollTop = 0); + }); + 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 page.evaluate(() => { + const textarea = document.querySelector('textarea')!; + return textarea.value.substring( + textarea.selectionStart, + textarea.selectionEnd + ); + }) + ).toBe(text); + }); + it('should trigger hover state', async () => { + const {page, server} = 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} = 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} = 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([ + ['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} = getTestState(); + + await page.goto(server.PREFIX + '/input/wheel.html'); + const 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} = 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} = 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 throw if buttons are pressed incorrectly', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.mouse.down(); + await expect(page.mouse.down()).rejects.toBeInstanceOf(Error); + }); + it('should not throw if clicking in parallel', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + interface ClickData { + type: string; + detail: number; + clientX: number; + clientY: number; + isTrusted: boolean; + button: number; + buttons: number; + } + + await page.evaluate(() => { + 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); + document.addEventListener('mouseup', mouseEventListener); + document.addEventListener('click', mouseEventListener); + (window as unknown as {clicks: ClickData[]}).clicks = clicks; + }); + + 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, + }, + }); + }); +}); 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..0f2c17e36f --- /dev/null +++ b/remote/test/puppeteer/test/src/navigation.spec.ts @@ -0,0 +1,850 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import {ServerResponse} from 'http'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, isFavicon, waitEvent} from './utils.js'; + +describe('navigation', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.goto', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with anchor navigation', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'networkidle0', + }); + expect(response!.status()).toBe(200); + }); + it('should navigate to empty page with networkidle2', async () => { + const {page, server} = 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, isChrome} = getTestState(); + + let error!: Error; + await page.goto('asdfasdf').catch(error_ => { + return (error = error_); + }); + if (isChrome) { + expect(error.message).toContain('Cannot navigate to invalid URL'); + } else { + expect(error.message).toContain('Invalid url'); + } + }); + + 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} = 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} = 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).toContain('SSL_ERROR_UNKNOWN'); + } + }); + it('should fail when main resources failed to load', async () => { + const {page, isChrome} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + let error!: Error; + let loaded = false; + page.once('load', () => { + return (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} = 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} = 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} = 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} = 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} = 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} = 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} = 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 + }); + + // 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', + }); + // Track when the navigation gets completed. + let navigationFinished = false; + navigationPromise.then(() => { + return (navigationFinished = true); + }); + + // 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`); + } + + const response = (await navigationPromise)!; + // Expect navigation to succeed. + expect(response.ok()).toBe(true); + }); + it('should not leak listeners during navigation', async function () { + this.timeout(25_000); + + const {page, server} = 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} = 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} = 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} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + const dataURL = 'data:text/html,
yo
'; + 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} = 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} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + let response!: ServerResponse; + server.setRoute('/one-style.css', (_req, res) => { + return (response = res); + }); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html'); + const domContentLoadedPromise = page.waitForNavigation({ + waitUntil: 'domcontentloaded', + }); + + let bothFired = false; + const bothFiredPromise = page + .waitForNavigation({ + waitUntil: ['load', 'domcontentloaded'], + }) + .then(() => { + return (bothFired = true); + }); + + await server.waitForRequest('/one-style.css').catch(() => {}); + await domContentLoadedPromise; + expect(bothFired).toBe(false); + response.end(); + await bothFiredPromise; + await navigationPromise; + }); + it('should work with clicking on anchor links', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(`foobar`); + 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + SPA + + `); + 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + SPA + + `); + 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + back + forward + + `); + 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 () => { + const {page, server} = getTestState(); + + server.setRoute('/frames/style.css', () => {}); + const navigationPromise = page.goto( + server.PREFIX + '/frames/one-frame.html' + ); + const frame = await waitEvent(page, 'frameattached'); + await waitEvent(page, 'framenavigated', f => { + return f === frame; + }); + await Promise.all([ + frame.evaluate(() => { + return window.stop(); + }), + navigationPromise, + ]); + }); + }); + + describe('Page.goBack', function () { + it('should work', async () => { + const {page, server} = 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} = 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} = 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} = 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).toBe('Navigating frame was detached'); + }); + it('should return matching responses', async () => { + const {page, server} = 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 = []; + 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']; + for (const i of [1, 2, 0]) { + serverResponses[i]!.end(serverResponseTexts[i]); + const response = (await navigations[i])!; + expect(response.frame()).toBe(frames[i]); + expect(await response.text()).toBe(serverResponseTexts[i]); + } + }); + }); + + describe('Frame.waitForNavigation', function () { + it('should work', async () => { + const {page, server} = 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} = 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).toBe('Navigating frame was detached'); + }); + }); + + describe('Page.reload', function () { + it('should work', async () => { + const {page, server} = 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..f3899a5ff7 --- /dev/null +++ b/remote/test/puppeteer/test/src/network.spec.ts @@ -0,0 +1,863 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import fs from 'fs'; +import {ServerResponse} from 'http'; +import path from 'path'; + +import expect from 'expect'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, isFavicon, waitEvent} from './utils.js'; + +describe('network', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.Events.Request', function () { + it('should fire for navigation requests', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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} = 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 Chrome as user agent header', async () => { + const {page, server} = getTestState(); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.request().headers()['user-agent']).toContain('Chrome'); + }); + + it('should define Firefox as user agent header', async () => { + const {page, server} = getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.request().headers()['user-agent']).toContain('Firefox'); + }); + }); + + describe('Response.headers', function () { + it('should work', async () => { + const {page, server} = 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} = 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} = getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.fromCache()).toBe(false); + }); + + it('should work', async () => { + const {page, server} = 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} = getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.fromServiceWorker()).toBe(false); + }); + + it('Response.fromServiceWorker', async () => { + const {page, server} = 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 await (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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (_req, res) => { + return res.end(); + }); + + const [request] = await Promise.all([ + waitEvent(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} = getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.request().postData()).toBe(undefined); + }); + }); + + describe('Response.text', function () { + it('should work', async () => { + const {page, server} = 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} = 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} = 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} = 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(x => { + serverResponse.end('ld!', () => { + return x(); + }); + }); + expect(await responseText).toBe('hello world!'); + }); + }); + + describe('Response.json', function () { + it('should work', async () => { + const {page, server} = 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} = 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} = 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} = 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( + 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} = 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} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('css')) { + request.abort(); + } else { + 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} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('requestfinished', 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]!.response()).toBeTruthy(); + expect(requests[0]!.frame() === page.mainFrame()).toBe(true); + expect(requests[0]!.frame()!.url()).toBe(server.EMPTY_PAGE); + }); + it('should fire events in proper order', async () => { + const {page, server} = 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); + expect(events).toEqual(['request', 'response', 'requestfinished']); + }); + it('should support redirects', async () => { + const {page, server} = 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} = 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} = getTestState(); + + const requests = new Map(); + page.on('request', request => { + requests.set(request.url().split('/').pop(), request); + 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} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return requests.push(request); + }); + await page.goto(server.PREFIX + '/pptr.png'); + expect(requests[0]!.isNavigationRequest()).toBe(true); + }); + }); + + describe('Page.setExtraHTTPHeaders', function () { + it('should work', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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} = 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', async () => { + it('Same-origin set-cookie navigation', async () => { + const {page, server} = 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} = 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 responsePromise = waitEvent(page, 'response'); + page.evaluate(() => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', '/foo'); + xhr.send(); + }); + const subresourceResponse = await responsePromise; + expect(subresourceResponse.headers()['set-cookie']).toBe(setCookieString); + }); + + it('Cross-origin set-cookie', async () => { + const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + ignoreHTTPSErrors: true, + }); + + const page = await browser.newPage(); + + 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(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 page.close(); + await browser.close(); + } + }); + }); +}); 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..744b31082f --- /dev/null +++ b/remote/test/puppeteer/test/src/oopif.spec.ts @@ -0,0 +1,454 @@ +/** + * Copyright 2017 Google Inc. 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. + */ + +import expect from 'expect'; +import {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {describeWithDebugLogs, getTestState} from './mocha-utils.js'; +import {attachFrame, detachFrame, navigateFrame} from './utils.js'; + +describeWithDebugLogs('OOPIF', function () { + /* We use a special browser for this test as we need the --site-per-process flag */ + let browser: Browser; + let context: BrowserContext; + let page: Page; + + before(async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat([ + '--site-per-process', + '--remote-debugging-port=21222', + '--host-rules=MAP * 127.0.0.1', + ]), + }) + ); + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + }); + + after(async () => { + await browser.close(); + }); + + it('should treat OOP iframes and normal iframes the same', async () => { + const {server} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + + 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-ignore + _test = 'Test 123!'; + }); + const result = await frame.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return window._test; + }); + expect(result).toBe('Test 123!'); + }); + it('should provide access to elements', async () => { + const {server, isHeadless, headless} = getTestState(); + + 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} = getTestState(); + + 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} = getTestState(); + 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} = getTestState(); + + const frame = page.waitForFrame(frame => { + return frame.url().endsWith('/oopif.html'); + }); + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context)).toHaveLength(1); + }); + it('should support frames within OOP iframes', async () => { + const {server} = getTestState(); + + 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} = getTestState(); + 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); + }); + const 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} = getTestState(); + + 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(); + browser1.disconnect(); + }); + + it('should support lazy OOP frames', async () => { + const {server} = getTestState(); + + 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} = getTestState(); + + 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'); + }); + }); + }); +}); + +function oopifs(context: BrowserContext) { + return context.targets().filter(target => { + return target._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..6bf28e10b1 --- /dev/null +++ b/remote/test/puppeteer/test/src/page.spec.ts @@ -0,0 +1,2330 @@ +/** + * Copyright 2017 Google Inc. 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. + */ +import fs from 'fs'; +import {ServerResponse} from 'http'; +import path from 'path'; + +import expect from 'expect'; +import {KnownDevices, TimeoutError} from 'puppeteer'; +import {Metrics, Page} from 'puppeteer-core/internal/api/Page.js'; +import {CDPSession} from 'puppeteer-core/internal/common/Connection.js'; +import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; +import {CDPPage} from 'puppeteer-core/internal/common/Page.js'; +import sinon from 'sinon'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, detachFrame, waitEvent} from './utils.js'; + +describe('Page', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.close', function () { + it('should reject all promises when page is closed', async () => { + const {context} = 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} = 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} = 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} = 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} = 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} = 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).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + }); + }); + + describe('Page.Events.Load', function () { + it('should fire when expected', async () => { + const {page} = getTestState(); + + await Promise.all([page.goto('about:blank'), waitEvent(page, 'load')]); + }); + }); + + describe('removing and adding event handlers', () => { + it('should correctly fire event handlers as they are added and then removed', async () => { + const {page, server} = getTestState(); + + const handler = sinon.spy(); + const onResponse = (response: {url: () => string}) => { + // Ignore default favicon requests. + if (!response.url().endsWith('favicon.ico')) { + 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} = getTestState(); + + const handler = sinon.spy(); + const onResponse = (response: {url: () => string}) => { + // Ignore default favicon requests. + if (!response.url().endsWith('favicon.ico')) { + 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} = getTestState(); + + let navigate: Promise; + if (isChrome) { + navigate = page.goto('chrome://crash').catch(() => {}); + } else { + navigate = page.goto('about:crashcontent').catch(() => {}); + } + const [error] = await Promise.all([ + waitEvent(page, 'error'), + navigate, + ]); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describe('Page.Events.Popup', function () { + it('should work', async () => { + const {page} = getTestState(); + + const [popup] = await Promise.all([ + waitEvent(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} = getTestState(); + + const [popup] = await Promise.all([ + waitEvent(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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + waitEvent(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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + 'yo' + ); + const [popup] = await Promise.all([ + waitEvent(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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + 'yo' + ); + const [popup] = await Promise.all([ + waitEvent(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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + 'yo' + ); + const [popup] = await Promise.all([ + waitEvent(page, 'popup'), + page.click('a'), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + }); + }); + + 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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'); + }); + }); + + describe('Page.setGeolocation', function () { + it('should work', async () => { + const {page, server, context} = 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} = 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} = 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} = 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('ExecutionContext.queryObjects', function () { + it('should work', async () => { + const {page} = getTestState(); + + // Create a custom class + const 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. + const prototypeHandle = await page.evaluateHandle(CustomClass => { + return CustomClass.prototype; + }, classHandle); + const 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} = getTestState(); + await page.goto(server.EMPTY_PAGE); + + // Create a custom class + const 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. + const prototypeHandle = await page.evaluateHandle(CustomClass => { + return CustomClass.prototype; + }, classHandle); + const 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} = getTestState(); + + const prototypeHandle = await page.evaluateHandle(() => { + return HTMLBodyElement.prototype; + }); + 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} = getTestState(); + + const 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' + ); + }); + }); + + describe('Page.Events.Console', function () { + it('should work', async () => { + const {page} = getTestState(); + + const [message] = await Promise.all([ + waitEvent(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 for different console API calls with logging functions', async () => { + const {page} = 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(() => { + 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} = 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} = getTestState(); + + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(() => { + return console.error(window); + }), + ]); + expect(message.text()).toBe('JSHandle@object'); + }); + it('should trigger correct Log', async () => { + const {page, server, isChrome} = getTestState(); + + await page.goto('about:blank'); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(async (url: string) => { + return 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} = 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(``), + ]); + 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} = 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} = 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 = ``; + 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} = getTestState(); + + page.goto('about:blank'); + await waitEvent(page, 'domcontentloaded'); + }); + }); + + describe('Page.metrics', function () { + it('should get metrics from a page', async () => { + const {page} = getTestState(); + + await page.goto('about:blank'); + const metrics = await page.metrics(); + checkMetrics(metrics); + }); + it('metrics event fired on console.timeStamp', async () => { + const {page} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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(() => { + return (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} = 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} = 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} = 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} = getTestState(); + + await page.goto(server.PREFIX + '/abort-request.html'); + + const element = await page.$(`#abort`); + await element!.click(); + + let error = false; + await page.waitForNetworkIdle().catch(() => { + return (error = true); + }); + + expect(error).toBe(false); + }); + }); + + describe('Page.exposeFunction', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return a * b; + }); + const result = await page.evaluate(async function () { + return await (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should throw exception in page context', async () => { + const {page} = getTestState(); + + await page.exposeFunction('woof', () => { + throw new Error('WOOF WOOF'); + }); + const {message, stack} = await page.evaluate(async () => { + try { + return await (globalThis as any).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} = getTestState(); + + await page.exposeFunction('woof', function () { + throw null; + }); + const thrown = await page.evaluate(async () => { + try { + await (globalThis as any).woof(); + } catch (error) { + return error; + } + }); + expect(thrown).toBe(null); + }); + it('should be callable from-inside evaluateOnNewDocument', async () => { + const {page} = 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} = 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 await (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should await returned promise', async () => { + const {page} = getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function () { + return await (globalThis as any).compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames', async () => { + const {page, server} = 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 await (globalThis as any).compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames before navigation', async () => { + const {page, server} = 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 await (globalThis as any).compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should not throw when frames detach', async () => { + const {page, server} = 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 await (globalThis as any).compute(3, 5); + }) + ).resolves.toEqual(15); + }); + it('should work with complex objects', async () => { + const {page} = getTestState(); + + await page.exposeFunction( + 'complexObject', + function (a: {x: any}, b: {x: any}) { + 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} = 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 await (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + }); + + describe('Page.Events.PageError', function () { + it('should fire', async () => { + const {page, server} = getTestState(); + + const [error] = await Promise.all([ + waitEvent(page, 'pageerror'), + page.goto(server.PREFIX + '/error.html'), + ]); + expect(error.message).toContain('Fancy'); + }); + }); + + describe('Page.setUserAgent', function () { + it('should work', async () => { + const {page, server} = 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} = 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} = 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} = 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-ignore: 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-ignore: 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 = + '
hello
'; + it('should work', async () => { + const {page} = getTestState(); + + await page.setContent('
hello
'); + const result = await page.content(); + expect(result).toBe(expectedOutput); + }); + it('should work with doctype', async () => { + const {page} = getTestState(); + + const doctype = ''; + await page.setContent(`${doctype}
hello
`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should work with HTML 4 doctype', async () => { + const {page} = getTestState(); + + const doctype = + ''; + await page.setContent(`${doctype}
hello
`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should respect timeout', async () => { + const {page, server} = getTestState(); + + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error!: Error; + await page + .setContent(``, { + timeout: 1, + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default navigation timeout', async () => { + const {page, server} = getTestState(); + + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error!: Error; + await page + .setContent(``) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should await resources to load', async () => { + const {page, server} = getTestState(); + + const imgPath = '/img.png'; + let imgResponse!: ServerResponse; + server.setRoute(imgPath, (_req, res) => { + return (imgResponse = res); + }); + let loaded = false; + const contentPromise = page + .setContent(``) + .then(() => { + return (loaded = true); + }); + await server.waitForRequest(imgPath); + expect(loaded).toBe(false); + imgResponse.end(); + await contentPromise; + }); + it('should work fast enough', async () => { + const {page} = getTestState(); + + for (let i = 0; i < 20; ++i) { + await page.setContent('
yo
'); + } + }); + it('should work with tricky content', async () => { + const {page} = getTestState(); + + await page.setContent('
hello world
' + '\x7F'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('hello world'); + }); + it('should work with accents', async () => { + const {page} = getTestState(); + + await page.setContent('
aberración
'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('aberración'); + }); + it('should work with emojis', async () => { + const {page} = getTestState(); + + await page.setContent('
🐥
'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('🐥'); + }); + it('should work with newline', async () => { + const {page} = getTestState(); + + await page.setContent('
\n
'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('\n'); + }); + }); + + describe('Page.setBypassCSP', function () { + it('should bypass CSP meta tag', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const 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} = 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const 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} = 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addStyleTag({ + path: path.join(__dirname, '../assets/injectedstyle.css'), + }); + const 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const 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} = 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} = 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} = 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} = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto( + 'data:text/html, ' + ); + 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, ' + ); + 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} = 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} = 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} = getTestState(); + + const outputFile = __dirname + '/../assets/output.pdf'; + await page.goto(server.PREFIX + '/pdf.html'); + await page.pdf({path: outputFile}); + expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0); + fs.unlinkSync(outputFile); + }); + + it('can print to PDF and stream the result', async () => { + const {page} = 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} = getTestState(); + + await page.goto(server.PREFIX + '/pdf.html'); + + let error!: Error; + await page.pdf({timeout: 1}).catch(_error => { + return (error = _error); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + }); + + describe('Page.title', function () { + it('should return the page title', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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} = 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 element.'); + }); + it('should return [] on no matched values', async () => { + const {page, server} = 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} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.setContent(''); + 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} = 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} = getTestState(); + + const newPagePromise = new Promise(fulfill => { + return context.once('targetcreated', target => { + return fulfill(target.page()); + }); + }); + await page.evaluate(() => { + return ((window as any)['newPage'] = window.open('about:blank')); + }); + const newPage = await newPagePromise; + 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} = 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} = getTestState(); + + expect(page.browser()).toBe(browser); + }); + }); + + describe('Page.browserContext', function () { + it('should return the correct browser context instance', async () => { + const {page, context} = getTestState(); + + expect(page.browserContext()).toBe(context); + }); + }); + + describe('Page.client', function () { + it('should return the client instance', async () => { + const {page} = getTestState(); + expect((page as CDPPage)._client()).toBeInstanceOf(CDPSession); + }); + }); +}); 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..de0e9f543a --- /dev/null +++ b/remote/test/puppeteer/test/src/proxy.spec.ts @@ -0,0 +1,236 @@ +/** + * Copyright 2021 Google Inc. 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. + */ + +import http from 'http'; +import type {Server, IncomingMessage, ServerResponse} from 'http'; +import type {AddressInfo} from 'net'; +import os from 'os'; + +import {TestServer} from '@pptr/testserver'; +import expect from 'expect'; +import type {Browser} from 'puppeteer-core/internal/api/Browser.js'; + +import {getTestState} 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 browser: Browser; + let proxiedRequestUrls: string[]; + let proxyServer: Server; + let proxyServerUrl: string; + const defaultArgs = [ + '--disable-features=NetworkTimeServiceQuerying', // We disable this in tests so that proxy-related tests don't intercept queries from this service in headful. + ]; + + 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 browser.close(); + + await new Promise((resolve, reject) => { + proxyServer.close(error => { + if (error) { + reject(error); + } else { + resolve(undefined); + } + }); + }); + }); + + it('should proxy requests when configured', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`], + }); + + const page = await browser.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + }); + + it('should respect proxy bypass list', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [ + ...defaultArgs, + `--proxy-server=${proxyServerUrl}`, + `--proxy-bypass-list=${new URL(emptyPageUrl).host}`, + ], + }); + + const page = await browser.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([]); + }); + + describe('in incognito browser context', () => { + it('should proxy requests when configured at browser level', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`], + }); + + 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]); + }); + + it('should respect proxy bypass list when configured at browser level', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [ + ...defaultArgs, + `--proxy-server=${proxyServerUrl}`, + `--proxy-bypass-list=${new URL(emptyPageUrl).host}`, + ], + }); + + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([]); + }); + + /** + * See issues #7873, #7719, and #7698. + */ + it('should proxy requests when configured at context level', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: defaultArgs, + }); + + 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]); + }); + + it('should respect proxy bypass list when configured at context level', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: defaultArgs, + }); + + 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([]); + }); + }); +}); 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..195a9e7e6f --- /dev/null +++ b/remote/test/puppeteer/test/src/queryhandler.spec.ts @@ -0,0 +1,594 @@ +/** + * Copyright 2018 Google Inc. 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. + */ +import assert from 'assert'; + +import expect from 'expect'; +import {Puppeteer} from 'puppeteer-core'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Query handler tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Pierce selectors', function () { + beforeEach(async () => { + const {page} = getTestState(); + await page.setContent( + `` + ); + }); + it('should find first element in shadow', async () => { + const {page} = getTestState(); + const div = (await page.$('pierce/.foo')) as ElementHandle; + const text = await div.evaluate(element => { + return element.textContent; + }); + expect(text).toBe('Hello'); + }); + it('should find all elements in shadow', async () => { + const {page} = getTestState(); + const divs = (await page.$$('pierce/.foo')) as Array< + ElementHandle + >; + 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} = getTestState(); + const parentElement = (await page.$('html > div'))!; + const childElement = (await parentElement.$( + 'pierce/div' + )) as ElementHandle; + const text = await childElement.evaluate(element => { + return element.textContent; + }); + expect(text).toBe('Hello'); + }); + it('should find all child elements', async () => { + const {page} = getTestState(); + const parentElement = (await page.$('html > div'))!; + const childElements = (await parentElement.$$('pierce/div')) as Array< + ElementHandle + >; + 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} = getTestState(); + + await page.setContent('
test
'); + + 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} = getTestState(); + + expect(await page.$('text/test')).toBeFalsy(); + expect(await page.$$('text/test')).toHaveLength(0); + }); + it('should return first element', async () => { + const {page} = getTestState(); + + await page.setContent('
a
a
'); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.id; + }) + ).toBe('1'); + }); + it('should return multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('
a
a
'); + + const elements = await page.$$('text/a'); + expect(elements).toHaveLength(2); + }); + it('should pierce shadow DOM', async () => { + const {page} = 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); + }); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a'); + }); + it('should query deeply nested text', async () => { + const {page} = getTestState(); + + await page.setContent('
a
b
'); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a'); + }); + it('should query inputs', async () => { + const {page} = getTestState(); + + await page.setContent(''); + + const element = (await page.$( + 'text/a' + )) as ElementHandle; + expect( + await element?.evaluate(e => { + return e.value; + }) + ).toBe('a'); + }); + it('should not query radio', async () => { + const {page} = getTestState(); + + await page.setContent(''); + + expect(await page.$('text/a')).toBeNull(); + }); + it('should query text spanning multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('
a b
'); + + const element = await page.$('text/a b'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a b'); + }); + it('should clear caches', async () => { + const {page} = getTestState(); + + await page.setContent( + '
text
text
' + ); + const div = (await page.$('#target1')) as ElementHandle; + const input = (await page.$( + '#target2' + )) as ElementHandle; + + 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} = getTestState(); + + await page.setContent('
a
'); + + const 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} = getTestState(); + + await page.setContent('
'); + + const 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} = getTestState(); + + await page.setContent('
test
'); + + 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} = 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} = getTestState(); + + await page.setContent('
a
'); + + const 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} = getTestState(); + + await page.setContent('
'); + + const elements = await page.$$('xpath/html/body/div'); + expect(elements).toHaveLength(2); + }); + }); + describe('in ElementHandles', function () { + it('should query existing element', async () => { + const {page} = getTestState(); + + await page.setContent('
a
'); + + const 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} = getTestState(); + + await page.setContent('
a
'); + + const elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`xpath/span`)).toBeFalsy(); + expect(await elementHandle.$$(`xpath/span`)).toHaveLength(0); + }); + }); + }); + + describe('P selectors', () => { + beforeEach(async () => { + const {page, server} = getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + Puppeteer.clearCustomQueryHandlers(); + }); + + it('should work with CSS selectors', async () => { + const {page} = getTestState(); + const 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 {page} = getTestState(); + { + const element = await page.$('div >>>> div'); + console.log({element}) + 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 {page} = getTestState(); + const 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 {page} = getTestState(); + const 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 XPath selectors', async () => { + const {page} = getTestState(); + const 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 {page} = getTestState(); + const 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 {page} = getTestState(); + Puppeteer.registerCustomQueryHandler('div', { + queryOne(_, selector) { + if (selector === 'true') { + return document.querySelector('div'); + } else { + return document.querySelector('button'); + } + }, + }); + + { + const element = await page.$('::-p-div(true)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const element = await page.$('::-p-div("true")'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const element = await page.$("::-p-div('true')"); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const 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 {page} = getTestState(); + let button = await page.$('div ::-p-text(world)'); + assert(button, 'Could not find element'); + await button.hover(); + await button.dispose(); + + button = await page.$('div ::-p-text(world):hover'); + assert(button, 'Could not find element'); + const value = await button.evaluate(span => { + return {textContent: span.textContent, tagName: span.tagName}; + }); + expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'}); + }); + + it('should work with selector lists', async () => { + const {page} = getTestState(); + const elements = await page.$$('div, ::-p-text(world)'); + expect(elements).toHaveLength(3); + }); + + const permute = (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 {page} = getTestState(); + 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 {page} = getTestState(); + const elements = await page.$$('::-p-text(world), button'); + expect(elements).toHaveLength(1); + }); + }); +}); 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..190578bd59 --- /dev/null +++ b/remote/test/puppeteer/test/src/queryselector.spec.ts @@ -0,0 +1,505 @@ +/** + * Copyright 2018 Google Inc. 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. + */ +import expect from 'expect'; +import {Puppeteer} from 'puppeteer'; +import type {CustomQueryHandler} from 'puppeteer-core/internal/common/CustomQueryHandler.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('querySelector', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.$eval', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.setContent('
43543
'); + const idAttribute = await page.$eval('section', e => { + return e.id; + }); + expect(idAttribute).toBe('testAttribute'); + }); + it('should accept arguments', async () => { + const {page} = getTestState(); + + await page.setContent('
hello
'); + 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} = getTestState(); + + await page.setContent('
hello
world
'); + const 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} = 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. + describe('Page.$$eval', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.setContent( + '
hello
beautiful
world!
' + ); + const divsCount = await page.$$eval('div', divs => { + return divs.length; + }); + expect(divsCount).toBe(3); + }); + it('should accept extra arguments', async () => { + const {page} = getTestState(); + await page.setContent( + '
hello
beautiful
world!
' + ); + 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} = getTestState(); + await page.setContent( + '
2
2
1
3
' + ); + const 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} = 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} = getTestState(); + + await page.setContent('
test
'); + const element = (await page.$('section'))!; + expect(element).toBeTruthy(); + }); + it('should return null for non-existing element', async () => { + const {page} = getTestState(); + + const element = (await page.$('non-existing-element'))!; + expect(element).toBe(null); + }); + }); + + describe('Page.$$', function () { + it('should query existing elements', async () => { + const {page} = getTestState(); + + await page.setContent('
A

B
'); + 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} = 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} = getTestState(); + + await page.setContent('
test
'); + 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} = getTestState(); + + const element = await page.$x('/html/body/non-existing-element'); + expect(element).toEqual([]); + }); + it('should return multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('
'); + const elements = await page.$x('/html/body/div'); + expect(elements).toHaveLength(2); + }); + }); + + describe('ElementHandle.$', function () { + it('should query existing element', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '
A
' + ); + const html = (await page.$('html'))!; + const second = (await html.$('.second'))!; + const 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} = getTestState(); + + await page.setContent( + '
B
' + ); + const html = (await page.$('html'))!; + const second = await html.$('.third'); + expect(second).toBe(null); + }); + }); + describe('ElementHandle.$eval', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.setContent( + '
10
' + ); + const 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} = getTestState(); + + const htmlContent = + '
not-a-child-div
a-child-div
'; + await page.setContent(htmlContent); + const 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} = getTestState(); + + const htmlContent = + '
not-a-child-div
'; + await page.setContent(htmlContent); + const 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} = getTestState(); + + await page.setContent( + '
' + ); + const 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} = getTestState(); + + const htmlContent = + '
not-a-child-div
a1-child-div
a2-child-div
'; + await page.setContent(htmlContent); + const 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} = getTestState(); + + const htmlContent = + '
not-a-child-div
'; + await page.setContent(htmlContent); + const 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} = getTestState(); + + await page.setContent( + '
A

B
' + ); + const 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} = getTestState(); + + await page.setContent( + 'A
B' + ); + const 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} = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '
A
' + ); + const 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} = getTestState(); + + await page.setContent( + '
B
' + ); + const 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} = getTestState(); + + await page.setContent( + '
A

B
' + ); + const 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} = getTestState(); + + await page.setContent( + 'A
B' + ); + const html = (await page.$('html'))!; + const elements = await html.$$('allArray/div'); + expect(elements).toHaveLength(0); + }); + it('$$eval should work', async () => { + const {page} = getTestState(); + + await page.setContent( + '
hello
beautiful
world!
' + ); + const divsCount = await page.$$eval('allArray/div', divs => { + return divs.length; + }); + expect(divsCount).toBe(3); + }); + it('$$eval should accept extra arguments', async () => { + const {page} = getTestState(); + await page.setContent( + '
hello
beautiful
world!
' + ); + 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} = getTestState(); + await page.setContent( + '
2
2
1
3
' + ); + const 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} = 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..62f0cb640e --- /dev/null +++ b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts @@ -0,0 +1,983 @@ +/** + * Copyright 2021 Google Inc. 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. + */ + +import fs from 'fs'; +import path from 'path'; + +import expect from 'expect'; +import { + ActionResult, + HTTPRequest, + InterceptResolutionAction, +} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {isFavicon, waitEvent} from './utils.js'; + +describe('request interception', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + 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} = getTestState(); + + const actionResults: ActionResult[] = []; + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('.css')) { + request.continue( + {headers: {...request.headers(), xaction: 'continue'}}, + expectedAction === 'continue' ? 1 : 0 + ); + } else { + request.continue({}, 0); + } + }); + page.on('request', request => { + if (request.url().endsWith('.css')) { + request.respond( + {headers: {xaction: 'respond'}}, + expectedAction === 'respond' ? 1 : 0 + ); + } else { + request.continue({}, 0); + } + }); + page.on('request', request => { + if (request.url().endsWith('.css')) { + request.abort('aborted', expectedAction === 'abort' ? 1 : 0); + } else { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (isFavicon(request)) { + 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'); + 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} = 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(` +
+ +
+ `); + 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} = getTestState(); + + server.setRedirect('/rrredirect', '/empty.html'); + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + }); + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + origin: undefined, // remove "origin" header + }); + 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + if (!isFavicon(request)) { + requests.push(request); + } + 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} = 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} = 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} = getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + await page.setRequestInterception(true); + page.on('request', request => { + expect(request.headers()['foo']).toBe('bar'); + 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} = 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} = getTestState(); + + await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE}); + await page.setRequestInterception(true); + page.on('request', request => { + expect(request.headers()['referer']).toBe(server.EMPTY_PAGE); + request.continue({}, 0); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.ok()).toBe(true); + }); + it('should be abortable', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('.css')) { + request.abort('failed', 0); + } else { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + request.abort('failed', 0); + }); + let abortReason = null; + page.on('request', request => { + abortReason = request.abortErrorReason(); + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + request.abort('internetdisconnected', 0); + }); + + const [failedRequest] = await Promise.all([ + waitEvent(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} = 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} = 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + 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} = 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')) { + request.abort('failed', 0); + } else { + 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} = 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)) { + request.continue({}, 0); + return; + } + 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + request.continue({}, 0); + }); + const dataURL = 'data:text/html,
yo
'; + 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + !isFavicon(request) && requests.push(request); + request.continue({}, 0); + }); + const dataURL = 'data:text/html,
yo
'; + const text = await page.evaluate((url: string) => { + return fetch(url).then(r => { + return r.text(); + }); + }, dataURL); + expect(text).toBe('
yo
'); + 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + 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} = 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} = 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} = 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 => { + request.continue({}, 0); + requests.push(request); + }); + const response = await page.goto( + `data:text/html,` + ); + 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} = getTestState(); + + await page.setContent(''); + await page.setRequestInterception(true); + let request!: HTTPRequest; + page.on('request', async r => { + return (request = r); + }); + 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} = 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} = getTestState(); + + await page.setRequestInterception(true); + const urls = new Set(); + page.on('request', request => { + urls.add(request.url().split('/').pop()); + 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers()); + headers['FOO'] = 'bar'; + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const redirectURL = request.url().includes('/empty.html') + ? server.PREFIX + '/consolelog.html' + : undefined; + request.continue({url: redirectURL}, 0); + }); + + const [consoleMessage] = await Promise.all([ + waitEvent(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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + request.respond( + { + status: 200, + body: 'Yo, page!', + }, + 0 + ); + }); + let response = null; + page.on('request', request => { + response = request.responseForRequest(); + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (!request.url().includes('rrredirect')) { + request.continue({}, 0); + return; + } + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const imageBuffer = fs.readFileSync( + path.join(__dirname, '../assets', 'pptr.png') + ); + 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); + const img = (await page.$('img'))!; + expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); + }); + it('should stringify intercepted request response headers', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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..51e63f4be0 --- /dev/null +++ b/remote/test/puppeteer/test/src/requestinterception.spec.ts @@ -0,0 +1,934 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import fs from 'fs'; +import path from 'path'; + +import expect from 'expect'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {isFavicon, waitEvent} from './utils.js'; + +describe('request interception', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.setRequestInterception', function () { + it('should intercept', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (isFavicon(request)) { + 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'); + 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} = 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(` +
+ +
+ `); + 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} = getTestState(); + + server.setRedirect('/rrredirect', '/empty.html'); + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + }); + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + origin: undefined, // remove "origin" header + }); + 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + if (!isFavicon(request)) { + requests.push(request); + } + 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} = 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} = 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} = 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} = getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + await page.setRequestInterception(true); + page.on('request', request => { + expect(request.headers()['foo']).toBe('bar'); + 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} = 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} = getTestState(); + + await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE}); + await page.setRequestInterception(true); + page.on('request', request => { + expect(request.headers()['referer']).toBe(server.EMPTY_PAGE); + request.continue(); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.ok()).toBe(true); + }); + it('should be abortable', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('.css')) { + request.abort(); + } else { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + request.abort('internetdisconnected'); + }); + const [failedRequest] = await Promise.all([ + waitEvent(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} = 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} = 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + 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} = 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')) { + request.abort(); + } else { + 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} = 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)) { + request.continue(); + return; + } + 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,
yo
'; + 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + !isFavicon(request) && requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,
yo
'; + const text = await page.evaluate((url: string) => { + return fetch(url).then(r => { + return r.text(); + }); + }, dataURL); + expect(text).toBe('
yo
'); + 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} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + 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} = 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} = 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 with encoded server - 2', async () => { + const {page, server} = 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 => { + request.continue(); + requests.push(request); + }); + const response = (await page.goto( + `data:text/html,` + ))!; + 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} = getTestState(); + + await page.setContent(''); + await page.setRequestInterception(true); + let request!: HTTPRequest; + page.on('request', async r => { + return (request = r); + }); + 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} = 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} = getTestState(); + + await page.setRequestInterception(true); + const urls = new Set(); + page.on('request', request => { + urls.add(request.url().split('/').pop()); + 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} = 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} = 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} = 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} = 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers()); + headers['FOO'] = 'bar'; + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const redirectURL = request.url().includes('/empty.html') + ? server.PREFIX + '/consolelog.html' + : undefined; + request.continue({url: redirectURL}); + }); + const [consoleMessage] = await Promise.all([ + waitEvent(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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (!request.url().includes('rrredirect')) { + request.continue(); + return; + } + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const imageBuffer = fs.readFileSync( + path.join(__dirname, '../assets', 'pptr.png') + ); + 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); + const img = (await page.$('img'))!; + expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); + }); + it('should stringify intercepted request response headers', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + 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} = 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/screenshot.spec.ts b/remote/test/puppeteer/test/src/screenshot.spec.ts new file mode 100644 index 0000000000..f38d7ee897 --- /dev/null +++ b/remote/test/puppeteer/test/src/screenshot.spec.ts @@ -0,0 +1,388 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Screenshots', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.screenshot', function () { + it('should work', async () => { + const {page, server} = 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} = 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 use scale for clip', async () => { + const {page, server} = 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 get screenshot bigger than the viewport', async () => { + const {page, server} = 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 run in parallel', async () => { + const {page, server} = 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} = 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 run in parallel in multiple pages', async () => { + const {server, context} = 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 allow transparency', async () => { + const {page, server} = 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} = 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'); + }); + it('should work with webp', async () => { + const {page, server} = 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 work with odd clip size on Retina displays', async () => { + const {page} = getTestState(); + + 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} = 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' + ); + }); + it('should work in "fromSurface: false" mode', async () => { + const {page, server} = 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'); + }); + }); + + describe('ElementHandle.screenshot', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + await page.evaluate(() => { + return window.scrollBy(50, 100); + }); + const 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 {defaultBrowserOptions, puppeteer, server} = getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + defaultViewport: null, + }); + + try { + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + await page.evaluate(() => { + return window.scrollBy(50, 100); + }); + const elementHandle = (await page.$('.box:nth-of-type(3)'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeTruthy(); + } finally { + await browser.close(); + } + }); + it('should take into account padding and border', async () => { + const {page} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + something above + +
+ `); + const 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} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + + await page.setContent(` + something above + +
+ `); + const 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} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + something above + +
+
+ `); + const 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} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(`
 
`); + const 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} = getTestState(); + + await page.setContent('

remove this

'); + const 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} = getTestState(); + + await page.setContent('
'); + const 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} = getTestState(); + + await page.setContent( + '
' + ); + const 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} = getTestState(); + + await page.setContent( + '
' + ); + const elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); + }); + }); +}); 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..3b5e5c3401 --- /dev/null +++ b/remote/test/puppeteer/test/src/target.spec.ts @@ -0,0 +1,336 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import {ServerResponse} from 'http'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; +import {Target} from 'puppeteer-core/internal/common/Target.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Target', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('Browser.targets should return all of the targets', async () => { + const {browser} = 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} = 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} = 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} = 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} = getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget(target => { + return target.page().then(page => { + return page!.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }); + }) + .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.toEqual(otherPage); + }); + it('should report when a new page is created and closed', async () => { + const {page, server, context} = getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget(target => { + return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }) + .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(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} = 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} = getTestState(); + + await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); + + const target = await context.waitForTarget(target => { + return target.type() === 'service_worker'; + }); + 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} = 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'; + }); + 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} = 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} = getTestState(); + + let targetChanged = false; + const listener = () => { + return (targetChanged = true); + }; + context.on('targetchanged', listener); + const targetPromise = waitEvent(context, 'targetcreated'); + const newPagePromise = context.newPage(); + const target = await targetPromise; + expect(target.url()).toBe('about:blank'); + + const newPage = await newPagePromise; + const targetPromise2 = waitEvent(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} = 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'); + }); + const newPage = (await target.page())!; + // Issue a redirect. + serverResponse.writeHead(302, {location: '/injectedstyle.css'}); + serverResponse.end(); + // Wait for the new page to load. + await waitEvent(newPage, 'load'); + // Cleanup. + await newPage.close(); + }); + it('should have an opener', async () => { + const {page, server, context} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [createdTarget] = await Promise.all([ + waitEvent(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} = getTestState(); + + let resolved = false; + const targetPromise = browser.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 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} = getTestState(); + + let error!: Error; + await browser + .waitForTarget( + target => { + return target.url() === server.EMPTY_PAGE; + }, + { + 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..cd78f41b20 --- /dev/null +++ b/remote/test/puppeteer/test/src/touchscreen.spec.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; +import {KnownDevices, BoundingBox} from 'puppeteer'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Touchscreen', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should tap the button', async () => { + const {page, server} = getTestState(); + const iPhone = KnownDevices['iPhone 6']!; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + await page.tap('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + + it('should report touches', async () => { + const {page, server} = getTestState(); + const iPhone = KnownDevices['iPhone 6']!; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/touches.html'); + const button = (await page.$('button'))!; + await button.tap(); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toEqual(['Touchstart: 0', 'Touchend: 0']); + }); + + it('should report touchMove', async () => { + const {page, server} = getTestState(); + const iPhone = KnownDevices['iPhone 6']!; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/touches-move.html'); + const touch = (await page.$('#touch'))!; + const touchObj = (await touch.boundingBox()) as BoundingBox; + await page.touchscreen.touchStart(touchObj.x, touchObj.y); + const movePosx = 100; + const movePosy = 100; + await page.touchscreen.touchMove(movePosx, movePosy); + await page.touchscreen.touchEnd(); + expect( + await page.evaluate(() => { + return (globalThis as any).touchX; + }) + ).toBe(movePosx); + expect( + await page.evaluate(() => { + return (globalThis as any).touchY; + }) + ).toBe(movePosy); + }); +}); 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..b9553c4dac --- /dev/null +++ b/remote/test/puppeteer/test/src/tracing.spec.ts @@ -0,0 +1,157 @@ +/** + * Copyright 2017 Google Inc. 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. + */ + +import fs from 'fs'; +import path from 'path'; + +import expect from 'expect'; +import {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {getTestState} from './mocha-utils.js'; + +describe('Tracing', function () { + let outputFile!: string; + let browser!: Browser; + let page!: Page; + + /* 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 () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + browser = await puppeteer.launch(defaultBrowserOptions); + page = await browser.newPage(); + outputFile = path.join(__dirname, 'trace.json'); + }); + + afterEach(async () => { + await browser.close(); + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + }); + it('should output a trace', async () => { + const {server} = getTestState(); + + 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 () => { + 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 () => { + 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 () => { + 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 {server} = getTestState(); + + 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 {server} = getTestState(); + + 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 {server} = getTestState(); + + await page.tracing.start({screenshots: true}); + await page.goto(server.PREFIX + '/grid.html'); + const oldBufferConcat = Buffer.concat; + Buffer.concat = () => { + throw 'error'; + }; + const trace = await page.tracing.stop(); + expect(trace).toEqual(undefined); + Buffer.concat = oldBufferConcat; + }); + + it('should support a buffer without a path', async () => { + const {server} = getTestState(); + + 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 () => { + 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..43c98e953f --- /dev/null +++ b/remote/test/puppeteer/test/src/utils.ts @@ -0,0 +1,150 @@ +/** + * Copyright 2017 Google Inc. 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. + */ + +import path from 'path'; + +import expect from 'expect'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; +import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import {Frame} from 'puppeteer-core/internal/common/Frame.js'; + +import {compare} from './golden-utils.js'; + +const PROJECT_ROOT = path.join(__dirname, '..', '..'); + +declare module 'expect' { + interface Matchers { + 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 => { + const 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 { + 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 { + await pageOrFrame.evaluate(navigateFrame, frameId, url); + + function navigateFrame(frameId: string, url: any) { + 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}\//, ':/'); + 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 = ( + emitter: EventEmitter, + eventName: string, + predicate: (event: T) => boolean = () => { + return true; + } +): Promise => { + return new Promise(fulfill => { + emitter.on(eventName, (event: T) => { + if (!predicate(event)) { + return; + } + fulfill(event); + }); + }); +}; 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..27977fb7ac --- /dev/null +++ b/remote/test/puppeteer/test/src/waittask.spec.ts @@ -0,0 +1,867 @@ +/** + * Copyright 2018 Google Inc. 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. + */ + +import expect from 'expect'; +import {TimeoutError, ElementHandle} from 'puppeteer'; +import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; + +import { + createTimeout, + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, detachFrame} from './utils.js'; + +describe('waittask specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Frame.waitForFunction', function () { + it('should accept a string', async () => { + const {page} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = getTestState(); + + expect( + await ( + await page.waitForFunction(() => { + return 5; + }) + ).jsonValue() + ).toBe(5); + }); + it('should return the window as a success value', async () => { + const {page} = getTestState(); + + expect( + await page.waitForFunction(() => { + return window; + }) + ).toBeTruthy(); + }); + it('should accept ElementHandle arguments', async () => { + const {page} = getTestState(); + + await page.setContent('
'); + const div = (await page.$('div'))!; + let resolved = false; + const waitForFunction = page + .waitForFunction( + (element: 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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(`
anything
`), + ]); + expect( + await page.evaluate(x => { + return x?.textContent; + }, handle) + ).toBe('anything'); + }); + + it('should resolve promise when node is added', async () => { + const {page, server} = 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'); + const 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} = 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 = + '

'); + }); + await watchdog; + }); + + it('Page.waitForSelector is shortcut for main frame', async () => { + const {page, server} = 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'); + const eHandle = await watchdog; + expect(eHandle?.frame).toBe(page.mainFrame()); + }); + + it('should run in specified frame', async () => { + const {page, server} = 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'); + const eHandle = await waitForSelectorPromise; + expect(eHandle?.frame).toBe(frame2); + }); + + it('should throw when frame is detached', async () => { + const {page, server} = 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} = 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} = getTestState(); + + const promise = page.waitForSelector('div', {visible: true}); + await page.setContent('
text
'); + const 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} = getTestState(); + + const promise = page.waitForSelector('div', {visible: true}); + await page.setContent('
text
'); + const 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} = getTestState(); + + const promise = page.waitForSelector('div', {visible: true}); + await page.setContent('
text
'); + const 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} = getTestState(); + + const promise = page.waitForSelector('div#inner', { + visible: true, + }); + await page.setContent( + `
hi
` + ); + const 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} = getTestState(); + + const promise = page.waitForSelector('div', {hidden: true}); + await page.setContent(`
text
`); + const 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} = getTestState(); + + const promise = page.waitForSelector('div', {hidden: true}); + await page.setContent(`
text
`); + const 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} = getTestState(); + + const promise = page.waitForSelector('div', {hidden: true}); + await page.setContent('
text
'); + const 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} = getTestState(); + + const promise = page.waitForSelector('div', {hidden: true}); + await page.setContent(`
text
`); + const 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} = getTestState(); + + const handle = await page.waitForSelector('non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + it('should respect timeout', async () => { + const {page} = 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} = getTestState(); + + await page.setContent(`
text
`); + 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} = getTestState(); + + let divFound = false; + const waitForSelector = page.waitForSelector('.zombo').then(() => { + return (divFound = true); + }); + await page.setContent(`
`); + 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} = getTestState(); + + const waitForSelector = page.waitForSelector('.zombo'); + await page.setContent(`
anything
`); + expect( + await page.evaluate(x => { + return x?.textContent; + }, await waitForSelector) + ).toBe('anything'); + }); + it('should have correct stack trace for timeout', async () => { + const {page} = 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} = getTestState(); + + await page.setContent(`

red herring

hello world

`); + 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} = 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} = 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'); + const eHandle = await waitForXPathPromise; + expect(eHandle?.frame).toBe(frame2); + }); + it('should throw when frame is detached', async () => { + const {page, server} = 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} = getTestState(); + + let divHidden = false; + await page.setContent(`
text
`); + 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} = getTestState(); + + const 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} = getTestState(); + + await page.setContent(`
text
`); + + const waitForXPath = await page.waitForXPath('//div', {hidden: true}); + + expect(waitForXPath).toBeInstanceOf(ElementHandle); + }); + it('should return the element handle', async () => { + const {page} = getTestState(); + + const waitForXPath = page.waitForXPath('//*[@class="zombo"]'); + await page.setContent(`
anything
`); + expect( + await page.evaluate(x => { + return x?.textContent; + }, await waitForXPath) + ).toBe('anything'); + }); + it('should allow you to select a text node', async () => { + const {page} = getTestState(); + + await page.setContent(`
some text
`); + const 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} = getTestState(); + + await page.setContent(`
some text
`); + 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..733c5bdff1 --- /dev/null +++ b/remote/test/puppeteer/test/src/worker.spec.ts @@ -0,0 +1,123 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +import expect from 'expect'; +import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; +import {WebWorker} from 'puppeteer-core/internal/common/WebWorker.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Workers', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.workers', async () => { + const {page, server} = 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} = getTestState(); + + const workerCreatedPromise = waitEvent(page, 'workercreated'); + const workerObj = await page.evaluateHandle(() => { + return new Worker('data:text/javascript,1'); + }); + const worker = await workerCreatedPromise; + const 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} = 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} = getTestState(); + + const logPromise = waitEvent(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} = getTestState(); + + const workerCreatedPromise = waitEvent(page, 'workercreated'); + await page.evaluate(() => { + return new Worker(`data:text/javascript,console.log(1)`); + }); + const worker = await workerCreatedPromise; + expect(await (await worker.executionContext()).evaluate('1+1')).toBe(2); + }); + it('should report errors', async () => { + const {page} = getTestState(); + + const errorPromise = waitEvent(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..10fdfa2924 --- /dev/null +++ b/remote/test/puppeteer/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "NodeNext", + "outDir": "build", + "rootDir": "src" + }, + "include": ["src"], + "references": [{"path": "../tools/mochaRunner/tsconfig.json"}] +} diff --git a/remote/test/puppeteer/tools/analyze_issue.mjs b/remote/test/puppeteer/tools/analyze_issue.mjs new file mode 100755 index 0000000000..74e35ca3f8 --- /dev/null +++ b/remote/test/puppeteer/tools/analyze_issue.mjs @@ -0,0 +1,280 @@ +#!/usr/bin/env node +// @ts-check + +'use strict'; + +import {writeFile, mkdir, copyFile} from 'fs/promises'; +import {dirname, join} from 'path'; +import semver from 'semver'; +import {fileURLToPath} from 'url'; +import core from '@actions/core'; +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 = ''; + // @ts-expect-error: `iterator` is new and experimental. + 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; + 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..89afe0f9e7 --- /dev/null +++ b/remote/test/puppeteer/tools/chmod.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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/cp.ts b/remote/test/puppeteer/tools/cp.ts new file mode 100644 index 0000000000..438c6ccd54 --- /dev/null +++ b/remote/test/puppeteer/tools/cp.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2023 Google Inc. 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. + */ + +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/ensure-pinned-deps.ts b/remote/test/puppeteer/tools/ensure-pinned-deps.ts new file mode 100644 index 0000000000..82f53df796 --- /dev/null +++ b/remote/test/puppeteer/tools/ensure-pinned-deps.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2021 Google Inc. 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. + */ + +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(); + +for (const [depKey, depValue] of Object.entries(allDeps)) { + 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/generate-matrix.js b/remote/test/puppeteer/tools/generate-matrix.js new file mode 100644 index 0000000000..1cb65d3491 --- /dev/null +++ b/remote/test/puppeteer/tools/generate-matrix.js @@ -0,0 +1,43 @@ +const fs = require('fs'); + +const data = JSON.parse(fs.readFileSync('./test/TestSuites.json', 'utf-8')); + +/** + * @param {string} platform + * @returns {string} + */ +function mapPlatform(platform) { + switch (platform) { + case 'linux': + return 'ubuntu-latest'; + case 'win32': + return 'windows-latest'; + case 'darwin': + return 'macos-latest'; + default: + throw new Error('Unsupported platform'); + } +} + +const result = []; +for (const suite of data.testSuites) { + for (const platform of suite.platforms) { + if (platform === 'linux' && suite.id !== 'firefox-bidi') { + for (const node of [14, 16, 18]) { + result.push(`- name: ${suite.id} + machine: ${mapPlatform(platform)} + xvfb: true + node: ${node} + suite: ${suite.id}`); + } + } else { + result.push(`- name: ${suite.id} + machine: ${mapPlatform(platform)} + xvfb: ${platform === 'linux'} + node: 18 + suite: ${suite.id}`); + } + } +} + +console.log(result.join('\n')); diff --git a/remote/test/puppeteer/tools/generate_docs.ts b/remote/test/puppeteer/tools/generate_docs.ts new file mode 100644 index 0000000000..86bc92f5f8 --- /dev/null +++ b/remote/test/puppeteer/tools/generate_docs.ts @@ -0,0 +1,152 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {copyFile, readFile, rm, writeFile} from 'fs/promises'; +import {join, resolve} from 'path'; +import {chdir} from 'process'; + +import semver from 'semver'; + +import {generateDocs} from './internal/custom_markdown_action.js'; +import {job} from './internal/job.js'; +import {spawnAndLog} from './internal/util.js'; + +chdir(resolve(join(__dirname, '..'))); + +function getOffsetAndLimit( + sectionName: string, + lines: string[] +): [offset: number, limit: number] { + const offset = + lines.findIndex(line => { + return line.includes(``); + }) + 1; + const limit = lines.slice(offset).findIndex(line => { + return line.includes(``); + }); + return [offset, limit]; +} + +function spliceIntoSection( + sectionName: string, + content: string, + sectionContent: string +): string { + const lines = content.split('\n'); + const [offset, limit] = getOffsetAndLimit(sectionName, lines); + lines.splice(offset, limit, ...sectionContent.split('\n')); + return lines.join('\n'); +} + +(async () => { + const job1 = job('', async ({inputs, outputs}) => { + await copyFile(inputs[0]!, outputs[0]!); + }) + .inputs(['README.md']) + .outputs(['docs/index.md']) + .build(); + + // Chrome Versions + const job2 = job('', async ({inputs, outputs}) => { + let content = await readFile(inputs[2]!, {encoding: 'utf8'}); + const versionModulePath = join('..', inputs[0]!); + const {versionsPerRelease} = await import(versionModulePath); + const versionsArchived = JSON.parse(await readFile(inputs[1]!, 'utf8')); + + // Generate versions + const buffer: string[] = []; + for (const [chromiumVersion, puppeteerVersion] of versionsPerRelease) { + if (puppeteerVersion === 'NEXT') { + continue; + } + if (versionsArchived.includes(puppeteerVersion.substring(1))) { + if (semver.gte(puppeteerVersion, '20.0.0')) { + buffer.push( + ` * [Chrome for Testing](https://goo.gle/chrome-for-testing) ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://pptr.dev/${puppeteerVersion.slice( + 1 + )})` + ); + } else { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api/index.md)` + ); + } + } else if (semver.lt(puppeteerVersion, '15.0.0')) { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api.md)` + ); + } else if (semver.gte(puppeteerVersion, '15.3.0')) { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://pptr.dev/${puppeteerVersion.slice( + 1 + )})` + ); + } else { + buffer.push( + ` * Chromium ${chromiumVersion} - Puppeteer ${puppeteerVersion}` + ); + } + } + content = spliceIntoSection('version', content, buffer.join('\n')); + + await writeFile(outputs[0]!, content); + }) + .inputs([ + 'versions.js', + 'website/versionsArchived.json', + 'docs/chromium-support.md', + ]) + .outputs(['docs/chromium-support.md']) + .build(); + + await Promise.all([job1, job2]); + + // Generate documentation + const puppeteerDocs = job('', async ({inputs, outputs}) => { + await rm(outputs[0]!, {recursive: true, force: true}); + generateDocs(inputs[0]!, outputs[0]!); + spawnAndLog('prettier', '--ignore-path', 'none', '--write', 'docs'); + }) + .inputs([ + 'docs/puppeteer.api.json', + 'tools/internal/custom_markdown_documenter.ts', + ]) + .outputs(['docs/api']) + .build(); + + const browsersDocs = job('', async ({inputs, outputs}) => { + await rm(outputs[0]!, {recursive: true, force: true}); + generateDocs(inputs[0]!, outputs[0]!); + spawnAndLog('prettier', '--ignore-path', 'none', '--write', 'docs'); + }) + .inputs([ + 'docs/browsers.api.json', + 'tools/internal/custom_markdown_documenter.ts', + ]) + .outputs(['docs/browsers-api']) + .build(); + + await Promise.all([puppeteerDocs, browsersDocs]); + + await job('', async ({inputs, outputs}) => { + const readme = await readFile(inputs[1]!, 'utf-8'); + const index = await readFile(inputs[0]!, 'utf-8'); + await writeFile(outputs[0]!, index.replace('# API Reference\n', readme)); + }) + .inputs(['docs/browsers-api/index.md', 'packages/browsers/README.md']) + .outputs(['docs/browsers-api/index.md']) + .build(); +})(); 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..75737463d5 --- /dev/null +++ b/remote/test/puppeteer/tools/generate_module_package_json.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +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..c817df61bf --- /dev/null +++ b/remote/test/puppeteer/tools/get_deprecated_version_range.js @@ -0,0 +1,27 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +const { + versionsPerRelease, + lastMaintainedChromiumVersion, +} = require('../versions.js'); +const version = versionsPerRelease.get(lastMaintainedChromiumVersion); +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/internal/custom_markdown_action.ts b/remote/test/puppeteer/tools/internal/custom_markdown_action.ts new file mode 100644 index 0000000000..d8b3228736 --- /dev/null +++ b/remote/test/puppeteer/tools/internal/custom_markdown_action.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {ApiModel} from '@microsoft/api-extractor-model'; + +import {MarkdownDocumenter} from './custom_markdown_documenter.js'; + +export const generateDocs = (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(); +}; diff --git a/remote/test/puppeteer/tools/internal/custom_markdown_documenter.ts b/remote/test/puppeteer/tools/internal/custom_markdown_documenter.ts new file mode 100644 index 0000000000..366dde1d55 --- /dev/null +++ b/remote/test/puppeteer/tools/internal/custom_markdown_documenter.ts @@ -0,0 +1,1481 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +// 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 {DocumenterConfig} from '@microsoft/api-documenter/lib/documenters/DocumenterConfig'; +import {CustomMarkdownEmitter} from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; +import {CustomDocNodes} from '@microsoft/api-documenter/lib/nodes/CustomDocNodeKind'; +import {DocEmphasisSpan} from '@microsoft/api-documenter/lib/nodes/DocEmphasisSpan'; +import {DocHeading} from '@microsoft/api-documenter/lib/nodes/DocHeading'; +import {DocNoteBox} from '@microsoft/api-documenter/lib/nodes/DocNoteBox'; +import {DocTable} from '@microsoft/api-documenter/lib/nodes/DocTable'; +import {DocTableCell} from '@microsoft/api-documenter/lib/nodes/DocTableCell'; +import {DocTableRow} from '@microsoft/api-documenter/lib/nodes/DocTableRow'; +import {MarkdownDocumenterAccessor} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterAccessor'; +import { + IMarkdownDocumenterFeatureOnBeforeWritePageArgs, + MarkdownDocumenterFeatureContext, +} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterFeature'; +import {PluginLoader} from '@microsoft/api-documenter/lib/plugin/PluginLoader'; +import {Utilities} from '@microsoft/api-documenter/lib/utils/Utilities'; +import { + ApiClass, + ApiDeclaredItem, + ApiDocumentedItem, + ApiEnum, + ApiInitializerMixin, + ApiInterface, + ApiItem, + ApiItemKind, + ApiModel, + ApiNamespace, + ApiOptionalMixin, + ApiPackage, + ApiParameterListMixin, + ApiPropertyItem, + ApiProtectedMixin, + ApiReadonlyMixin, + ApiReleaseTagMixin, + ApiReturnTypeMixin, + ApiStaticMixin, + ApiTypeAlias, + Excerpt, + ExcerptToken, + ExcerptTokenKind, + IResolveDeclarationReferenceResult, + ReleaseTag, +} from '@microsoft/api-extractor-model'; +import { + DocBlock, + DocCodeSpan, + DocComment, + DocFencedCode, + DocLinkTag, + DocNodeContainer, + DocNodeKind, + DocParagraph, + DocPlainText, + DocSection, + StandardTags, + StringBuilder, + TSDocConfiguration, +} from '@microsoft/tsdoc'; +import { + FileSystem, + NewlineKind, + PackageName, +} from '@rushstack/node-core-library'; + +export interface IMarkdownDocumenterOptions { + apiModel: ApiModel; + documenterConfig: DocumenterConfig | undefined; + outputFolder: string; +} + +/** + * 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) => { + console.log(apiItem); + return this._getLinkFilenameForApiItem(apiItem); + }, + }), + }); + }); + } + + console.log(); + 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>/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: Set = new Set(); + 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/internal/job.ts b/remote/test/puppeteer/tools/internal/job.ts new file mode 100644 index 0000000000..ef6ea10237 --- /dev/null +++ b/remote/test/puppeteer/tools/internal/job.ts @@ -0,0 +1,153 @@ +import {createHash} from 'crypto'; +import {existsSync, Stats} from 'fs'; +import {mkdir, readFile, stat, writeFile} from 'fs/promises'; +import {tmpdir} from 'os'; +import {dirname, join} from 'path'; + +import glob from 'glob'; + +interface JobContext { + name: string; + inputs: string[]; + outputs: string[]; +} + +class JobBuilder { + #inputs: string[] = []; + #outputs: string[] = []; + #callback: (ctx: JobContext) => Promise; + #name: string; + #value = ''; + #force = false; + + constructor(name: string, callback: (ctx: JobContext) => Promise) { + this.#name = name; + this.#callback = callback; + } + + get jobHash(): string { + return createHash('sha256').update(this.#name).digest('hex'); + } + + force() { + this.#force = true; + return this; + } + + value(value: string) { + this.#value = value; + return this; + } + + inputs(inputs: string[]): JobBuilder { + this.#inputs = inputs.flatMap(value => { + if (glob.hasMagic(value)) { + return glob.sync(value); + } + return value; + }); + return this; + } + + outputs(outputs: string[]): JobBuilder { + if (!this.#name) { + this.#name = outputs.join(' and '); + } + + this.#outputs = outputs; + return this; + } + + async build(): Promise { + console.log(`Running job ${this.#name}...`); + // For debugging. + if (this.#force) { + return this.#run(); + } + // In case we deleted an output file on purpose. + if (!this.getOutputStats()) { + return this.#run(); + } + // Run if the job has a value, but it changes. + if (this.#value) { + if (!(await this.isValueDifferent())) { + return; + } + return this.#run(); + } + // Always run when there is no output. + if (!this.#outputs.length) { + return this.#run(); + } + // Make-like comparator. + if (!(await this.areInputsNewer())) { + return; + } + return this.#run(); + } + + async isValueDifferent(): Promise { + const file = join(tmpdir(), `puppeteer/${this.jobHash}.txt`); + await mkdir(dirname(file), {recursive: true}); + if (!existsSync(file)) { + await writeFile(file, this.#value); + return true; + } + return this.#value !== (await readFile(file, 'utf8')); + } + + #outputStats?: Stats[]; + async getOutputStats(): Promise { + if (this.#outputStats) { + return this.#outputStats; + } + try { + this.#outputStats = await Promise.all( + this.#outputs.map(output => { + return stat(output); + }) + ); + } catch {} + return this.#outputStats; + } + + async areInputsNewer(): Promise { + const inputStats = await Promise.all( + this.#inputs.map(input => { + return stat(input); + }) + ); + const outputStats = await this.getOutputStats(); + if ( + outputStats && + outputStats.reduce(reduceMinTime, Infinity) > + inputStats.reduce(reduceMaxTime, 0) + ) { + return false; + } + return true; + } + + #run(): Promise { + return this.#callback({ + name: this.#name, + inputs: this.#inputs, + outputs: this.#outputs, + }); + } +} + +export const job = ( + name: string, + callback: (ctx: JobContext) => Promise +): JobBuilder => { + return new JobBuilder(name, callback); +}; + +const reduceMaxTime = (time: number, stat: Stats) => { + return time < stat.mtimeMs ? stat.mtimeMs : time; +}; + +const reduceMinTime = (time: number, stat: Stats) => { + return time > stat.mtimeMs ? stat.mtimeMs : time; +}; diff --git a/remote/test/puppeteer/tools/internal/util.ts b/remote/test/puppeteer/tools/internal/util.ts new file mode 100644 index 0000000000..4ebbe8b86b --- /dev/null +++ b/remote/test/puppeteer/tools/internal/util.ts @@ -0,0 +1,14 @@ +import {spawnSync} from 'child_process'; + +export const spawnAndLog = (...args: string[]): void => { + const {stdout, stderr} = spawnSync(args[0]!, args.slice(1), { + encoding: 'utf-8', + shell: true, + }); + if (stdout) { + console.log(stdout); + } + if (stderr) { + console.error(stderr); + } +}; diff --git a/remote/test/puppeteer/tools/mochaRunner/README.md b/remote/test/puppeteer/tools/mochaRunner/README.md new file mode 100644 index 0000000000..1e4398a63c --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/README.md @@ -0,0 +1,73 @@ +# 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 run build && npx c8 node tools/mochaRunner/lib/test.js +``` + +## 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] *` | 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`,
`[page.spec] Page Page.goto should work with anchor navigation` | +| `[test.spec] * ` | Matches test with a surfix | `[navigation.spec] * should work` | `[navigation.spec] navigation Page.goto should work`,
`[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. diff --git a/remote/test/puppeteer/tools/mochaRunner/src/interface.ts b/remote/test/puppeteer/tools/mochaRunner/src/interface.ts new file mode 100644 index 0000000000..79329fcb0d --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/interface.ts @@ -0,0 +1,130 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import Mocha from 'mocha'; +import commonInterface from 'mocha/lib/interfaces/common'; + +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']) + : []; + +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 customBDDInterface(suite: Mocha.Suite) { + const suites = [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, + }); + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + context['describe'] = describe; + + function it(title: string, fn: Mocha.TestFunction, itOnly = false) { + const suite = suites[0]!; + 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-ignore + suite.parent?._onlySuites.find(child => { + return child === suite; + }) + ); + + if (shouldSkipTest(test) && !(itOnly || describeOnly)) { + 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 typeof it)(title, fn, true) + ); + }; + + it.skip = function (title: string) { + return context['it'](title); + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + context.it = it; + } + ); +} + +customBDDInterface.description = 'Custom BDD'; + +module.exports = customBDDInterface; diff --git a/remote/test/puppeteer/tools/mochaRunner/src/main.ts b/remote/test/puppeteer/tools/mochaRunner/src/main.ts new file mode 100644 index 0000000000..d2547e721c --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/main.ts @@ -0,0 +1,259 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {randomUUID} from 'crypto'; +import fs from 'fs'; +import {spawn, SpawnOptions} from 'node:child_process'; +import os from 'os'; +import path from 'path'; + +import { + TestExpectation, + MochaResults, + zTestSuiteFile, + zPlatform, + TestSuite, + TestSuiteFile, + Platform, +} from './types.js'; +import { + extendProcessEnv, + filterByPlatform, + readJSON, + filterByParameters, + getExpectationUpdates, + printSuggestions, + RecommendedExpectation, + writeJSON, +} from './utils.js'; + +function getApplicableTestSuites( + parsedSuitesFile: TestSuiteFile, + platform: Platform +): TestSuite[] { + const testSuiteArgIdx = process.argv.indexOf('--test-suite'); + let applicableSuites: TestSuite[] = []; + + if (testSuiteArgIdx === -1) { + applicableSuites = filterByPlatform(parsedSuitesFile.testSuites, platform); + } else { + const testSuiteId = process.argv[testSuiteArgIdx + 1]; + 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() { + const noCoverage = process.argv.indexOf('--no-coverage') !== -1; + const noSuggestions = process.argv.indexOf('--no-suggestions') !== -1; + + const statsFilenameIdx = process.argv.indexOf('--save-stats-to'); + let statsFilename = ''; + if (statsFilenameIdx !== -1) { + statsFilename = process.argv[statsFilenameIdx + 1] as string; + if (statsFilename.includes('INSERTID')) { + statsFilename = statsFilename.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 (statsFilename) { + console.log('Test stats will be saved to', statsFilename); + } + + 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({ + 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 = statsFilename + ? statsFilename + : path.join(tmpDir, 'output.json'); + console.log('Running', JSON.stringify(parameters), tmpFilename); + const reporterArgumentIndex = process.argv.indexOf('--reporter'); + const args = [ + '-u', + path.join(__dirname, 'interface.js'), + '-R', + reporterArgumentIndex === -1 + ? path.join(__dirname, 'reporter.js') + : process.argv[reporterArgumentIndex + 1] || '', + '-O', + 'output=' + tmpFilename, + ]; + const retriesArgumentIndex = process.argv.indexOf('--retries'); + const timeoutArgumentIndex = process.argv.indexOf('--timeout'); + if (retriesArgumentIndex > -1) { + args.push('--retries', process.argv[retriesArgumentIndex + 1] || ''); + } + if (timeoutArgumentIndex > -1) { + args.push('--timeout', process.argv[timeoutArgumentIndex + 1] || ''); + } + if (process.argv.indexOf('--no-parallel')) { + args.push('--no-parallel'); + } + if (process.argv.indexOf('--fullTrace')) { + args.push('--fullTrace'); + } + const spawnArgs: SpawnOptions = { + shell: true, + cwd: process.cwd(), + stdio: 'inherit', + env, + }; + const handle = noCoverage + ? spawn('npx', ['mocha', ...args], spawnArgs) + : spawn( + 'npx', + [ + 'c8', + '--check-coverage', + '--lines', + String(suite.expectedLineCoverage), + 'npx mocha', + ...args, + ], + spawnArgs + ); + await new Promise((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, + }); + 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 { + 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 (!noSuggestions) { + 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/mochaRunner/src/reporter.ts b/remote/test/puppeteer/tools/mochaRunner/src/reporter.ts new file mode 100644 index 0000000000..37ca586215 --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/reporter.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +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/mochaRunner/src/test.ts b/remote/test/puppeteer/tools/mochaRunner/src/test.ts new file mode 100644 index 0000000000..1e0328499c --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/test.ts @@ -0,0 +1,134 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ +import assert from 'node:assert/strict'; +import {describe, test} from 'node:test'; + +import {TestExpectation} from './types.js'; +import { + filterByParameters, + getTestResultForFailure, + isWildCardPattern, + testIdMatchesExpectationPattern, +} from './utils.js'; +import {getFilename, extendProcessEnv} from './utils.js'; + +void test('extendProcessEnv', () => { + const env = extendProcessEnv([{TEST: 'TEST'}, {TEST2: 'TEST2'}]); + assert.equal(env['TEST'], 'TEST'); + assert.equal(env['TEST2'], 'TEST2'); +}); + +void test('getFilename', () => { + assert.equal(getFilename('/etc/test.ts'), 'test'); + assert.equal(getFilename('/etc/test.js'), 'test'); +}); + +void test('getTestResultForFailure', () => { + assert.equal( + getTestResultForFailure({err: {code: 'ERR_MOCHA_TIMEOUT'}}), + 'TIMEOUT' + ); + assert.equal(getTestResultForFailure({err: {code: 'ERROR'}}), 'FAIL'); +}); + +void test('filterByParameters', () => { + 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); +}); + +void test('isWildCardPattern', () => { + 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], + ]; + + void test('with MochaTest', () => { + const test = { + title: 'should work', + file: 'page.spec.ts', + fullTitle() { + return 'Page Page.setContent should work'; + }, + } as any; + + for (const [pattern, expected] of expectations) { + assert.equal( + testIdMatchesExpectationPattern(test, pattern), + expected, + `Expected "${pattern}" to yield "${expected}"` + ); + } + }); + void test('with MochaTestResult', () => { + const test = { + title: 'should work', + file: 'page.spec.ts', + fullTitle: 'Page Page.setContent should work', + } as any; + + for (const [pattern, expected] of expectations) { + assert.equal( + testIdMatchesExpectationPattern(test, pattern), + expected, + `Expected "${pattern}" to yield "${expected}"` + ); + } + }); +}); diff --git a/remote/test/puppeteer/tools/mochaRunner/src/types.ts b/remote/test/puppeteer/tools/mochaRunner/src/types.ts new file mode 100644 index 0000000000..8d8a08ee98 --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/types.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import {z} from 'zod'; + +import {RecommendedExpectation} from './utils.js'; + +export const zPlatform = z.enum(['win32', 'linux', 'darwin']); + +export type Platform = z.infer; + +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; + +export const zTestSuiteFile = z.object({ + testSuites: z.array(zTestSuite), + parameterDefinitions: z.record(z.any()), +}); + +export type TestSuiteFile = z.infer; + +export type TestResult = 'PASS' | 'FAIL' | 'TIMEOUT' | 'SKIP'; + +export type TestExpectation = { + testIdPattern: string; + platforms: NodeJS.Platform[]; + parameters: string[]; + expectations: TestResult[]; +}; + +export type MochaTestResult = { + fullTitle: string; + title: string; + file: string; + err?: {code: string}; +}; + +export type MochaResults = { + stats: unknown; + pending: MochaTestResult[]; + passes: MochaTestResult[]; + failures: MochaTestResult[]; + // Added by mochaRunner. + updates?: RecommendedExpectation[]; + parameters?: string[]; + platform?: string; + date?: string; +}; diff --git a/remote/test/puppeteer/tools/mochaRunner/src/utils.ts b/remote/test/puppeteer/tools/mochaRunner/src/utils.ts new file mode 100644 index 0000000000..9fdbf65583 --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/utils.ts @@ -0,0 +1,265 @@ +/** + * Copyright 2022 Google Inc. 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 + * + * 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. + */ + +import fs from 'fs'; +import path from 'path'; + +import { + MochaTestResult, + TestExpectation, + MochaResults, + TestResult, +} from './types.js'; + +export function extendProcessEnv(envs: object[]): NodeJS.ProcessEnv { + return envs.reduce( + (acc: object, item: object) => { + Object.assign(acc, item); + return acc; + }, + { + ...process.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( + 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; + }) + ); + console.log( + 'The recommendations are based on the following applied expectaions:' + ); + 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 type 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: Map = new Map(); + + for (const pass of results.passes) { + // If an error occurs during a hook + // the error not have a file associated with it + if (!pass.file) { + continue; + } + + 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) { + 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 +): 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 | Mocha.Test, + 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/mochaRunner/tsconfig.json b/remote/test/puppeteer/tools/mochaRunner/tsconfig.json new file mode 100644 index 0000000000..c2576c2564 --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "composite": true, + "module": "CommonJS", + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/remote/test/puppeteer/tools/remove_version_suffix.js b/remote/test/puppeteer/tools/remove_version_suffix.js new file mode 100644 index 0000000000..091a35ec9b --- /dev/null +++ b/remote/test/puppeteer/tools/remove_version_suffix.js @@ -0,0 +1,26 @@ +/** + * Copyright 2020 Google Inc. 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 + * + * 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. + */ + +const fs = require('fs'); +const json = fs.readFileSync('./package.json', 'utf8').toString(); +const pkg = JSON.parse(json); +const oldVersion = pkg.version; +const version = oldVersion.replace(/-post$/, ''); +const updated = json.replace( + `"version": "${oldVersion}"`, + `"version": "${version}"` +); +fs.writeFileSync('./package.json', updated); diff --git a/remote/test/puppeteer/tools/sort-test-expectations.js b/remote/test/puppeteer/tools/sort-test-expectations.js new file mode 100644 index 0000000000..96e32145e4 --- /dev/null +++ b/remote/test/puppeteer/tools/sort-test-expectations.js @@ -0,0 +1,59 @@ +/** + * Copyright 2023 Google Inc. 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 + * + * 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. + */ + +// TODO: this could be an eslint rule probably. + +const fs = require('fs'); +const path = require('path'); + +const prettier = require('prettier'); + +const source = 'test/TestExpectations.json'; + +const testExpectations = JSON.parse(fs.readFileSync(source, 'utf-8')); + +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(); +}); + +fs.writeFileSync( + source, + prettier.format(JSON.stringify(testExpectations), { + ...require(path.join(__dirname, '..', '.prettierrc.cjs')), + 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..4d1c05497b --- /dev/null +++ b/remote/test/puppeteer/tools/third_party/validate-licenses.ts @@ -0,0 +1,153 @@ +// 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', + + '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 { + 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..393392c494 --- /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/tsconfig.base.json b/remote/test/puppeteer/tsconfig.base.json new file mode 100644 index 0000000000..9c29d30d9a --- /dev/null +++ b/remote/test/puppeteer/tsconfig.base.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "allowJs": true, + "alwaysStrict": true, + "checkJs": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "incremental": true, + "module": "ESNext", + "moduleResolution": "node", + "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": "ES2019", + "useUnknownInCatchVariables": true + } +} diff --git a/remote/test/puppeteer/versions.js b/remote/test/puppeteer/versions.js new file mode 100644 index 0000000000..63be16fb3e --- /dev/null +++ b/remote/test/puppeteer/versions.js @@ -0,0 +1,73 @@ +/** + * Copyright 2020 Google Inc. 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. + */ + +const versionsPerRelease = new Map([ + // This is a mapping from Chrome version => Puppeteer version. + // In Chrome roll patches, use `NEXT` for the Puppeteer version. + ['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 = '109.0.5412.0'; + +if (!versionsPerRelease.has(lastMaintainedChromeVersion)) { + throw new Error( + 'lastMaintainedChromeVersion is missing from versionsPerRelease' + ); +} + +module.exports = { + versionsPerRelease, + lastMaintainedChromeVersion, +}; -- cgit v1.2.3