From 81b4e21aabf2a1a8fd823d964bd68379306c71d2 Mon Sep 17 00:00:00 2001 From: tasneemkoushar Date: Sat, 29 May 2021 20:48:58 +0530 Subject: [PATCH 1/3] fix: added --- .editorconfig | 9 + .eslintignore | 15 + .eslintrc.js | 185 + .gitattributes | 2 + .gitignore | 13 +- .npmrc | 1 + .prettierignore | 8 + .versionrc.js | 26 + CHANGELOG.md | 171 + LICENSE | 4 +- README.md | 470 +- api-extractor.json | 45 + build.js | 190 - cjs-entry-core.js | 29 + cjs-entry.js | 29 + commitlint.config.js | 22 + docs/CNAME | 1 - docs/api.md | 4767 +++++++++++++++++ docs/assets/overview.png | Bin 0 -> 43504 bytes docs/assets/overview.svg | 1 + docs/favicons/android-chrome-192x192.png | Bin 3484 -> 0 bytes docs/favicons/android-chrome-384x384.png | Bin 8412 -> 0 bytes docs/favicons/apple-touch-icon.png | Bin 2696 -> 0 bytes docs/favicons/browserconfig.xml | 9 - docs/favicons/favicon-16x16.png | Bin 661 -> 0 bytes docs/favicons/favicon-32x32.png | Bin 1034 -> 0 bytes docs/favicons/favicon.ico | Bin 15086 -> 0 bytes docs/favicons/mstile-150x150.png | Bin 2533 -> 0 bytes docs/favicons/safari-pinned-tab.svg | 1 - docs/favicons/site.webmanifest | 20 - docs/images/checkmark.svg | 4 - docs/images/close.svg | 4 - docs/images/cog.svg | 4 - docs/images/github.png | Bin 1571 -> 0 bytes docs/images/home.svg | 4 - docs/images/menu.svg | 4 - docs/images/pptr.png | Bin 14594 -> 0 bytes docs/images/search.svg | 4 - docs/images/slack.svg | 31 - docs/images/stackoverflow.svg | 1 - docs/images/wrench.svg | 4 - docs/index.html | 26 - docs/index.js | 194 - docs/issue_template.md | 40 + docs/style.css | 3 - docs/sw.js | 128 - docs/troubleshooting.md | 453 ++ examples/README.md | 39 + examples/block-images.js | 33 + examples/cross-browser.js | 48 + examples/custom-event.js | 50 + examples/detect-sniff.js | 44 + examples/oopif.js | 47 + examples/pdf.js | 35 + examples/proxy.js | 35 + examples/screenshot-fullpage.js | 28 + examples/screenshot.js | 27 + examples/search.js | 55 + .../.ci/node10/Dockerfile.linux | 17 + .../.ci/node10/Dockerfile.windows | 11 + experimental/puppeteer-firefox/.cirrus.yml | 31 + experimental/puppeteer-firefox/.gitignore | 10 + experimental/puppeteer-firefox/.npmignore | 36 + .../puppeteer-firefox/DeviceDescriptors.js | 17 + experimental/puppeteer-firefox/Errors.js | 1 + experimental/puppeteer-firefox/LICENSE | 202 + experimental/puppeteer-firefox/README.md | 48 + .../puppeteer-firefox/examples/screenshot.js | 27 + .../puppeteer-firefox/examples/search.js | 55 + experimental/puppeteer-firefox/index.js | 25 + experimental/puppeteer-firefox/install.js | 97 + .../puppeteer-firefox/lib/Accessibility.js | 322 ++ experimental/puppeteer-firefox/lib/Browser.js | 369 ++ .../puppeteer-firefox/lib/BrowserFetcher.js | 342 ++ .../puppeteer-firefox/lib/Connection.js | 242 + .../puppeteer-firefox/lib/DOMWorld.js | 625 +++ .../lib/DeviceDescriptors.js | 824 +++ experimental/puppeteer-firefox/lib/Dialog.js | 57 + experimental/puppeteer-firefox/lib/Errors.js | 29 + experimental/puppeteer-firefox/lib/Events.js | 53 + .../puppeteer-firefox/lib/ExecutionContext.js | 103 + .../puppeteer-firefox/lib/FrameManager.js | 478 ++ experimental/puppeteer-firefox/lib/Input.js | 340 ++ .../puppeteer-firefox/lib/JSHandle.js | 435 ++ .../puppeteer-firefox/lib/Launcher.js | 298 ++ .../lib/NavigationWatchdog.js | 119 + .../puppeteer-firefox/lib/NetworkManager.js | 356 ++ experimental/puppeteer-firefox/lib/Page.js | 813 +++ .../puppeteer-firefox/lib/Puppeteer.js | 67 + .../puppeteer-firefox/lib/TimeoutSettings.js | 60 + .../puppeteer-firefox/lib/USKeyboardLayout.js | 281 + .../lib/WebSocketTransport.js | 102 + experimental/puppeteer-firefox/lib/api.js | 22 + .../puppeteer-firefox/lib/externs.d.ts | 28 + experimental/puppeteer-firefox/lib/helper.js | 194 + .../misc/00-puppeteer-prefs.js | 3 + .../misc/install-preferences.js | 59 + .../puppeteer-firefox/misc/puppeteer.cfg | 212 + experimental/puppeteer-firefox/package.json | 30 + experimental/puppeteer-firefox/tsconfig.json | 11 + install.js | 89 + mocha-config/base.js | 6 + mocha-config/coverage-tests.js | 6 + mocha-config/doclint-tests.js | 6 + mocha-config/puppeteer-unit-tests.js | 32 + my-website/.gitignore | 20 + my-website/README.md | 33 + my-website/babel.config.js | 3 + my-website/docs/index.md | 12 + my-website/docs/intro.md | 35 + my-website/docs/new-doc/index.md | 12 + .../puppeteer.browser.browsercontexts.md | 17 + .../docs/new-doc/puppeteer.browser.close.md | 17 + ...r.browser.createincognitobrowsercontext.md | 33 + ...puppeteer.browser.defaultbrowsercontext.md | 17 + .../new-doc/puppeteer.browser.disconnect.md | 17 + .../new-doc/puppeteer.browser.isconnected.md | 17 + my-website/docusaurus.config.js | 102 + my-website/package.json | 39 + my-website/sidebars.js | 26 + my-website/src/components/HomepageFeatures.js | 64 + .../components/HomepageFeatures.module.css | 13 + my-website/src/css/custom.css | 25 + my-website/src/pages/index.js | 40 + my-website/src/pages/index.module.css | 25 + my-website/src/pages/markdown-page.md | 7 + my-website/static/.nojekyll | 0 my-website/static/img/docusaurus.png | Bin 0 -> 5142 bytes my-website/static/img/favicon.ico | Bin 0 -> 3626 bytes my-website/static/img/logo.svg | 1 + .../img/tutorial/docsVersionDropdown.png | Bin 0 -> 25102 bytes .../static/img/tutorial/localeDropdown.png | Bin 0 -> 30020 bytes .../static/img/undraw_docusaurus_mountain.svg | 170 + .../static/img/undraw_docusaurus_react.svg | 169 + .../static/img/undraw_docusaurus_tree.svg | 1 + package.json | 132 +- prettier.config.js | 5 + ...nsure-correct-devtools-protocol-package.ts | 84 + scripts/ensure-pinned-deps.ts | 37 + scripts/test-install.sh | 41 + scripts/test-ts-definition-files.ts | 208 + scripts/tsconfig.json | 6 + src/.eslintrc.js | 19 + src/api-docs-entry.ts | 151 + src/common/Accessibility.ts | 502 ++ src/common/AriaQueryHandler.ts | 139 + src/common/Browser.ts | 782 +++ src/common/BrowserConnector.ts | 138 + src/common/BrowserWebSocketTransport.ts | 55 + src/common/Connection.ts | 360 ++ src/common/ConnectionTransport.ts | 25 + src/common/ConsoleMessage.ts | 123 + src/common/Coverage.ts | 435 ++ src/common/DOMWorld.ts | 945 ++++ src/common/Debug.ts | 83 + src/common/DeviceDescriptors.ts | 1048 ++++ src/common/Dialog.ts | 112 + src/common/EmulationManager.ts | 59 + src/common/Errors.ts | 48 + src/common/EvalTypes.ts | 81 + src/common/EventEmitter.ts | 149 + src/common/Events.ts | 97 + src/common/ExecutionContext.ts | 392 ++ src/common/FileChooser.ts | 86 + src/common/FrameManager.ts | 1312 +++++ src/common/HTTPRequest.ts | 541 ++ src/common/HTTPResponse.ts | 213 + src/common/Input.ts | 524 ++ src/common/JSHandle.ts | 989 ++++ src/common/LifecycleWatcher.ts | 250 + src/common/NetworkConditions.ts | 38 + src/common/NetworkManager.ts | 459 ++ src/common/PDFOptions.ts | 184 + src/common/Page.ts | 2344 ++++++++ src/common/Product.ts | 21 + src/common/Puppeteer.ts | 200 + src/common/PuppeteerViewport.ts | 51 + src/common/QueryHandler.ts | 239 + src/common/SecurityDetails.ts | 88 + src/common/Target.ts | 233 + src/common/TimeoutSettings.ts | 50 + src/common/Tracing.ts | 118 + src/common/USKeyboardLayout.ts | 681 +++ src/common/WebWorker.ts | 172 + src/common/assert.ts | 24 + src/common/fetch.ts | 22 + src/common/helper.ts | 392 ++ src/environment.ts | 17 + src/favicons/android-chrome-192x192.png | Bin 3484 -> 0 bytes src/favicons/android-chrome-384x384.png | Bin 8412 -> 0 bytes src/favicons/apple-touch-icon.png | Bin 2696 -> 0 bytes src/favicons/browserconfig.xml | 9 - src/favicons/favicon-16x16.png | Bin 661 -> 0 bytes src/favicons/favicon-32x32.png | Bin 1034 -> 0 bytes src/favicons/favicon.ico | Bin 15086 -> 0 bytes src/favicons/mstile-150x150.png | Bin 2533 -> 0 bytes src/favicons/safari-pinned-tab.svg | 1 - src/favicons/site.webmanifest | 20 - src/images/checkmark.svg | 4 - src/images/close.svg | 4 - src/images/cog.svg | 4 - src/images/github.png | Bin 1571 -> 0 bytes src/images/home.svg | 4 - src/images/menu.svg | 4 - src/images/pptr.png | Bin 14594 -> 0 bytes src/images/search.svg | 4 - src/images/slack.svg | 31 - src/images/stackoverflow.svg | 1 - src/images/wrench.svg | 4 - src/index.html | 40 - src/index.js | 21 - src/initialize-node.ts | 43 + src/initialize-web.ts | 24 + src/node-puppeteer-core.ts | 25 + src/node.ts | 23 + src/node/BrowserFetcher.ts | 612 +++ src/node/BrowserRunner.ts | 255 + src/node/LaunchOptions.ts | 128 + src/node/Launcher.ts | 680 +++ src/node/NodeWebSocketTransport.ts | 59 + src/node/PipeTransport.ts | 80 + src/node/Puppeteer.ts | 229 + src/node/install.ts | 187 + src/pptr/APIDocumentation.js | 528 -- src/pptr/PPTRProduct.js | 640 --- src/pptr/style.css | 353 -- src/revisions.ts | 25 + src/sw-template.js | 31 - src/third_party/CodeMirror.LICENSE | 19 - src/third_party/commonmark.min.js | 1 - src/third_party/commonmarkjs.LICENSE | 115 - src/third_party/idb-keyval.LICENSE | 14 - src/third_party/idb-keyval.mjs | 64 - src/third_party/javascript.js | 874 --- src/third_party/runmode-standalone.js | 158 - src/tsconfig.cjs.json | 11 + src/tsconfig.esm.json | 9 + src/ui/App.js | 253 - src/ui/ContentComponent.js | 32 - src/ui/EventEmitter.js | 74 - src/ui/FuzzySearch.js | 142 - src/ui/SearchComponent.js | 272 - src/ui/SettingsComponent.js | 128 - src/ui/SidebarComponent.js | 79 - src/ui/ToolbarComponent.js | 41 - src/ui/content-component.css | 22 - src/ui/html.js | 154 - src/ui/main.css | 225 - src/ui/polyfills.js | 43 - src/ui/search-component.css | 111 - src/ui/settings-component.css | 125 - src/ui/sidebar-component.css | 58 - src/ui/toolbar-component.css | 68 - src/web.ts | 24 + test-browser/connection.spec.js | 55 + test-browser/debug.spec.js | 65 + test-browser/helper.js | 27 + test-ts-types/js-cjs-import-cjs-output/bad.js | 18 + .../js-cjs-import-cjs-output/good.js | 17 + .../js-cjs-import-cjs-output/package.json | 12 + .../js-cjs-import-cjs-output/tsconfig.json | 11 + test-ts-types/js-cjs-import-esm-output/bad.js | 18 + .../js-cjs-import-esm-output/good.js | 17 + .../js-cjs-import-esm-output/package.json | 15 + .../js-cjs-import-esm-output/tsconfig.json | 11 + test-ts-types/js-esm-import-cjs-output/bad.js | 18 + .../js-esm-import-cjs-output/good.js | 17 + .../js-esm-import-cjs-output/package.json | 15 + .../js-esm-import-cjs-output/tsconfig.json | 11 + test-ts-types/js-esm-import-esm-output/bad.js | 18 + .../js-esm-import-esm-output/good.js | 17 + .../js-esm-import-esm-output/package.json | 15 + .../js-esm-import-esm-output/tsconfig.json | 11 + test-ts-types/ts-cjs-import-cjs-output/bad.ts | 17 + .../ts-cjs-import-cjs-output/good.ts | 16 + .../ts-cjs-import-cjs-output/package.json | 15 + .../ts-cjs-import-cjs-output/tsconfig.json | 9 + test-ts-types/ts-esm-import-cjs-output/bad.ts | 18 + .../ts-esm-import-cjs-output/good.ts | 13 + .../ts-esm-import-cjs-output/package.json | 15 + .../ts-esm-import-cjs-output/tsconfig.json | 9 + test-ts-types/ts-esm-import-esm-output/bad.ts | 18 + .../ts-esm-import-esm-output/good.ts | 13 + .../ts-esm-import-esm-output/package.json | 15 + .../ts-esm-import-esm-output/tsconfig.json | 9 + test/.eslintrc.js | 13 + test/CDPSession.spec.ts | 106 + test/EventEmitter.spec.ts | 170 + test/README.md | 87 + test/accessibility.spec.ts | 520 ++ test/ariaqueryhandler.spec.ts | 594 ++ test/assert-coverage-test.js | 25 + test/assets/beforeunload.html | 10 + test/assets/cached/one-style-font.css | 9 + test/assets/cached/one-style-font.html | 2 + test/assets/cached/one-style.css | 3 + test/assets/cached/one-style.html | 2 + test/assets/chromium-linux.zip | Bin 0 -> 325 bytes test/assets/consolelog.html | 17 + test/assets/csp.html | 1 + test/assets/csscoverage/Dosis-Regular.ttf | Bin 0 -> 136940 bytes test/assets/csscoverage/OFL.txt | 95 + test/assets/csscoverage/involved.html | 26 + test/assets/csscoverage/media.html | 4 + test/assets/csscoverage/multiple.html | 8 + test/assets/csscoverage/simple.html | 6 + test/assets/csscoverage/sourceurl.html | 7 + test/assets/csscoverage/stylesheet1.css | 3 + test/assets/csscoverage/stylesheet2.css | 4 + test/assets/csscoverage/unused.html | 7 + test/assets/detect-touch.html | 12 + test/assets/digits/0.png | Bin 0 -> 434 bytes test/assets/digits/1.png | Bin 0 -> 346 bytes test/assets/digits/2.png | Bin 0 -> 413 bytes test/assets/digits/3.png | Bin 0 -> 434 bytes test/assets/digits/4.png | Bin 0 -> 403 bytes test/assets/digits/5.png | Bin 0 -> 422 bytes test/assets/digits/6.png | Bin 0 -> 445 bytes test/assets/digits/7.png | Bin 0 -> 387 bytes test/assets/digits/8.png | Bin 0 -> 447 bytes test/assets/digits/9.png | Bin 0 -> 437 bytes test/assets/dynamic-oopif.html | 10 + test/assets/empty.html | 0 test/assets/error.html | 15 + test/assets/es6/.eslintrc | 5 + test/assets/es6/es6import.js | 2 + test/assets/es6/es6module.js | 1 + test/assets/es6/es6pathimport.js | 2 + test/assets/favicon.ico | Bin 0 -> 70 bytes test/assets/file-to-upload.txt | 1 + .../firefox-75.0a1.en-US.linux-x86_64.tar.bz2 | Bin 0 -> 211 bytes test/assets/frames/frame.html | 8 + test/assets/frames/frameset.html | 8 + test/assets/frames/nested-frames.html | 25 + .../assets/frames/one-frame-url-fragment.html | 1 + test/assets/frames/one-frame.html | 1 + test/assets/frames/script.js | 1 + test/assets/frames/style.css | 3 + test/assets/frames/two-frames.html | 13 + test/assets/global-var.html | 3 + test/assets/grid.html | 52 + test/assets/historyapi.html | 5 + test/assets/idle-detector.html | 23 + test/assets/injectedfile.js | 2 + test/assets/injectedstyle.css | 3 + test/assets/input/button.html | 16 + test/assets/input/checkbox.html | 42 + test/assets/input/fileupload.html | 9 + test/assets/input/keyboard.html | 42 + test/assets/input/mouse-helper.js | 74 + test/assets/input/rotatedButton.html | 21 + test/assets/input/scrollable.html | 23 + test/assets/input/select.html | 69 + test/assets/input/textarea.html | 15 + test/assets/input/touches.html | 35 + test/assets/input/wheel.html | 43 + test/assets/jscoverage/eval.html | 1 + test/assets/jscoverage/involved.html | 15 + test/assets/jscoverage/multiple.html | 2 + test/assets/jscoverage/ranges.html | 2 + test/assets/jscoverage/script1.js | 1 + test/assets/jscoverage/script2.js | 1 + test/assets/jscoverage/simple.html | 2 + test/assets/jscoverage/sourceurl.html | 4 + test/assets/jscoverage/unused.html | 1 + test/assets/mobile.html | 1 + test/assets/modernizr.js | 3 + test/assets/networkidle.html | 19 + test/assets/offscreenbuttons.html | 38 + test/assets/one-style.css | 3 + test/assets/one-style.html | 2 + test/assets/playground.html | 15 + test/assets/popup/popup.html | 9 + test/assets/popup/window-open.html | 11 + test/assets/pptr.png | Bin 0 -> 6138 bytes test/assets/resetcss.html | 50 + test/assets/self-request.html | 5 + test/assets/serviceworkers/empty/sw.html | 3 + test/assets/serviceworkers/empty/sw.js | 0 test/assets/serviceworkers/fetch/style.css | 3 + test/assets/serviceworkers/fetch/sw.html | 5 + test/assets/serviceworkers/fetch/sw.js | 7 + test/assets/shadow.html | 17 + .../assets/simple-extension/content-script.js | 2 + test/assets/simple-extension/index.js | 2 + test/assets/simple-extension/manifest.json | 14 + test/assets/simple.json | 1 + test/assets/tamperable.html | 3 + test/assets/title.html | 1 + test/assets/worker/worker.html | 14 + test/assets/worker/worker.js | 16 + test/assets/wrappedlink.html | 32 + test/browser.spec.ts | 81 + test/browsercontext.spec.ts | 207 + test/chromiumonly.spec.ts | 159 + test/click.spec.ts | 352 ++ test/cookies.spec.ts | 564 ++ test/coverage-utils.js | 164 + test/coverage.spec.ts | 280 + test/defaultbrowsercontext.spec.ts | 114 + test/dialog.spec.ts | 74 + test/diffstyle.css | 13 + test/elementhandle.spec.ts | 453 ++ test/emulation.spec.ts | 389 ++ test/evaluation.spec.ts | 476 ++ test/fixtures.spec.ts | 93 + test/fixtures/closeme.js | 5 + test/fixtures/dumpio.js | 8 + test/frame.spec.ts | 270 + test/golden-chromium/csscoverage-involved.txt | 16 + test/golden-chromium/grid-cell-0.png | Bin 0 -> 436 bytes test/golden-chromium/grid-cell-1.png | Bin 0 -> 276 bytes test/golden-chromium/grid-cell-2.png | Bin 0 -> 428 bytes test/golden-chromium/grid-cell-3.png | Bin 0 -> 448 bytes test/golden-chromium/jscoverage-involved.txt | 28 + test/golden-chromium/mock-binary-response.png | Bin 0 -> 6789 bytes .../screenshot-clip-odd-size.png | Bin 0 -> 81 bytes test/golden-chromium/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 ...creenshot-element-larger-than-viewport.png | Bin 0 -> 2807 bytes .../screenshot-element-padding-border.png | Bin 0 -> 168 bytes .../screenshot-element-rotate.png | Bin 0 -> 2342 bytes .../screenshot-element-scrolled-into-view.png | Bin 0 -> 168 bytes .../screenshot-grid-fullpage.png | Bin 0 -> 74972 bytes .../screenshot-offscreen-clip.png | Bin 0 -> 266 bytes test/golden-chromium/screenshot-sanity.png | Bin 0 -> 36252 bytes test/golden-chromium/transparent.png | Bin 0 -> 119 bytes .../vision-deficiency-achromatopsia.png | Bin 0 -> 33569 bytes .../vision-deficiency-blurredVision.png | Bin 0 -> 84544 bytes .../vision-deficiency-deuteranopia.png | Bin 0 -> 37483 bytes .../vision-deficiency-protanopia.png | Bin 0 -> 36282 bytes .../vision-deficiency-tritanopia.png | Bin 0 -> 37282 bytes test/golden-chromium/white.jpg | Bin 0 -> 357 bytes test/golden-firefox/grid-cell-0.png | Bin 0 -> 331 bytes test/golden-firefox/grid-cell-1.png | Bin 0 -> 201 bytes .../screenshot-clip-odd-size.png | Bin 0 -> 75 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 ...creenshot-element-larger-than-viewport.png | Bin 0 -> 2797 bytes .../screenshot-element-padding-border.png | Bin 0 -> 153 bytes .../screenshot-element-rotate.png | Bin 0 -> 1800 bytes .../screenshot-element-scrolled-into-view.png | Bin 0 -> 153 bytes .../screenshot-grid-fullpage.png | Bin 0 -> 55662 bytes .../screenshot-offscreen-clip.png | Bin 0 -> 279 bytes test/golden-firefox/screenshot-sanity.png | Bin 0 -> 26146 bytes test/golden-utils.js | 160 + test/headful.spec.ts | 269 + test/idle_override.spec.ts | 94 + test/ignorehttpserrors.spec.ts | 135 + test/input.spec.ts | 343 ++ test/jshandle.spec.ts | 300 ++ test/keyboard.spec.ts | 408 ++ test/launcher.spec.ts | 718 +++ test/mocha-ts-require.js | 11 + test/mocha-utils.ts | 309 ++ test/mouse.spec.ts | 242 + test/navigation.spec.ts | 776 +++ test/network.spec.ts | 610 +++ test/oopif.spec.ts | 74 + test/page.spec.ts | 1770 ++++++ test/queryselector.spec.ts | 507 ++ test/requestinterception.spec.ts | 743 +++ test/run_static_server.js | 33 + test/screenshot.spec.ts | 329 ++ test/target.spec.ts | 294 + test/touchscreen.spec.ts | 49 + test/tracing.spec.ts | 133 + test/tsconfig.json | 7 + test/tsconfig.test.json | 6 + test/utils.js | 135 + test/waittask.spec.ts | 773 +++ test/worker.spec.ts | 126 + tsconfig.base.json | 14 + tsconfig.json | 12 + typescript-if-required.js | 61 + utils/ESTreeWalker.js | 135 + utils/apply_next_version.js | 32 + utils/bisect.js | 229 + utils/check_availability.js | 298 ++ utils/doclint/.gitignore | 1 + utils/doclint/Message.js | 44 + utils/doclint/README.md | 27 + utils/doclint/Source.js | 117 + .../doclint/check_public_api/Documentation.js | 157 + utils/doclint/check_public_api/JSBuilder.js | 279 + utils/doclint/check_public_api/MDBuilder.js | 402 ++ utils/doclint/check_public_api/index.js | 1027 ++++ utils/doclint/cli.js | 140 + utils/doclint/preprocessor/index.js | 168 + .../doclint/preprocessor/preprocessor.spec.js | 248 + utils/fetch_devices.js | 278 + utils/prepare_puppeteer_core.js | 27 + utils/remove_version_suffix.js | 26 + utils/testserver/LICENSE | 202 + utils/testserver/README.md | 18 + utils/testserver/cert.pem | 20 + utils/testserver/index.js | 284 + utils/testserver/key.pem | 28 + utils/testserver/package.json | 15 + vendor/README.md | 13 + vendor/mitt/README.md | 179 + vendor/mitt/dist/mitt.es.js | 2 + vendor/mitt/dist/mitt.es.js.map | 1 + vendor/mitt/dist/mitt.js | 2 + vendor/mitt/dist/mitt.js.map | 1 + vendor/mitt/dist/mitt.modern.js | 2 + vendor/mitt/dist/mitt.modern.js.map | 1 + vendor/mitt/dist/mitt.umd.js | 2 + vendor/mitt/dist/mitt.umd.js.map | 1 + vendor/mitt/index.d.ts | 21 + vendor/mitt/package.json | 141 + vendor/mitt/src/index.ts | 92 + vendor/tsconfig.cjs.json | 11 + vendor/tsconfig.esm.json | 11 + versions.js | 41 + web-test-runner.config.js | 44 + 520 files changed, 54228 insertions(+), 5451 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .gitattributes create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .versionrc.js create mode 100644 CHANGELOG.md create mode 100644 api-extractor.json delete mode 100644 build.js create mode 100644 cjs-entry-core.js create mode 100644 cjs-entry.js create mode 100644 commitlint.config.js delete mode 100644 docs/CNAME create mode 100644 docs/api.md create mode 100644 docs/assets/overview.png create mode 100644 docs/assets/overview.svg delete mode 100644 docs/favicons/android-chrome-192x192.png delete mode 100644 docs/favicons/android-chrome-384x384.png delete mode 100644 docs/favicons/apple-touch-icon.png delete mode 100644 docs/favicons/browserconfig.xml delete mode 100644 docs/favicons/favicon-16x16.png delete mode 100644 docs/favicons/favicon-32x32.png delete mode 100644 docs/favicons/favicon.ico delete mode 100644 docs/favicons/mstile-150x150.png delete mode 100644 docs/favicons/safari-pinned-tab.svg delete mode 100644 docs/favicons/site.webmanifest delete mode 100644 docs/images/checkmark.svg delete mode 100644 docs/images/close.svg delete mode 100644 docs/images/cog.svg delete mode 100644 docs/images/github.png delete mode 100644 docs/images/home.svg delete mode 100644 docs/images/menu.svg delete mode 100644 docs/images/pptr.png delete mode 100644 docs/images/search.svg delete mode 100644 docs/images/slack.svg delete mode 100644 docs/images/stackoverflow.svg delete mode 100644 docs/images/wrench.svg delete mode 100644 docs/index.html delete mode 100644 docs/index.js create mode 100644 docs/issue_template.md delete mode 100644 docs/style.css delete mode 100644 docs/sw.js create mode 100644 docs/troubleshooting.md create mode 100644 examples/README.md create mode 100644 examples/block-images.js create mode 100644 examples/cross-browser.js create mode 100644 examples/custom-event.js create mode 100644 examples/detect-sniff.js create mode 100644 examples/oopif.js create mode 100644 examples/pdf.js create mode 100644 examples/proxy.js create mode 100644 examples/screenshot-fullpage.js create mode 100644 examples/screenshot.js create mode 100644 examples/search.js create mode 100644 experimental/puppeteer-firefox/.ci/node10/Dockerfile.linux create mode 100644 experimental/puppeteer-firefox/.ci/node10/Dockerfile.windows create mode 100644 experimental/puppeteer-firefox/.cirrus.yml create mode 100644 experimental/puppeteer-firefox/.gitignore create mode 100644 experimental/puppeteer-firefox/.npmignore create mode 100644 experimental/puppeteer-firefox/DeviceDescriptors.js create mode 100644 experimental/puppeteer-firefox/Errors.js create mode 100644 experimental/puppeteer-firefox/LICENSE create mode 100644 experimental/puppeteer-firefox/README.md create mode 100644 experimental/puppeteer-firefox/examples/screenshot.js create mode 100644 experimental/puppeteer-firefox/examples/search.js create mode 100644 experimental/puppeteer-firefox/index.js create mode 100644 experimental/puppeteer-firefox/install.js create mode 100644 experimental/puppeteer-firefox/lib/Accessibility.js create mode 100644 experimental/puppeteer-firefox/lib/Browser.js create mode 100644 experimental/puppeteer-firefox/lib/BrowserFetcher.js create mode 100644 experimental/puppeteer-firefox/lib/Connection.js create mode 100644 experimental/puppeteer-firefox/lib/DOMWorld.js create mode 100644 experimental/puppeteer-firefox/lib/DeviceDescriptors.js create mode 100644 experimental/puppeteer-firefox/lib/Dialog.js create mode 100644 experimental/puppeteer-firefox/lib/Errors.js create mode 100644 experimental/puppeteer-firefox/lib/Events.js create mode 100644 experimental/puppeteer-firefox/lib/ExecutionContext.js create mode 100644 experimental/puppeteer-firefox/lib/FrameManager.js create mode 100644 experimental/puppeteer-firefox/lib/Input.js create mode 100644 experimental/puppeteer-firefox/lib/JSHandle.js create mode 100644 experimental/puppeteer-firefox/lib/Launcher.js create mode 100644 experimental/puppeteer-firefox/lib/NavigationWatchdog.js create mode 100644 experimental/puppeteer-firefox/lib/NetworkManager.js create mode 100644 experimental/puppeteer-firefox/lib/Page.js create mode 100644 experimental/puppeteer-firefox/lib/Puppeteer.js create mode 100644 experimental/puppeteer-firefox/lib/TimeoutSettings.js create mode 100644 experimental/puppeteer-firefox/lib/USKeyboardLayout.js create mode 100644 experimental/puppeteer-firefox/lib/WebSocketTransport.js create mode 100644 experimental/puppeteer-firefox/lib/api.js create mode 100644 experimental/puppeteer-firefox/lib/externs.d.ts create mode 100644 experimental/puppeteer-firefox/lib/helper.js create mode 100644 experimental/puppeteer-firefox/misc/00-puppeteer-prefs.js create mode 100644 experimental/puppeteer-firefox/misc/install-preferences.js create mode 100644 experimental/puppeteer-firefox/misc/puppeteer.cfg create mode 100644 experimental/puppeteer-firefox/package.json create mode 100644 experimental/puppeteer-firefox/tsconfig.json create mode 100644 install.js create mode 100644 mocha-config/base.js create mode 100644 mocha-config/coverage-tests.js create mode 100644 mocha-config/doclint-tests.js create mode 100644 mocha-config/puppeteer-unit-tests.js create mode 100644 my-website/.gitignore create mode 100644 my-website/README.md create mode 100644 my-website/babel.config.js create mode 100644 my-website/docs/index.md create mode 100644 my-website/docs/intro.md create mode 100644 my-website/docs/new-doc/index.md create mode 100644 my-website/docs/new-doc/puppeteer.browser.browsercontexts.md create mode 100644 my-website/docs/new-doc/puppeteer.browser.close.md create mode 100644 my-website/docs/new-doc/puppeteer.browser.createincognitobrowsercontext.md create mode 100644 my-website/docs/new-doc/puppeteer.browser.defaultbrowsercontext.md create mode 100644 my-website/docs/new-doc/puppeteer.browser.disconnect.md create mode 100644 my-website/docs/new-doc/puppeteer.browser.isconnected.md create mode 100644 my-website/docusaurus.config.js create mode 100644 my-website/package.json create mode 100644 my-website/sidebars.js create mode 100644 my-website/src/components/HomepageFeatures.js create mode 100644 my-website/src/components/HomepageFeatures.module.css create mode 100644 my-website/src/css/custom.css create mode 100644 my-website/src/pages/index.js create mode 100644 my-website/src/pages/index.module.css create mode 100644 my-website/src/pages/markdown-page.md create mode 100644 my-website/static/.nojekyll create mode 100644 my-website/static/img/docusaurus.png create mode 100644 my-website/static/img/favicon.ico create mode 100644 my-website/static/img/logo.svg create mode 100644 my-website/static/img/tutorial/docsVersionDropdown.png create mode 100644 my-website/static/img/tutorial/localeDropdown.png create mode 100644 my-website/static/img/undraw_docusaurus_mountain.svg create mode 100644 my-website/static/img/undraw_docusaurus_react.svg create mode 100644 my-website/static/img/undraw_docusaurus_tree.svg create mode 100644 prettier.config.js create mode 100644 scripts/ensure-correct-devtools-protocol-package.ts create mode 100644 scripts/ensure-pinned-deps.ts create mode 100644 scripts/test-install.sh create mode 100644 scripts/test-ts-definition-files.ts create mode 100644 scripts/tsconfig.json create mode 100644 src/.eslintrc.js create mode 100644 src/api-docs-entry.ts create mode 100644 src/common/Accessibility.ts create mode 100644 src/common/AriaQueryHandler.ts create mode 100644 src/common/Browser.ts create mode 100644 src/common/BrowserConnector.ts create mode 100644 src/common/BrowserWebSocketTransport.ts create mode 100644 src/common/Connection.ts create mode 100644 src/common/ConnectionTransport.ts create mode 100644 src/common/ConsoleMessage.ts create mode 100644 src/common/Coverage.ts create mode 100644 src/common/DOMWorld.ts create mode 100644 src/common/Debug.ts create mode 100644 src/common/DeviceDescriptors.ts create mode 100644 src/common/Dialog.ts create mode 100644 src/common/EmulationManager.ts create mode 100644 src/common/Errors.ts create mode 100644 src/common/EvalTypes.ts create mode 100644 src/common/EventEmitter.ts create mode 100644 src/common/Events.ts create mode 100644 src/common/ExecutionContext.ts create mode 100644 src/common/FileChooser.ts create mode 100644 src/common/FrameManager.ts create mode 100644 src/common/HTTPRequest.ts create mode 100644 src/common/HTTPResponse.ts create mode 100644 src/common/Input.ts create mode 100644 src/common/JSHandle.ts create mode 100644 src/common/LifecycleWatcher.ts create mode 100644 src/common/NetworkConditions.ts create mode 100644 src/common/NetworkManager.ts create mode 100644 src/common/PDFOptions.ts create mode 100644 src/common/Page.ts create mode 100644 src/common/Product.ts create mode 100644 src/common/Puppeteer.ts create mode 100644 src/common/PuppeteerViewport.ts create mode 100644 src/common/QueryHandler.ts create mode 100644 src/common/SecurityDetails.ts create mode 100644 src/common/Target.ts create mode 100644 src/common/TimeoutSettings.ts create mode 100644 src/common/Tracing.ts create mode 100644 src/common/USKeyboardLayout.ts create mode 100644 src/common/WebWorker.ts create mode 100644 src/common/assert.ts create mode 100644 src/common/fetch.ts create mode 100644 src/common/helper.ts create mode 100644 src/environment.ts delete mode 100644 src/favicons/android-chrome-192x192.png delete mode 100644 src/favicons/android-chrome-384x384.png delete mode 100644 src/favicons/apple-touch-icon.png delete mode 100644 src/favicons/browserconfig.xml delete mode 100644 src/favicons/favicon-16x16.png delete mode 100644 src/favicons/favicon-32x32.png delete mode 100644 src/favicons/favicon.ico delete mode 100644 src/favicons/mstile-150x150.png delete mode 100644 src/favicons/safari-pinned-tab.svg delete mode 100644 src/favicons/site.webmanifest delete mode 100644 src/images/checkmark.svg delete mode 100644 src/images/close.svg delete mode 100644 src/images/cog.svg delete mode 100644 src/images/github.png delete mode 100644 src/images/home.svg delete mode 100644 src/images/menu.svg delete mode 100644 src/images/pptr.png delete mode 100644 src/images/search.svg delete mode 100644 src/images/slack.svg delete mode 100644 src/images/stackoverflow.svg delete mode 100644 src/images/wrench.svg delete mode 100644 src/index.html delete mode 100644 src/index.js create mode 100644 src/initialize-node.ts create mode 100644 src/initialize-web.ts create mode 100644 src/node-puppeteer-core.ts create mode 100644 src/node.ts create mode 100644 src/node/BrowserFetcher.ts create mode 100644 src/node/BrowserRunner.ts create mode 100644 src/node/LaunchOptions.ts create mode 100644 src/node/Launcher.ts create mode 100644 src/node/NodeWebSocketTransport.ts create mode 100644 src/node/PipeTransport.ts create mode 100644 src/node/Puppeteer.ts create mode 100644 src/node/install.ts delete mode 100644 src/pptr/APIDocumentation.js delete mode 100644 src/pptr/PPTRProduct.js delete mode 100644 src/pptr/style.css create mode 100644 src/revisions.ts delete mode 100644 src/sw-template.js delete mode 100644 src/third_party/CodeMirror.LICENSE delete mode 100644 src/third_party/commonmark.min.js delete mode 100644 src/third_party/commonmarkjs.LICENSE delete mode 100644 src/third_party/idb-keyval.LICENSE delete mode 100644 src/third_party/idb-keyval.mjs delete mode 100644 src/third_party/javascript.js delete mode 100644 src/third_party/runmode-standalone.js create mode 100644 src/tsconfig.cjs.json create mode 100644 src/tsconfig.esm.json delete mode 100644 src/ui/App.js delete mode 100644 src/ui/ContentComponent.js delete mode 100644 src/ui/EventEmitter.js delete mode 100644 src/ui/FuzzySearch.js delete mode 100644 src/ui/SearchComponent.js delete mode 100644 src/ui/SettingsComponent.js delete mode 100644 src/ui/SidebarComponent.js delete mode 100644 src/ui/ToolbarComponent.js delete mode 100644 src/ui/content-component.css delete mode 100644 src/ui/html.js delete mode 100644 src/ui/main.css delete mode 100644 src/ui/polyfills.js delete mode 100644 src/ui/search-component.css delete mode 100644 src/ui/settings-component.css delete mode 100644 src/ui/sidebar-component.css delete mode 100644 src/ui/toolbar-component.css create mode 100644 src/web.ts create mode 100644 test-browser/connection.spec.js create mode 100644 test-browser/debug.spec.js create mode 100644 test-browser/helper.js create mode 100644 test-ts-types/js-cjs-import-cjs-output/bad.js create mode 100644 test-ts-types/js-cjs-import-cjs-output/good.js create mode 100644 test-ts-types/js-cjs-import-cjs-output/package.json create mode 100644 test-ts-types/js-cjs-import-cjs-output/tsconfig.json create mode 100644 test-ts-types/js-cjs-import-esm-output/bad.js create mode 100644 test-ts-types/js-cjs-import-esm-output/good.js create mode 100644 test-ts-types/js-cjs-import-esm-output/package.json create mode 100644 test-ts-types/js-cjs-import-esm-output/tsconfig.json create mode 100644 test-ts-types/js-esm-import-cjs-output/bad.js create mode 100644 test-ts-types/js-esm-import-cjs-output/good.js create mode 100644 test-ts-types/js-esm-import-cjs-output/package.json create mode 100644 test-ts-types/js-esm-import-cjs-output/tsconfig.json create mode 100644 test-ts-types/js-esm-import-esm-output/bad.js create mode 100644 test-ts-types/js-esm-import-esm-output/good.js create mode 100644 test-ts-types/js-esm-import-esm-output/package.json create mode 100644 test-ts-types/js-esm-import-esm-output/tsconfig.json create mode 100644 test-ts-types/ts-cjs-import-cjs-output/bad.ts create mode 100644 test-ts-types/ts-cjs-import-cjs-output/good.ts create mode 100644 test-ts-types/ts-cjs-import-cjs-output/package.json create mode 100644 test-ts-types/ts-cjs-import-cjs-output/tsconfig.json create mode 100644 test-ts-types/ts-esm-import-cjs-output/bad.ts create mode 100644 test-ts-types/ts-esm-import-cjs-output/good.ts create mode 100644 test-ts-types/ts-esm-import-cjs-output/package.json create mode 100644 test-ts-types/ts-esm-import-cjs-output/tsconfig.json create mode 100644 test-ts-types/ts-esm-import-esm-output/bad.ts create mode 100644 test-ts-types/ts-esm-import-esm-output/good.ts create mode 100644 test-ts-types/ts-esm-import-esm-output/package.json create mode 100644 test-ts-types/ts-esm-import-esm-output/tsconfig.json create mode 100644 test/.eslintrc.js create mode 100644 test/CDPSession.spec.ts create mode 100644 test/EventEmitter.spec.ts create mode 100644 test/README.md create mode 100644 test/accessibility.spec.ts create mode 100644 test/ariaqueryhandler.spec.ts create mode 100644 test/assert-coverage-test.js create mode 100644 test/assets/beforeunload.html create mode 100644 test/assets/cached/one-style-font.css create mode 100644 test/assets/cached/one-style-font.html create mode 100644 test/assets/cached/one-style.css create mode 100644 test/assets/cached/one-style.html create mode 100644 test/assets/chromium-linux.zip create mode 100644 test/assets/consolelog.html create mode 100644 test/assets/csp.html create mode 100644 test/assets/csscoverage/Dosis-Regular.ttf create mode 100644 test/assets/csscoverage/OFL.txt create mode 100644 test/assets/csscoverage/involved.html create mode 100644 test/assets/csscoverage/media.html create mode 100644 test/assets/csscoverage/multiple.html create mode 100644 test/assets/csscoverage/simple.html create mode 100644 test/assets/csscoverage/sourceurl.html create mode 100644 test/assets/csscoverage/stylesheet1.css create mode 100644 test/assets/csscoverage/stylesheet2.css create mode 100644 test/assets/csscoverage/unused.html create mode 100644 test/assets/detect-touch.html create mode 100644 test/assets/digits/0.png create mode 100644 test/assets/digits/1.png create mode 100644 test/assets/digits/2.png create mode 100644 test/assets/digits/3.png create mode 100644 test/assets/digits/4.png create mode 100644 test/assets/digits/5.png create mode 100644 test/assets/digits/6.png create mode 100644 test/assets/digits/7.png create mode 100644 test/assets/digits/8.png create mode 100644 test/assets/digits/9.png create mode 100644 test/assets/dynamic-oopif.html create mode 100644 test/assets/empty.html create mode 100644 test/assets/error.html create mode 100644 test/assets/es6/.eslintrc create mode 100644 test/assets/es6/es6import.js create mode 100644 test/assets/es6/es6module.js create mode 100644 test/assets/es6/es6pathimport.js create mode 100644 test/assets/favicon.ico create mode 100644 test/assets/file-to-upload.txt create mode 100644 test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 create mode 100644 test/assets/frames/frame.html create mode 100644 test/assets/frames/frameset.html create mode 100644 test/assets/frames/nested-frames.html create mode 100644 test/assets/frames/one-frame-url-fragment.html create mode 100644 test/assets/frames/one-frame.html create mode 100644 test/assets/frames/script.js create mode 100644 test/assets/frames/style.css create mode 100644 test/assets/frames/two-frames.html create mode 100644 test/assets/global-var.html create mode 100644 test/assets/grid.html create mode 100644 test/assets/historyapi.html create mode 100644 test/assets/idle-detector.html create mode 100644 test/assets/injectedfile.js create mode 100644 test/assets/injectedstyle.css create mode 100644 test/assets/input/button.html create mode 100644 test/assets/input/checkbox.html create mode 100644 test/assets/input/fileupload.html create mode 100644 test/assets/input/keyboard.html create mode 100644 test/assets/input/mouse-helper.js create mode 100644 test/assets/input/rotatedButton.html create mode 100644 test/assets/input/scrollable.html create mode 100644 test/assets/input/select.html create mode 100644 test/assets/input/textarea.html create mode 100644 test/assets/input/touches.html create mode 100644 test/assets/input/wheel.html create mode 100644 test/assets/jscoverage/eval.html create mode 100644 test/assets/jscoverage/involved.html create mode 100644 test/assets/jscoverage/multiple.html create mode 100644 test/assets/jscoverage/ranges.html create mode 100644 test/assets/jscoverage/script1.js create mode 100644 test/assets/jscoverage/script2.js create mode 100644 test/assets/jscoverage/simple.html create mode 100644 test/assets/jscoverage/sourceurl.html create mode 100644 test/assets/jscoverage/unused.html create mode 100644 test/assets/mobile.html create mode 100644 test/assets/modernizr.js create mode 100644 test/assets/networkidle.html create mode 100644 test/assets/offscreenbuttons.html create mode 100644 test/assets/one-style.css create mode 100644 test/assets/one-style.html create mode 100644 test/assets/playground.html create mode 100644 test/assets/popup/popup.html create mode 100644 test/assets/popup/window-open.html create mode 100644 test/assets/pptr.png create mode 100644 test/assets/resetcss.html create mode 100644 test/assets/self-request.html create mode 100644 test/assets/serviceworkers/empty/sw.html create mode 100644 test/assets/serviceworkers/empty/sw.js create mode 100644 test/assets/serviceworkers/fetch/style.css create mode 100644 test/assets/serviceworkers/fetch/sw.html create mode 100644 test/assets/serviceworkers/fetch/sw.js create mode 100644 test/assets/shadow.html create mode 100644 test/assets/simple-extension/content-script.js create mode 100644 test/assets/simple-extension/index.js create mode 100644 test/assets/simple-extension/manifest.json create mode 100644 test/assets/simple.json create mode 100644 test/assets/tamperable.html create mode 100644 test/assets/title.html create mode 100644 test/assets/worker/worker.html create mode 100644 test/assets/worker/worker.js create mode 100644 test/assets/wrappedlink.html create mode 100644 test/browser.spec.ts create mode 100644 test/browsercontext.spec.ts create mode 100644 test/chromiumonly.spec.ts create mode 100644 test/click.spec.ts create mode 100644 test/cookies.spec.ts create mode 100644 test/coverage-utils.js create mode 100644 test/coverage.spec.ts create mode 100644 test/defaultbrowsercontext.spec.ts create mode 100644 test/dialog.spec.ts create mode 100644 test/diffstyle.css create mode 100644 test/elementhandle.spec.ts create mode 100644 test/emulation.spec.ts create mode 100644 test/evaluation.spec.ts create mode 100644 test/fixtures.spec.ts create mode 100644 test/fixtures/closeme.js create mode 100644 test/fixtures/dumpio.js create mode 100644 test/frame.spec.ts create mode 100644 test/golden-chromium/csscoverage-involved.txt create mode 100644 test/golden-chromium/grid-cell-0.png create mode 100644 test/golden-chromium/grid-cell-1.png create mode 100644 test/golden-chromium/grid-cell-2.png create mode 100644 test/golden-chromium/grid-cell-3.png create mode 100644 test/golden-chromium/jscoverage-involved.txt create mode 100644 test/golden-chromium/mock-binary-response.png create mode 100644 test/golden-chromium/screenshot-clip-odd-size.png create mode 100644 test/golden-chromium/screenshot-clip-rect.png create mode 100644 test/golden-chromium/screenshot-element-bounding-box.png create mode 100644 test/golden-chromium/screenshot-element-fractional-offset.png create mode 100644 test/golden-chromium/screenshot-element-fractional.png create mode 100644 test/golden-chromium/screenshot-element-larger-than-viewport.png create mode 100644 test/golden-chromium/screenshot-element-padding-border.png create mode 100644 test/golden-chromium/screenshot-element-rotate.png create mode 100644 test/golden-chromium/screenshot-element-scrolled-into-view.png create mode 100644 test/golden-chromium/screenshot-grid-fullpage.png create mode 100644 test/golden-chromium/screenshot-offscreen-clip.png create mode 100644 test/golden-chromium/screenshot-sanity.png create mode 100644 test/golden-chromium/transparent.png create mode 100644 test/golden-chromium/vision-deficiency-achromatopsia.png create mode 100644 test/golden-chromium/vision-deficiency-blurredVision.png create mode 100644 test/golden-chromium/vision-deficiency-deuteranopia.png create mode 100644 test/golden-chromium/vision-deficiency-protanopia.png create mode 100644 test/golden-chromium/vision-deficiency-tritanopia.png create mode 100644 test/golden-chromium/white.jpg create mode 100644 test/golden-firefox/grid-cell-0.png create mode 100644 test/golden-firefox/grid-cell-1.png create mode 100644 test/golden-firefox/screenshot-clip-odd-size.png create mode 100644 test/golden-firefox/screenshot-clip-rect.png create mode 100644 test/golden-firefox/screenshot-element-bounding-box.png create mode 100644 test/golden-firefox/screenshot-element-fractional-offset.png create mode 100644 test/golden-firefox/screenshot-element-fractional.png create mode 100644 test/golden-firefox/screenshot-element-larger-than-viewport.png create mode 100644 test/golden-firefox/screenshot-element-padding-border.png create mode 100644 test/golden-firefox/screenshot-element-rotate.png create mode 100644 test/golden-firefox/screenshot-element-scrolled-into-view.png create mode 100644 test/golden-firefox/screenshot-grid-fullpage.png create mode 100644 test/golden-firefox/screenshot-offscreen-clip.png create mode 100644 test/golden-firefox/screenshot-sanity.png create mode 100644 test/golden-utils.js create mode 100644 test/headful.spec.ts create mode 100644 test/idle_override.spec.ts create mode 100644 test/ignorehttpserrors.spec.ts create mode 100644 test/input.spec.ts create mode 100644 test/jshandle.spec.ts create mode 100644 test/keyboard.spec.ts create mode 100644 test/launcher.spec.ts create mode 100644 test/mocha-ts-require.js create mode 100644 test/mocha-utils.ts create mode 100644 test/mouse.spec.ts create mode 100644 test/navigation.spec.ts create mode 100644 test/network.spec.ts create mode 100644 test/oopif.spec.ts create mode 100644 test/page.spec.ts create mode 100644 test/queryselector.spec.ts create mode 100644 test/requestinterception.spec.ts create mode 100644 test/run_static_server.js create mode 100644 test/screenshot.spec.ts create mode 100644 test/target.spec.ts create mode 100644 test/touchscreen.spec.ts create mode 100644 test/tracing.spec.ts create mode 100644 test/tsconfig.json create mode 100644 test/tsconfig.test.json create mode 100644 test/utils.js create mode 100644 test/waittask.spec.ts create mode 100644 test/worker.spec.ts create mode 100644 tsconfig.base.json create mode 100644 tsconfig.json create mode 100644 typescript-if-required.js create mode 100644 utils/ESTreeWalker.js create mode 100644 utils/apply_next_version.js create mode 100644 utils/bisect.js create mode 100644 utils/check_availability.js create mode 100644 utils/doclint/.gitignore create mode 100644 utils/doclint/Message.js create mode 100644 utils/doclint/README.md create mode 100644 utils/doclint/Source.js create mode 100644 utils/doclint/check_public_api/Documentation.js create mode 100644 utils/doclint/check_public_api/JSBuilder.js create mode 100644 utils/doclint/check_public_api/MDBuilder.js create mode 100644 utils/doclint/check_public_api/index.js create mode 100644 utils/doclint/cli.js create mode 100644 utils/doclint/preprocessor/index.js create mode 100644 utils/doclint/preprocessor/preprocessor.spec.js create mode 100644 utils/fetch_devices.js create mode 100644 utils/prepare_puppeteer_core.js create mode 100644 utils/remove_version_suffix.js create mode 100644 utils/testserver/LICENSE create mode 100644 utils/testserver/README.md create mode 100644 utils/testserver/cert.pem create mode 100644 utils/testserver/index.js create mode 100644 utils/testserver/key.pem create mode 100644 utils/testserver/package.json create mode 100644 vendor/README.md create mode 100644 vendor/mitt/README.md create mode 100644 vendor/mitt/dist/mitt.es.js create mode 100644 vendor/mitt/dist/mitt.es.js.map create mode 100644 vendor/mitt/dist/mitt.js create mode 100644 vendor/mitt/dist/mitt.js.map create mode 100644 vendor/mitt/dist/mitt.modern.js create mode 100644 vendor/mitt/dist/mitt.modern.js.map create mode 100644 vendor/mitt/dist/mitt.umd.js create mode 100644 vendor/mitt/dist/mitt.umd.js.map create mode 100644 vendor/mitt/index.d.ts create mode 100644 vendor/mitt/package.json create mode 100644 vendor/mitt/src/index.ts create mode 100644 vendor/tsconfig.cjs.json create mode 100644 vendor/tsconfig.esm.json create mode 100644 versions.js create mode 100644 web-test-runner.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.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/.eslintignore b/.eslintignore new file mode 100644 index 0000000..112136a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,15 @@ +test/assets/modernizr.js +third_party/* +utils/browser/puppeteer-web.js +utils/doclint/check_public_api/test/ +node6/* +node6-test/* +experimental/ +lib/ +/index.d.ts +# We ignore this file because it uses ES imports which we don't yet use +# in the Puppeteer src, so it trips up the ESLint-TypeScript parser. +utils/doclint/generate_types/test/test.ts +vendor/ +web-test-runner.config.mjs +test-ts-types/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a119974 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,185 @@ +module.exports = { + root: true, + env: { + node: true, + es6: true, + }, + + parser: '@typescript-eslint/parser', + + plugins: ['mocha', '@typescript-eslint', 'unicorn', 'import'], + + extends: ['plugin:prettier/recommended'], + + rules: { + // Error if files are not formatted with Prettier correctly. + 'prettier/prettier': 2, + // syntax preferences + quotes: [ + 2, + 'single', + { + avoidEscape: true, + allowTemplateLiterals: true, + }, + ], + 'spaced-comment': [ + 2, + 'always', + { + markers: ['*'], + }, + ], + eqeqeq: [2], + 'accessor-pairs': [ + 2, + { + getWithoutSet: false, + setWithoutGet: false, + }, + ], + 'new-parens': 2, + 'func-call-spacing': 2, + 'prefer-const': 2, + + 'max-len': [ + 2, + { + /* 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': 2, + 'no-with': 2, + 'no-multi-str': 2, + 'no-caller': 2, + 'no-implied-eval': 2, + 'no-labels': 2, + 'no-new-object': 2, + 'no-octal-escape': 2, + 'no-self-compare': 2, + 'no-shadow-restricted-names': 2, + 'no-cond-assign': 2, + 'no-debugger': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-empty-character-class': 2, + 'no-unreachable': 2, + 'no-unsafe-negation': 2, + radix: 2, + 'valid-typeof': 2, + 'no-unused-vars': [ + 2, + { + args: 'none', + vars: 'local', + varsIgnorePattern: + '([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)', + }, + ], + 'no-implicit-globals': [2], + + // es2015 features + 'require-yield': 2, + 'template-curly-spacing': [2, 'never'], + + // ensure we don't have any it.only or describe.only in prod + 'mocha/no-exclusive-tests': 'error', + + // enforce the variable in a catch block is named error + 'unicorn/catch-error-name': 'error', + + 'no-restricted-imports': [ + 'error', + { + patterns: ['*Events'], + paths: [ + { + name: 'mitt', + message: + 'Import Mitt from the vendored location: vendor/mitt/src/index.js', + }, + ], + }, + ], + 'import/extensions': ['error', 'ignorePackages'], + }, + overrides: [ + { + files: ['*.ts'], + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + ], + rules: { + 'no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': 2, + 'func-call-spacing': 0, + '@typescript-eslint/func-call-spacing': 2, + semi: 0, + '@typescript-eslint/semi': 2, + '@typescript-eslint/no-empty-function': 0, + '@typescript-eslint/no-use-before-define': 0, + // We have to use any on some types so the warning isn't valuable. + '@typescript-eslint/no-explicit-any': 0, + // We don't require explicit return types on basic functions or + // dummy functions in tests, for example + '@typescript-eslint/explicit-function-return-type': 0, + // We know it's bad and use it very sparingly but it's needed :( + '@typescript-eslint/ban-ts-ignore': 0, + /** + * This is the default options (as per + * https://github.com/typescript-eslint/typescript-eslint/blob/master/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, + }, + }, + ], + '@typescript-eslint/array-type': [ + 2, + { + default: 'array-simple', + }, + ], + // By default this is a warning but we want it to error. + '@typescript-eslint/explicit-module-boundary-types': 2, + }, + }, + { + files: ['test-browser/**/*.js'], + parserOptions: { + sourceType: 'module', + }, + env: { + es6: true, + browser: true, + es2020: true, + }, + }, + ], +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..222aec2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Declare files that will always have LF line endings on checkout. +*.txt eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index fc65c93..1b6fdd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ /node_modules/ -/test/output +test-ts-types/**/node_modules +test-ts-types/**/dist/ +/test/output-chromium +/test/output-firefox /test/test-user-data-dir* /.local-chromium/ +/.local-firefox/ /.dev_profile* .DS_Store *.swp @@ -10,4 +14,9 @@ package-lock.json yarn.lock /node6 -/lib/protocol.d.ts +/utils/browser/puppeteer-web.js +/lib +test/coverage.json +temp/ +new-docs/ +puppeteer.tgz diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..94a06c2 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +access=public diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..dacae78 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules/ +lib/ +third_party/ +vendor/ + +package-lock.json +yarn.lock +package.json diff --git a/.versionrc.js b/.versionrc.js new file mode 100644 index 0000000..117096c --- /dev/null +++ b/.versionrc.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. + */ + +module.exports = { + releaseCommitMessageFormat: 'chore(release): mark v{{currentTag}}', + skip: { + tag: true, + }, + scripts: { + prerelease: 'node utils/remove_version_suffix.js', + postbump: 'IS_RELEASE=true npm run doc && git add --update', + }, +}; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..51ae72f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,171 @@ +# 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. + +### [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)) + +# 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. diff --git a/LICENSE b/LICENSE index afdfe50..d2c171d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,7 +193,7 @@ 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 + 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, diff --git a/README.md b/README.md index 1be26c5..34e2081 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,462 @@ -# https://pptr.dev +# Puppeteer -This repository contains source code for https://pptr.dev website. + -## How it works +[![Build status](https://github.com/puppeteer/puppeteer/workflows/run-checks/badge.svg)](https://github.com/puppeteer/puppeteer/actions?query=workflow%3Arun-checks) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) -`pptr.dev` doesn't have any server side part. All the data is fetched dynamically from NPM and GitHub via XHRs. + -On the first load, `pptr.dev`: -- fetches puppeteer releases from NPM -- fetches `docs/api.md` for every release -- caches all the loaded data locally + -On a subsequent load, `pptr.dev` occasionally invalidates cached documentation and releases. +###### [API](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) -## Building and Running +> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. -1. To run debug version, use `npm run serve` and navigate browser to `http://localhost:8887`. -2. To run prod version, use `npm run build && npm run prod` and then navigate browser to `http://localhost:8888` + -> **NOTE** Debug version of `pptr.dev` doesn't require any build steps; serving [`index.html`](https://github.com/GoogleChromeLabs/pptr.dev/blob/master/index.html) with any static server -is sufficient. +###### What can I do? -> **NOTE** Debug version of `pptr.dev` doesn't include service worker to simplify development +Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started: -## Dependencies +- 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 up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome 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. + -- [commonmark.js](https://github.com/commonmark/commonmark.js/) is used to parse and render markdown documentation -- [idb-keyval](https://github.com/jakearchibald/idb-keyval) is used to work with IndexedDB -- [codemirror](http://codemirror.com/) is used to highlight code snippets inside markdown +Give it a spin: https://try-puppeteer.appspot.com/ -## FAQ + -#### Q: Does pptr.dev use Custom Elements? +## Getting Started -No. `pptr.dev` creates HTML elements with descriptive names to make markup nicer; this approach works in old browsers as well. +### Installation + +To use Puppeteer in your project, run: + +```bash +npm i puppeteer +# or "yarn add puppeteer" +``` + +Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, or to download a different browser, see [Environment variables](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#environment-variables). + +### puppeteer-core + +Since version 1.7.0 we publish the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package, +a version of Puppeteer that doesn't download any browser by default. + +```bash +npm i puppeteer-core +# or "yarn add puppeteer-core" +``` + +`puppeteer-core` is intended to be a lightweight version of Puppeteer for launching an existing browser installation or for connecting to a remote one. Be sure that the version of puppeteer-core you install is compatible with the +browser you intend to connect to. + +See [puppeteer vs puppeteer-core](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteer-vs-puppeteer-core). + +### Usage + +Puppeteer follows the latest [maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of Node. + +Note: Prior to v1.18.1, Puppeteer required at least Node v6.4.0. Versions from v1.18.1 to v2.1.0 rely on +Node 8.9.0+. Starting from v3.0.0 Puppeteer starts to rely on Node 10.18.1+. All examples below use async/await which is only supported in Node v7.6.0 or greater. + +Puppeteer will be familiar to people using other browser testing frameworks. You create an instance +of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#). + +**Example** - navigating to https://example.com and saving a screenshot as _example.png_: + +Save file as **example.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({ path: 'example.png' }); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node example.js +``` + +Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#pagesetviewportviewport). + +**Example** - create a PDF. + +Save file as **hn.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://news.ycombinator.com', { + waitUntil: 'networkidle2', + }); + await page.pdf({ path: 'hn.pdf', format: 'a4' }); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node hn.js +``` + +See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#pagepdfoptions) for more information about creating pdfs. + +**Example** - evaluate script in the context of the page + +Save file as **get-dimensions.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + + // Get the "viewport" of the page, as reported by the page. + const dimensions = await page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio, + }; + }); + + console.log('Dimensions:', dimensions); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node get-dimensions.js +``` + +See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. + + + + + +## Default runtime settings + +**1. Uses Headless mode** + +Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#puppeteerlaunchoptions) when launching a browser: + +```js +const browser = await puppeteer.launch({ headless: false }); // default is true +``` + +**2. Runs a bundled version of Chromium** + +By default, Puppeteer downloads and uses a specific version of Chromium 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: + +```js +const browser = await puppeteer.launch({ executablePath: '/path/to/Chrome' }); +``` + +You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#puppeteerlaunchoptions) 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/+/master/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**. + + + +## Resources + +- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md) +- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples/) +- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) + + + +## Debugging tips + +1. Turn off headless mode - sometimes it's useful to see what the browser is + displaying. Instead of launching in headless mode, launch a full version of + the browser using `headless: false`: + + ```js + const browser = await puppeteer.launch({ headless: false }); + ``` + +2. Slow it down - the `slowMo` option slows down Puppeteer operations by the + specified amount of milliseconds. It's another way to help see what's going on. + + ```js + const browser = await puppeteer.launch({ + headless: false, + slowMo: 250, // slow down by 250ms + }); + ``` + +3. Capture console output - You can listen for the `console` event. + This is also handy when debugging code in `page.evaluate()`: + + ```js + page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + + await page.evaluate(() => console.log(`url is ${location.href}`)); + ``` + +4. Use debugger in application code browser + + There are two execution context: node.js that is running test code, and the browser + running application code being tested. This lets you debug code in the + application code browser; ie code inside `evaluate()`. + + - Use `{devtools: true}` when launching Puppeteer: + + ```js + const browser = await puppeteer.launch({ devtools: true }); + ``` + + - Change default test timeout: + + jest: `jest.setTimeout(100000);` + + jasmine: `jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;` + + mocha: `this.timeout(100000);` (don't forget to change test to use [function and not '=>'](https://stackoverflow.com/a/23492442)) + + - Add an evaluate statement with `debugger` inside / add `debugger` to an existing evaluate statement: + + ```js + await page.evaluate(() => { + debugger; + }); + ``` + + The test will now stop executing in the above evaluate statement, and chromium will stop in debug mode. + +5. Use debugger in node.js + + This will let you debug test code. For example, you can step over `await page.click()` in the node.js script and see the click happen in the application code browser. + + Note that you won't be able to run `await page.click()` in + DevTools console due to this [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=833928). So if + you want to try something out, you have to add it to your test file. + + - Add `debugger;` to your test, eg: + + ```js + debugger; + await page.click('a[target=_blank]'); + ``` + + - Set `headless` to `false` + - Run `node --inspect-brk`, eg `node --inspect-brk node_modules/.bin/jest tests` + - In Chrome open `chrome://inspect/#devices` and click `inspect` + - In the newly opened test browser, type `F8` to resume test execution + - Now your `debugger` will be hit and you can debug in the test browser + +6. Enable verbose logging - internal DevTools protocol traffic + will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace. + + # Basic verbose logging + env DEBUG="puppeteer:*" node script.js + + # Protocol traffic can be rather noisy. This example filters out all Network domain messages + env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network' + +7. Debug your Puppeteer (node) code easily, using [ndb](https://github.com/GoogleChromeLabs/ndb) + +- `npm install -g ndb` (or even better, use [npx](https://github.com/zkat/npx)!) + +- add a `debugger` to your Puppeteer (node) code + +- add `ndb` (or `npx ndb`) before your test command. For example: + + `ndb jest` or `ndb mocha` (or `npx ndb jest` / `npx ndb mocha`) + +- debug your test inside chromium like a boss! + + + + + +## Usage with TypeScript + +We have recently completed a migration to move the Puppeteer source code from JavaScript to TypeScript and as of version 7.0.1 we ship our own built-in type definitions. + +If you are on a version older than 7, we recommend installing the Puppeteer type definitions from the [DefinitelyTyped](https://definitelytyped.org/) repository: + +```bash +npm install --save-dev @types/puppeteer +``` + +The types that you'll see appearing in the Puppeteer source code are based off the great work of those who have contributed to the `@types/puppeteer` package. We really appreciate the hard work those people put in to providing high quality TypeScript definitions for Puppeteer's users. + + + +## Contributing to Puppeteer + +Check out [contributing guide](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) to get an overview of Puppeteer development. + + + +# FAQ + +#### Q: Who maintains Puppeteer? + +The Chrome DevTools team maintains the library, but we'd love your help and expertise on the project! +See [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md). + +#### Q: What is the status of cross-browser support? + +Official Firefox support is currently experimental. The ongoing collaboration with Mozilla aims to support common end-to-end testing use cases, for which developers expect cross-browser coverage. The Puppeteer team needs input from users to stabilize Firefox support and to bring missing APIs to our attention. + +From Puppeteer v2.1.0 onwards you can specify [`puppeteer.launch({product: 'firefox'})`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#puppeteerlaunchoptions) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches. While [an older experiment](https://www.npmjs.com/package/puppeteer-firefox) required a patched version of Firefox, [the current approach](https://wiki.mozilla.org/Remote) works with “stock” Firefox. + +We will continue to collaborate with other browser vendors to bring Puppeteer support to browsers such as Safari. +This effort includes exploration of a standard for executing cross-browser commands (instead of relying on the non-standard DevTools Protocol used by Chrome). + +#### Q: What are Puppeteer’s goals and principles? + +The goals of the project are: + +- Provide a slim, canonical library that highlights the capabilities of the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). +- Provide a reference implementation for similar testing libraries. Eventually, these other frameworks could adopt Puppeteer as their foundational layer. +- Grow the adoption of headless/automated browser testing. +- Help dogfood new DevTools Protocol features...and catch bugs! +- Learn more about the pain points of automated browser testing and help fill those gaps. + +We adapt [Chromium principles](https://www.chromium.org/developers/core-principles) to help us drive product decisions: + +- **Speed**: Puppeteer has almost zero performance overhead over an automated page. +- **Security**: Puppeteer operates off-process with respect to Chromium, making it safe to automate potentially malicious pages. +- **Stability**: Puppeteer should not be flaky and should not leak memory. +- **Simplicity**: Puppeteer provides a high-level API that’s easy to use, understand, and debug. + +#### Q: Is Puppeteer replacing Selenium/WebDriver? + +**No**. Both projects are valuable for very different reasons: + +- Selenium/WebDriver focuses on cross-browser automation; its value proposition is a single standard API that works across all major browsers. +- Puppeteer focuses on Chromium; its value proposition is richer functionality and higher reliability. + +That said, you **can** use Puppeteer to run tests against Chromium, e.g. using the community-driven [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer). While this probably shouldn’t be your only testing solution, it does have a few good points compared to WebDriver: + +- Puppeteer requires zero setup and comes bundled with the Chromium version it works best with, making it [very easy to start with](https://github.com/puppeteer/puppeteer/#getting-started). At the end of the day, it’s better to have a few tests running chromium-only, than no tests at all. +- Puppeteer has event-driven architecture, which removes a lot of potential flakiness. There’s no need for evil “sleep(1000)” calls in puppeteer scripts. +- Puppeteer runs headless by default, which makes it fast to run. Puppeteer v1.5.0 also exposes browser contexts, making it possible to efficiently parallelize test execution. +- Puppeteer shines when it comes to debugging: flip the “headless” bit to false, add “slowMo”, and you’ll see what the browser is doing. You can even open Chrome DevTools to inspect the test environment. + +#### Q: Why doesn’t Puppeteer v.XXX work with Chromium v.YYY? + +We see Puppeteer as an **indivisible entity** with Chromium. Each version of Puppeteer bundles a specific version of Chromium – **the only** version it is guaranteed to work with. + +This is not an artificial constraint: A lot of work on Puppeteer is actually taking place in the Chromium repository. Here’s a typical story: + +- A Puppeteer bug is reported: https://github.com/puppeteer/puppeteer/issues/2709 +- It turned out this is an issue with the DevTools protocol, so we’re fixing it in Chromium: https://chromium-review.googlesource.com/c/chromium/src/+/1102154 +- Once the upstream fix is landed, we roll updated Chromium into Puppeteer: https://github.com/puppeteer/puppeteer/pull/2769 + +However, oftentimes it is desirable to use Puppeteer with the official Google Chrome rather than Chromium. For this to work, you should install a `puppeteer-core` version that corresponds to the Chrome version. + +For example, in order to drive Chrome 71 with puppeteer-core, use `chrome-71` npm tag: + +```bash +npm install puppeteer-core@chrome-71 +``` + +#### Q: Which Chromium version does Puppeteer use? + +Look for the `chromium` entry in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts). To find the corresponding Chromium commit and version number, search for the revision prefixed by an `r` in [OmahaProxy](https://omahaproxy.appspot.com/)'s "Find Releases" section. + +#### Q: Which Firefox version does Puppeteer use? + +Since Firefox support is experimental, Puppeteer downloads the latest [Firefox Nightly](https://wiki.mozilla.org/Nightly) when the `PUPPETEER_PRODUCT` environment variable is set to `firefox`. That's also why the value of `firefox` in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts) is `latest` -- Puppeteer isn't tied to a particular Firefox version. + +To fetch Firefox Nightly as part of Puppeteer installation: + +```bash +PUPPETEER_PRODUCT=firefox npm i puppeteer +# or "yarn add puppeteer" +``` + +#### Q: What’s considered a “Navigation”? + +From Puppeteer’s standpoint, **“navigation” is anything that changes a page’s URL**. +Aside from regular navigation where the browser hits the network to fetch a new document from the web server, this includes [anchor navigations](https://www.w3.org/TR/html5/single-page.html#scroll-to-fragid) and [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) usage. + +With this definition of “navigation,” **Puppeteer works seamlessly with single-page applications.** + +#### Q: What’s the difference between a “trusted" and "untrusted" input event? + +In browsers, input events could be divided into two big groups: trusted vs. untrusted. + +- **Trusted events**: events generated by users interacting with the page, e.g. using a mouse or keyboard. +- **Untrusted event**: events generated by Web APIs, e.g. `document.createEvent` or `element.click()` methods. + +Websites can distinguish between these two groups: + +- using an [`Event.isTrusted`](https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted) event flag +- sniffing for accompanying events. For example, every trusted `'click'` event is preceded by `'mousedown'` and `'mouseup'` events. + +For automation purposes it’s important to generate trusted events. **All input events generated with Puppeteer are trusted and fire proper accompanying events.** If, for some reason, one needs an untrusted event, it’s always possible to hop into a page context with `page.evaluate` and generate a fake event: + +```js +await page.evaluate(() => { + document.querySelector('button[type=submit]').click(); +}); +``` + +#### Q: What features does Puppeteer not support? + +You may find that Puppeteer does not behave as expected when controlling pages that incorporate audio and video. (For example, [video playback/screenshots is likely to fail](https://github.com/puppeteer/puppeteer/issues/291).) There are two reasons for this: + +- Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/v9.1.1/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.) +- Since Puppeteer (in all configurations) controls a desktop version of Chromium/Chrome, features that are only supported by the mobile version of Chrome are not supported. This means that Puppeteer [does not support HTTP Live Streaming (HLS)](https://caniuse.com/#feat=http-live-streaming). + +#### Q: I am having trouble installing / running Puppeteer in my test environment. Where should I look for help? + +We have a [troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) guide for various operating systems that lists the required dependencies. + +#### Q: How do I try/test a prerelease version of Puppeteer? + +You can check out this repo or install the latest prerelease from npm: + +```bash +npm i --save puppeteer@next +``` + +Please note that prerelease may be unstable and contain bugs. + +#### Q: I have more questions! Where do I ask? + +There are many ways to get help on Puppeteer: + +- [bugtracker](https://github.com/puppeteer/puppeteer/issues) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/puppeteer) + +Make sure to search these channels before posting your question. + + diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 0000000..0426638 --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "/lib/cjs/puppeteer/api-docs-entry.d.ts", + "bundledPackages": ["devtools-protocol"], + + "apiReport": { + "enabled": false + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "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/build.js b/build.js deleted file mode 100644 index b941c21..0000000 --- a/build.js +++ /dev/null @@ -1,190 +0,0 @@ -/** - * 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. - */ - -const os = require('os'); -const path = require('path'); -const fs = require('fs'); - -const SRC_PATH = path.join(__dirname, 'src'); -const DST_PATH = path.join(__dirname, 'docs'); - -if (os.platform() === 'win32') { - console.error('ERROR: build is not supported on Win32'); - process.exit(1); - return; -} - -(async () => { - const startTime = Date.now(); - const BUILD_VERSION = await generateVersion(); - - await step(`1. cleanup output folder`, async () => { - let cnameText = null; - const cnamePath = path.join(DST_PATH, 'CNAME'); - if (fs.existsSync(cnamePath)) - cnameText = fs.readFileSync(cnamePath, 'utf8'); - await rmAsync(DST_PATH); - fs.mkdirSync(DST_PATH); - if (cnameText) - fs.writeFileSync(cnamePath, cnameText, 'utf8'); - }); - - await step('2. generate index.js', async () => { - const rollup = require('rollup'); - const UglifyJS = require('uglify-es'); - - const bundle = await rollup.rollup({input: path.join(SRC_PATH, 'index.js')}); - const {code} = await bundle.generate({format: 'iife'}); - const result = UglifyJS.minify(code); - if (result.error) { - console.error('JS Minification failed: ' + result.error); - process.exit(1); - return; - } - - const header = '/* THIS FILE IS GENERATED BY build.js */\n\n'; - const versionScript = `window.__WEBSITE_VERSION__ = "${BUILD_VERSION}";\n`; - const scriptContent = header + versionScript + result.code; - fs.writeFileSync(path.join(DST_PATH, 'index.js'), scriptContent, 'utf8'); - }); - - await step('3. generate style.css', async () => { - const csso = require('csso'); - - const stylePaths = []; - stylePaths.push(...(await globAsync(SRC_PATH, 'ui/**/*.css'))); - stylePaths.push(...(await globAsync(SRC_PATH, 'pptr/**/*.css'))); - stylePaths.push(...(await globAsync(SRC_PATH, 'third_party/**/*.css'))); - const styles = stylePaths.map(stylePath => fs.readFileSync(stylePath, 'utf8')); - const styleContent = '/* THIS FILE IS GENERATED BY build.js */\n\n' + csso.minify(styles.join('\n'), {restructure: false}).css; - fs.writeFileSync(path.join(DST_PATH, 'style.css'), styleContent, 'utf8'); - }); - - await step('4. generate index.html', async () => { - // Launch browser, replace stylesheet links with concat style and generate index.html - const pptr = require('puppeteer'); - const browser = await pptr.launch(); - const [page] = await browser.pages(); - await page.setJavaScriptEnabled(false); - await page.goto('file://' + path.join(SRC_PATH, 'index.html'), {waitUnit: 'domcontentloaded'}); - await page.evaluate(() => { - const $$ = selector => Array.from(document.querySelectorAll(selector)); - const links = $$('link[rel=stylesheet]').filter(link => link.href.startsWith('file://')); - links.shift().href = '/style.css'; - links.forEach(link => link.remove()); - }); - const indexContent = '\n\n' + (await page.content()).split('\n').filter(line => !/^\s*$/.test(line)).join('\n'); - await browser.close(); - fs.writeFileSync(path.join(DST_PATH, 'index.html'), indexContent, 'utf8'); - }); - - await step('5. copy images and favicons', async () => { - // 5. Copy images and favicons into dist/ - await cpAsync(path.join(SRC_PATH, 'images'), path.join(DST_PATH, 'images')); - await cpAsync(path.join(SRC_PATH, 'favicons'), path.join(DST_PATH, 'favicons')); - }); - - await step('6. generate sw.js', async () => { - const {injectManifest} = require('workbox-build'); - - const {count, size} = await injectManifest({ - swSrc: path.join(SRC_PATH, 'sw-template.js'), - swDest: path.join(DST_PATH, 'sw.js'), - globDirectory: DST_PATH, - globIgnores: ['CNAME'], - globPatterns: ['**/*'] - }); - const kbSize = Math.round(size / 1024 * 100) / 100; - console.log(` - sw precaches ${count} files, totaling ${kbSize} Kb.`); - }); - - - const finish = Date.now(); - const seconds = Math.round((Date.now() - startTime) / 100) / 10; - console.log(`\nBuild ${BUILD_VERSION} is done in ${seconds} seconds.`); -})(); - -function rmAsync(dirPath) { - const rimraf = require('rimraf'); - return new Promise((resolve, reject) => { - rimraf(dirPath, err => { - if (err) - reject(err); - else - resolve(); - }); - }); -} - -async function cpAsync(from, to) { - const ncp = require('ncp').ncp; - return new Promise((resolve, reject) => { - ncp(from, to, err => { - if (err) - reject(err); - else - resolve(); - }); - }); -} - -async function writeAsync(path, content) { - return new Promise((resolve, reject) => { - fs.writeFile(INDEX_PATH, INDEX_CONTENT, 'utf8', err => { - if (err) - reject(err); - else - resolve(); - }); - }); -} - -async function step(name, callback) { - console.time(name); - await callback(); - console.timeEnd(name); -} - -async function globAsync(cwd, pattern) { - const glob = require('glob'); - return new Promise((resolve, reject) => { - glob(pattern, {cwd}, (err, files) => { - if (err) - reject(err); - else - resolve(files.map(file => path.join(cwd, file))); - }); - }); -} - -async function generateVersion() { - // Version consists of semver and commit SHA. - const {stdout} = await execAsync('git log -n 1 --pretty=format:%h'); - const semver = require('./package.json').version; - return semver + '+' + stdout; -} - -async function execAsync(command) { - const {exec} = require('child_process'); - return new Promise((resolve, reject) => { - exec(command, (err, stdout, stderr) => { - if (err !== null) - reject(err); - else - resolve({stdout, stderr}); - }); - }); -} diff --git a/cjs-entry-core.js b/cjs-entry-core.js new file mode 100644 index 0000000..446726f --- /dev/null +++ b/cjs-entry-core.js @@ -0,0 +1,29 @@ +/** + * 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. + */ + +/** + * We use `export default puppeteer` in `src/index.ts` to expose the library But + * TypeScript in CJS mode compiles that to `exports.default = `. This means that + * our CJS Node users would have to use `require('puppeteer').default` which + * isn't very nice. + * + * So instead we expose this file as our entry point. This requires the compiled + * Puppeteer output and re-exports the `default` export via `module.exports.` + * This means that we can publish to CJS and ESM whilst maintaining the expected + * import behaviour for CJS and ESM users. + */ +const puppeteerExport = require('./lib/cjs/puppeteer/node-puppeteer-core'); +module.exports = puppeteerExport.default; diff --git a/cjs-entry.js b/cjs-entry.js new file mode 100644 index 0000000..d1840a9 --- /dev/null +++ b/cjs-entry.js @@ -0,0 +1,29 @@ +/** + * 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. + */ + +/** + * We use `export default puppeteer` in `src/index.ts` to expose the library But + * TypeScript in CJS mode compiles that to `exports.default = `. This means that + * our CJS Node users would have to use `require('puppeteer').default` which + * isn't very nice. + * + * So instead we expose this file as our entry point. This requires the compiled + * Puppeteer output and re-exports the `default` export via `module.exports.` + * This means that we can publish to CJS and ESM whilst maintaining the expected + * import behaviour for CJS and ESM users. + */ +const puppeteerExport = require('./lib/cjs/puppeteer/node'); +module.exports = puppeteerExport.default; diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..2e216cd --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,22 @@ +/** + * 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 = { + extends: ['@commitlint/config-conventional'], + rules: { + 'body-max-line-length': [0, 'always', 100], + 'footer-max-line-length': [0, 'always', 100], + }, +}; diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 6c2b13e..0000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -pptr.dev \ No newline at end of file diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..bcb114e --- /dev/null +++ b/docs/api.md @@ -0,0 +1,4767 @@ +# Puppeteer API Tip-Of-Tree + + + +- Interactive Documentation: https://pptr.dev +- API Translations: [中文|Chinese](https://zhaoqize.github.io/puppeteer-api-zh_CN/#/) +- Troubleshooting: [troubleshooting.md](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) + + + + +- Releases per Chromium version: + * Chromium 91.0.4469.0 - [Puppeteer v9.0.0](https://github.com/puppeteer/puppeteer/blob/v9.0.0/docs/api.md) + * Chromium 90.0.4427.0 - [Puppeteer v8.0.0](https://github.com/puppeteer/puppeteer/blob/v8.0.0/docs/api.md) + * Chromium 90.0.4403.0 - [Puppeteer v7.0.0](https://github.com/puppeteer/puppeteer/blob/v7.0.0/docs/api.md) + * Chromium 89.0.4389.0 - [Puppeteer v6.0.0](https://github.com/puppeteer/puppeteer/blob/v6.0.0/docs/api.md) + * Chromium 88.0.4298.0 - [Puppeteer v5.5.0](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md) + * Chromium 87.0.4272.0 - [Puppeteer v5.4.0](https://github.com/puppeteer/puppeteer/blob/v5.4.0/docs/api.md) + * Chromium 86.0.4240.0 - [Puppeteer v5.3.0](https://github.com/puppeteer/puppeteer/blob/v5.3.0/docs/api.md) + * Chromium 85.0.4182.0 - [Puppeteer v5.2.1](https://github.com/puppeteer/puppeteer/blob/v5.2.1/docs/api.md) + * Chromium 84.0.4147.0 - [Puppeteer v5.1.0](https://github.com/puppeteer/puppeteer/blob/v5.1.0/docs/api.md) + * Chromium 83.0.4103.0 - [Puppeteer v3.1.0](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md) + * Chromium 81.0.4044.0 - [Puppeteer v3.0.0](https://github.com/puppeteer/puppeteer/blob/v3.0.0/docs/api.md) + * Chromium 80.0.3987.0 - [Puppeteer v2.1.0](https://github.com/puppeteer/puppeteer/blob/v2.1.0/docs/api.md) + * Chromium 79.0.3942.0 - [Puppeteer v2.0.0](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md) + * Chromium 78.0.3882.0 - [Puppeteer v1.20.0](https://github.com/puppeteer/puppeteer/blob/v1.20.0/docs/api.md) + * Chromium 77.0.3803.0 - [Puppeteer v1.19.0](https://github.com/puppeteer/puppeteer/blob/v1.19.0/docs/api.md) + * Chromium 76.0.3803.0 - [Puppeteer v1.17.0](https://github.com/puppeteer/puppeteer/blob/v1.17.0/docs/api.md) + * Chromium 75.0.3765.0 - [Puppeteer v1.15.0](https://github.com/puppeteer/puppeteer/blob/v1.15.0/docs/api.md) + * Chromium 74.0.3723.0 - [Puppeteer v1.13.0](https://github.com/puppeteer/puppeteer/blob/v1.13.0/docs/api.md) + * Chromium 73.0.3679.0 - [Puppeteer v1.12.2](https://github.com/puppeteer/puppeteer/blob/v1.12.2/docs/api.md) + * [All releases](https://github.com/puppeteer/puppeteer/releases) + + + + +##### Table of Contents + + + + +- [Overview](#overview) +- [puppeteer vs puppeteer-core](#puppeteer-vs-puppeteer-core) +- [Environment Variables](#environment-variables) +- [Working with Chrome Extensions](#working-with-chrome-extensions) +- [class: Puppeteer](#class-puppeteer) + * [puppeteer.clearCustomQueryHandlers()](#puppeteerclearcustomqueryhandlers) + * [puppeteer.connect(options)](#puppeteerconnectoptions) + * [puppeteer.createBrowserFetcher([options])](#puppeteercreatebrowserfetcheroptions) + * [puppeteer.customQueryHandlerNames()](#puppeteercustomqueryhandlernames) + * [puppeteer.defaultArgs([options])](#puppeteerdefaultargsoptions) + * [puppeteer.devices](#puppeteerdevices) + * [puppeteer.errors](#puppeteererrors) + * [puppeteer.executablePath()](#puppeteerexecutablepath) + * [puppeteer.launch([options])](#puppeteerlaunchoptions) + * [puppeteer.networkConditions](#puppeteernetworkconditions) + * [puppeteer.product](#puppeteerproduct) + * [puppeteer.registerCustomQueryHandler(name, queryHandler)](#puppeteerregistercustomqueryhandlername-queryhandler) + * [puppeteer.unregisterCustomQueryHandler(name)](#puppeteerunregistercustomqueryhandlername) +- [class: BrowserFetcher](#class-browserfetcher) + * [browserFetcher.canDownload(revision)](#browserfetchercandownloadrevision) + * [browserFetcher.download(revision[, progressCallback])](#browserfetcherdownloadrevision-progresscallback) + * [browserFetcher.host()](#browserfetcherhost) + * [browserFetcher.localRevisions()](#browserfetcherlocalrevisions) + * [browserFetcher.platform()](#browserfetcherplatform) + * [browserFetcher.product()](#browserfetcherproduct) + * [browserFetcher.remove(revision)](#browserfetcherremoverevision) + * [browserFetcher.revisionInfo(revision)](#browserfetcherrevisioninforevision) +- [class: Browser](#class-browser) + * [event: 'disconnected'](#event-disconnected) + * [event: 'targetchanged'](#event-targetchanged) + * [event: 'targetcreated'](#event-targetcreated) + * [event: 'targetdestroyed'](#event-targetdestroyed) + * [browser.browserContexts()](#browserbrowsercontexts) + * [browser.close()](#browserclose) + * [browser.createIncognitoBrowserContext()](#browsercreateincognitobrowsercontext) + * [browser.defaultBrowserContext()](#browserdefaultbrowsercontext) + * [browser.disconnect()](#browserdisconnect) + * [browser.isConnected()](#browserisconnected) + * [browser.newPage()](#browsernewpage) + * [browser.pages()](#browserpages) + * [browser.process()](#browserprocess) + * [browser.target()](#browsertarget) + * [browser.targets()](#browsertargets) + * [browser.userAgent()](#browseruseragent) + * [browser.version()](#browserversion) + * [browser.waitForTarget(predicate[, options])](#browserwaitfortargetpredicate-options) + * [browser.wsEndpoint()](#browserwsendpoint) +- [class: BrowserContext](#class-browsercontext) + * [event: 'targetchanged'](#event-targetchanged-1) + * [event: 'targetcreated'](#event-targetcreated-1) + * [event: 'targetdestroyed'](#event-targetdestroyed-1) + * [browserContext.browser()](#browsercontextbrowser) + * [browserContext.clearPermissionOverrides()](#browsercontextclearpermissionoverrides) + * [browserContext.close()](#browsercontextclose) + * [browserContext.isIncognito()](#browsercontextisincognito) + * [browserContext.newPage()](#browsercontextnewpage) + * [browserContext.overridePermissions(origin, permissions)](#browsercontextoverridepermissionsorigin-permissions) + * [browserContext.pages()](#browsercontextpages) + * [browserContext.targets()](#browsercontexttargets) + * [browserContext.waitForTarget(predicate[, options])](#browsercontextwaitfortargetpredicate-options) +- [class: Page](#class-page) + * [event: 'close'](#event-close) + * [event: 'console'](#event-console) + * [event: 'dialog'](#event-dialog) + * [event: 'domcontentloaded'](#event-domcontentloaded) + * [event: 'error'](#event-error) + * [event: 'frameattached'](#event-frameattached) + * [event: 'framedetached'](#event-framedetached) + * [event: 'framenavigated'](#event-framenavigated) + * [event: 'load'](#event-load) + * [event: 'metrics'](#event-metrics) + * [event: 'pageerror'](#event-pageerror) + * [event: 'popup'](#event-popup) + * [event: 'request'](#event-request) + * [event: 'requestfailed'](#event-requestfailed) + * [event: 'requestfinished'](#event-requestfinished) + * [event: 'response'](#event-response) + * [event: 'workercreated'](#event-workercreated) + * [event: 'workerdestroyed'](#event-workerdestroyed) + * [page.$(selector)](#pageselector) + * [page.$$(selector)](#pageselector-1) + * [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) + * [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args-1) + * [page.$x(expression)](#pagexexpression) + * [page.accessibility](#pageaccessibility) + * [page.addScriptTag(options)](#pageaddscripttagoptions) + * [page.addStyleTag(options)](#pageaddstyletagoptions) + * [page.authenticate(credentials)](#pageauthenticatecredentials) + * [page.bringToFront()](#pagebringtofront) + * [page.browser()](#pagebrowser) + * [page.browserContext()](#pagebrowsercontext) + * [page.click(selector[, options])](#pageclickselector-options) + * [page.close([options])](#pagecloseoptions) + * [page.content()](#pagecontent) + * [page.cookies([...urls])](#pagecookiesurls) + * [page.coverage](#pagecoverage) + * [page.deleteCookie(...cookies)](#pagedeletecookiecookies) + * [page.emulate(options)](#pageemulateoptions) + * [page.emulateIdleState(overrides)](#pageemulateidlestateoverrides) + * [page.emulateMediaFeatures(features)](#pageemulatemediafeaturesfeatures) + * [page.emulateMediaType(type)](#pageemulatemediatypetype) + * [page.emulateNetworkConditions(networkConditions)](#pageemulatenetworkconditionsnetworkconditions) + * [page.emulateTimezone(timezoneId)](#pageemulatetimezonetimezoneid) + * [page.emulateVisionDeficiency(type)](#pageemulatevisiondeficiencytype) + * [page.evaluate(pageFunction[, ...args])](#pageevaluatepagefunction-args) + * [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args) + * [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args) + * [page.exposeFunction(name, puppeteerFunction)](#pageexposefunctionname-puppeteerfunction) + * [page.focus(selector)](#pagefocusselector) + * [page.frames()](#pageframes) + * [page.goBack([options])](#pagegobackoptions) + * [page.goForward([options])](#pagegoforwardoptions) + * [page.goto(url[, options])](#pagegotourl-options) + * [page.hover(selector)](#pagehoverselector) + * [page.isClosed()](#pageisclosed) + * [page.isJavaScriptEnabled()](#pageisjavascriptenabled) + * [page.keyboard](#pagekeyboard) + * [page.mainFrame()](#pagemainframe) + * [page.metrics()](#pagemetrics) + * [page.mouse](#pagemouse) + * [page.pdf([options])](#pagepdfoptions) + * [page.queryObjects(prototypeHandle)](#pagequeryobjectsprototypehandle) + * [page.reload([options])](#pagereloadoptions) + * [page.screenshot([options])](#pagescreenshotoptions) + * [page.select(selector, ...values)](#pageselectselector-values) + * [page.setBypassCSP(enabled)](#pagesetbypasscspenabled) + * [page.setCacheEnabled([enabled])](#pagesetcacheenabledenabled) + * [page.setContent(html[, options])](#pagesetcontenthtml-options) + * [page.setCookie(...cookies)](#pagesetcookiecookies) + * [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) + * [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) + * [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) + * [page.setGeolocation(options)](#pagesetgeolocationoptions) + * [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) + * [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled) + * [page.setRequestInterception(value[, cacheSafe])](#pagesetrequestinterceptionvalue-cachesafe) + * [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) + * [page.setViewport(viewport)](#pagesetviewportviewport) + * [page.tap(selector)](#pagetapselector) + * [page.target()](#pagetarget) + * [page.title()](#pagetitle) + * [page.touchscreen](#pagetouchscreen) + * [page.tracing](#pagetracing) + * [page.type(selector, text[, options])](#pagetypeselector-text-options) + * [page.url()](#pageurl) + * [page.viewport()](#pageviewport) + * [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) + * [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions) + * [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) + * [page.waitForNavigation([options])](#pagewaitfornavigationoptions) + * [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) + * [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options) + * [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) + * [page.waitForTimeout(milliseconds)](#pagewaitfortimeoutmilliseconds) + * [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options) + * [page.workers()](#pageworkers) + * [GeolocationOptions](#geolocationoptions) + * [WaitTimeoutOptions](#waittimeoutoptions) +- [class: WebWorker](#class-webworker) + * [webWorker.evaluate(pageFunction[, ...args])](#webworkerevaluatepagefunction-args) + * [webWorker.evaluateHandle(pageFunction[, ...args])](#webworkerevaluatehandlepagefunction-args) + * [webWorker.executionContext()](#webworkerexecutioncontext) + * [webWorker.url()](#webworkerurl) +- [class: Accessibility](#class-accessibility) + * [accessibility.snapshot([options])](#accessibilitysnapshotoptions) +- [class: Keyboard](#class-keyboard) + * [keyboard.down(key[, options])](#keyboarddownkey-options) + * [keyboard.press(key[, options])](#keyboardpresskey-options) + * [keyboard.sendCharacter(char)](#keyboardsendcharacterchar) + * [keyboard.type(text[, options])](#keyboardtypetext-options) + * [keyboard.up(key)](#keyboardupkey) +- [class: Mouse](#class-mouse) + * [mouse.click(x, y[, options])](#mouseclickx-y-options) + * [mouse.down([options])](#mousedownoptions) + * [mouse.move(x, y[, options])](#mousemovex-y-options) + * [mouse.up([options])](#mouseupoptions) + * [mouse.wheel([options])](#mousewheeloptions) +- [class: Touchscreen](#class-touchscreen) + * [touchscreen.tap(x, y)](#touchscreentapx-y) +- [class: Tracing](#class-tracing) + * [tracing.start([options])](#tracingstartoptions) + * [tracing.stop()](#tracingstop) +- [class: FileChooser](#class-filechooser) + * [fileChooser.accept(filePaths)](#filechooseracceptfilepaths) + * [fileChooser.cancel()](#filechoosercancel) + * [fileChooser.isMultiple()](#filechooserismultiple) +- [class: Dialog](#class-dialog) + * [dialog.accept([promptText])](#dialogacceptprompttext) + * [dialog.defaultValue()](#dialogdefaultvalue) + * [dialog.dismiss()](#dialogdismiss) + * [dialog.message()](#dialogmessage) + * [dialog.type()](#dialogtype) +- [class: ConsoleMessage](#class-consolemessage) + * [consoleMessage.args()](#consolemessageargs) + * [consoleMessage.location()](#consolemessagelocation) + * [consoleMessage.stackTrace()](#consolemessagestacktrace) + * [consoleMessage.text()](#consolemessagetext) + * [consoleMessage.type()](#consolemessagetype) +- [class: Frame](#class-frame) + * [frame.$(selector)](#frameselector) + * [frame.$$(selector)](#frameselector-1) + * [frame.$$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) + * [frame.$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args-1) + * [frame.$x(expression)](#framexexpression) + * [frame.addScriptTag(options)](#frameaddscripttagoptions) + * [frame.addStyleTag(options)](#frameaddstyletagoptions) + * [frame.childFrames()](#framechildframes) + * [frame.click(selector[, options])](#frameclickselector-options) + * [frame.content()](#framecontent) + * [frame.evaluate(pageFunction[, ...args])](#frameevaluatepagefunction-args) + * [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args) + * [frame.executionContext()](#frameexecutioncontext) + * [frame.focus(selector)](#framefocusselector) + * [frame.goto(url[, options])](#framegotourl-options) + * [frame.hover(selector)](#framehoverselector) + * [frame.isDetached()](#frameisdetached) + * [frame.name()](#framename) + * [frame.parentFrame()](#frameparentframe) + * [frame.select(selector, ...values)](#frameselectselector-values) + * [frame.setContent(html[, options])](#framesetcontenthtml-options) + * [frame.tap(selector)](#frametapselector) + * [frame.title()](#frametitle) + * [frame.type(selector, text[, options])](#frametypeselector-text-options) + * [frame.url()](#frameurl) + * [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args) + * [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args) + * [frame.waitForNavigation([options])](#framewaitfornavigationoptions) + * [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) + * [frame.waitForTimeout(milliseconds)](#framewaitfortimeoutmilliseconds) + * [frame.waitForXPath(xpath[, options])](#framewaitforxpathxpath-options) +- [class: ExecutionContext](#class-executioncontext) + * [executionContext.evaluate(pageFunction[, ...args])](#executioncontextevaluatepagefunction-args) + * [executionContext.evaluateHandle(pageFunction[, ...args])](#executioncontextevaluatehandlepagefunction-args) + * [executionContext.frame()](#executioncontextframe) + * [executionContext.queryObjects(prototypeHandle)](#executioncontextqueryobjectsprototypehandle) +- [class: JSHandle](#class-jshandle) + * [jsHandle.asElement()](#jshandleaselement) + * [jsHandle.dispose()](#jshandledispose) + * [jsHandle.evaluate(pageFunction[, ...args])](#jshandleevaluatepagefunction-args) + * [jsHandle.evaluateHandle(pageFunction[, ...args])](#jshandleevaluatehandlepagefunction-args) + * [jsHandle.executionContext()](#jshandleexecutioncontext) + * [jsHandle.getProperties()](#jshandlegetproperties) + * [jsHandle.getProperty(propertyName)](#jshandlegetpropertypropertyname) + * [jsHandle.jsonValue()](#jshandlejsonvalue) +- [class: ElementHandle](#class-elementhandle) + * [elementHandle.$(selector)](#elementhandleselector) + * [elementHandle.$$(selector)](#elementhandleselector-1) + * [elementHandle.$$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args) + * [elementHandle.$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args-1) + * [elementHandle.$x(expression)](#elementhandlexexpression) + * [elementHandle.asElement()](#elementhandleaselement) + * [elementHandle.boundingBox()](#elementhandleboundingbox) + * [elementHandle.boxModel()](#elementhandleboxmodel) + * [elementHandle.click([options])](#elementhandleclickoptions) + * [elementHandle.contentFrame()](#elementhandlecontentframe) + * [elementHandle.dispose()](#elementhandledispose) + * [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args) + * [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args) + * [elementHandle.executionContext()](#elementhandleexecutioncontext) + * [elementHandle.focus()](#elementhandlefocus) + * [elementHandle.getProperties()](#elementhandlegetproperties) + * [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname) + * [elementHandle.hover()](#elementhandlehover) + * [elementHandle.isIntersectingViewport()](#elementhandleisintersectingviewport) + * [elementHandle.jsonValue()](#elementhandlejsonvalue) + * [elementHandle.press(key[, options])](#elementhandlepresskey-options) + * [elementHandle.screenshot([options])](#elementhandlescreenshotoptions) + * [elementHandle.select(...values)](#elementhandleselectvalues) + * [elementHandle.tap()](#elementhandletap) + * [elementHandle.toString()](#elementhandletostring) + * [elementHandle.type(text[, options])](#elementhandletypetext-options) + * [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) +- [class: HTTPRequest](#class-httprequest) + * [httpRequest.abort([errorCode])](#httprequestaborterrorcode) + * [httpRequest.continue([overrides])](#httprequestcontinueoverrides) + * [httpRequest.failure()](#httprequestfailure) + * [httpRequest.frame()](#httprequestframe) + * [httpRequest.headers()](#httprequestheaders) + * [httpRequest.isNavigationRequest()](#httprequestisnavigationrequest) + * [httpRequest.method()](#httprequestmethod) + * [httpRequest.postData()](#httprequestpostdata) + * [httpRequest.redirectChain()](#httprequestredirectchain) + * [httpRequest.resourceType()](#httprequestresourcetype) + * [httpRequest.respond(response)](#httprequestrespondresponse) + * [httpRequest.response()](#httprequestresponse) + * [httpRequest.url()](#httprequesturl) +- [class: HTTPResponse](#class-httpresponse) + * [httpResponse.buffer()](#httpresponsebuffer) + * [httpResponse.frame()](#httpresponseframe) + * [httpResponse.fromCache()](#httpresponsefromcache) + * [httpResponse.fromServiceWorker()](#httpresponsefromserviceworker) + * [httpResponse.headers()](#httpresponseheaders) + * [httpResponse.json()](#httpresponsejson) + * [httpResponse.ok()](#httpresponseok) + * [httpResponse.remoteAddress()](#httpresponseremoteaddress) + * [httpResponse.request()](#httpresponserequest) + * [httpResponse.securityDetails()](#httpresponsesecuritydetails) + * [httpResponse.status()](#httpresponsestatus) + * [httpResponse.statusText()](#httpresponsestatustext) + * [httpResponse.text()](#httpresponsetext) + * [httpResponse.url()](#httpresponseurl) +- [class: SecurityDetails](#class-securitydetails) + * [securityDetails.issuer()](#securitydetailsissuer) + * [securityDetails.protocol()](#securitydetailsprotocol) + * [securityDetails.subjectAlternativeNames()](#securitydetailssubjectalternativenames) + * [securityDetails.subjectName()](#securitydetailssubjectname) + * [securityDetails.validFrom()](#securitydetailsvalidfrom) + * [securityDetails.validTo()](#securitydetailsvalidto) +- [class: Target](#class-target) + * [target.browser()](#targetbrowser) + * [target.browserContext()](#targetbrowsercontext) + * [target.createCDPSession()](#targetcreatecdpsession) + * [target.opener()](#targetopener) + * [target.page()](#targetpage) + * [target.type()](#targettype) + * [target.url()](#targeturl) + * [target.worker()](#targetworker) +- [class: CDPSession](#class-cdpsession) + * [cdpSession.connection()](#cdpsessionconnection) + * [cdpSession.detach()](#cdpsessiondetach) + * [cdpSession.send(method[, ...paramArgs])](#cdpsessionsendmethod-paramargs) +- [class: Coverage](#class-coverage) + * [coverage.startCSSCoverage([options])](#coveragestartcsscoverageoptions) + * [coverage.startJSCoverage([options])](#coveragestartjscoverageoptions) + * [coverage.stopCSSCoverage()](#coveragestopcsscoverage) + * [coverage.stopJSCoverage()](#coveragestopjscoverage) +- [class: TimeoutError](#class-timeouterror) +- [class: EventEmitter](#class-eventemitter) + * [eventEmitter.addListener(event, handler)](#eventemitteraddlistenerevent-handler) + * [eventEmitter.emit(event, [eventData])](#eventemitteremitevent-eventdata) + * [eventEmitter.listenerCount(event)](#eventemitterlistenercountevent) + * [eventEmitter.off(event, handler)](#eventemitteroffevent-handler) + * [eventEmitter.on(event, handler)](#eventemitteronevent-handler) + * [eventEmitter.once(event, handler)](#eventemitteronceevent-handler) + * [eventEmitter.removeAllListeners([event])](#eventemitterremovealllistenersevent) + * [eventEmitter.removeListener(event, handler)](#eventemitterremovelistenerevent-handler) +- [interface: CustomQueryHandler](#interface-customqueryhandler) + + + + +### Overview + +Puppeteer is a Node library which provides a high-level API to control Chromium or Chrome over the DevTools Protocol. + +The Puppeteer API is hierarchical and mirrors the browser structure. + +> **NOTE** On the following diagram, faded entities are not currently represented in Puppeteer. + +![puppeteer overview](https://user-images.githubusercontent.com/81942/86137523-ab2ba080-baed-11ea-9d4b-30eda784585a.png) + +- [`Puppeteer`](#class-puppeteer) communicates with the browser using [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). +- [`Browser`](#class-browser) instance can own multiple browser contexts. +- [`BrowserContext`](#class-browsercontext) instance defines a browsing session and can own multiple pages. +- [`Page`](#class-page) has at least one frame: main frame. There might be other frames created by [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) or [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frame) tags. +- [`Frame`](#class-frame) has at least one execution context - the default execution context - where the frame's JavaScript is executed. A Frame might have additional execution contexts that are associated with [extensions](https://developer.chrome.com/extensions). +- [`Worker`](#class-worker) has a single execution context and facilitates interacting with [WebWorkers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). + +(Diagram source: [link](https://docs.google.com/drawings/d/1Q_AM6KYs9kbyLZF-Lpp5mtpAWth73Cq8IKCsWYgi8MM/edit?usp=sharing)) + +### puppeteer vs 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 +Chromium, which it then drives using `puppeteer-core`. Being an end-user product, `puppeteer` supports a bunch of convenient `PUPPETEER_*` env variables to tweak its behavior. + +`puppeteer-core` is a _library_ to help drive anything that supports DevTools protocol. `puppeteer-core` doesn't download Chromium when installed. Being a library, `puppeteer-core` is fully driven +through its programmatic interface and disregards all the `PUPPETEER_*` env variables. + +To sum up, the only differences between `puppeteer-core` and `puppeteer` are: + +- `puppeteer-core` doesn't automatically download Chromium when installed. +- `puppeteer-core` ignores all `PUPPETEER_*` env variables. + +In most cases, you'll be fine using the `puppeteer` package. + +However, you should use `puppeteer-core` if: + +- you're building another end-user product or library atop of DevTools protocol. For example, one might build a PDF generator using `puppeteer-core` and write a custom `install.js` script that downloads [`headless_shell`](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) instead of Chromium to save disk space. +- you're bundling Puppeteer to use in Chrome Extension / browser with the DevTools protocol where downloading an additional Chromium binary is unnecessary. +- you're building a set of tools where `puppeteer-core` is one of the ingredients and you want to postpone `install.js` script execution until Chromium is about to be used. + +When using `puppeteer-core`, remember to change the _include_ line: + +```js +const puppeteer = require('puppeteer-core'); +``` + +You will then need to call [`puppeteer.connect([options])`](#puppeteerconnectoptions) or [`puppeteer.launch([options])`](#puppeteerlaunchoptions) with an explicit `executablePath` option. + +### Environment Variables + +Puppeteer looks for certain [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to aid its operations. +If Puppeteer doesn't find them in the environment during the installation step, a lowercased variant of these variables will be used from the [npm config](https://docs.npmjs.com/cli/config). + +- `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` - defines HTTP proxy settings that are used to download and run Chromium. +- `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` - do not download bundled Chromium during installation step. +- `PUPPETEER_DOWNLOAD_HOST` - overwrite URL prefix that is used to download Chromium. Note: this includes protocol and might even include path prefix. Defaults to `https://storage.googleapis.com`. +- `PUPPETEER_DOWNLOAD_PATH` - overwrite the path for the downloads folder. Defaults to `/.local-chromium`, where `` is Puppeteer's package root. +- `PUPPETEER_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Puppeteer to use. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. +- `PUPPETEER_EXECUTABLE_PATH` - specify an executable path to be used in `puppeteer.launch`. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how the executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. +- `PUPPETEER_PRODUCT` - specify which browser you'd like Puppeteer to use. Must be one of `chrome` or `firefox`. This can also be used during installation to fetch the recommended browser binary. Setting `product` programmatically in [puppeteer.launch([options])](#puppeteerlaunchoptions) supersedes this environment variable. The product is exposed in [`puppeteer.product`](#puppeteerproduct) + +> **NOTE** `PUPPETEER_*` env variables are not accounted for in the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package. + +### Working with Chrome Extensions + +Puppeteer can be used for testing Chrome Extensions. + +> **NOTE** Extensions in Chrome / Chromium currently only work in non-headless mode. + +The following is code for getting a handle to the [background page](https://developer.chrome.com/extensions/background_pages) of an extension whose source is located in `./my-extension`: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const pathToExtension = require('path').join(__dirname, 'my-extension'); + const browser = await puppeteer.launch({ + headless: false, + args: [ + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}`, + ], + }); + const targets = await browser.targets(); + const backgroundPageTarget = targets.find( + (target) => target.type() === 'background_page' + ); + const backgroundPage = await backgroundPageTarget.page(); + // Test the background page as you would any other page. + await browser.close(); +})(); +``` + +> **NOTE** It is not yet possible to test extension popups or content scripts. + +### class: Puppeteer + +Puppeteer module provides a method to launch a Chromium instance. +The following is a typical example of using Puppeteer to drive automation: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +#### puppeteer.clearCustomQueryHandlers() + +Clears all registered handlers. + +#### puppeteer.connect(options) + +- `options` <[Object]> + - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserURL` a browser URL to connect to, in format `http://${host}:${port}`. Use interchangeably with `browserWSEndpoint` to let Puppeteer fetch it from [metadata endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). + - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. + - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `width` <[number]> page width in pixels. + - `height` <[number]> page height in pixels. + - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as DPR). Defaults to `1`. + - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`. + - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` + - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. + - `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on. + - `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Puppeteer to use. + - `product` <[string]> Possible values are: `chrome`, `firefox`. Defaults to `chrome`. + - `targetFilter` Use this function to decide if Puppeteer should connect to the given target. If a `targetFilter` is provided, Puppeteer only connects to targets for which `targetFilter` returns `true`. By default, Puppeteer connects to all available targets. +- returns: <[Promise]<[Browser]>> + +This methods attaches Puppeteer to an existing browser instance. + +#### puppeteer.createBrowserFetcher([options]) + +- `options` <[Object]> + - `host` <[string]> A download host to be used. Defaults to `https://storage.googleapis.com`. If the `product` is `firefox`, this defaults to `https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central`. + - `path` <[string]> A path for the downloads folder. Defaults to `/.local-chromium`, where `` is Puppeteer's package root. If the `product` is `firefox`, this defaults to `/.local-firefox`. + - `platform` <"linux"|"mac"|"win32"|"win64"> [string] for the current platform. Possible values are: `mac`, `win32`, `win64`, `linux`. Defaults to the current platform. + - `product` <"chrome"|"firefox"> [string] for the product to run. Possible values are: `chrome`, `firefox`. Defaults to `chrome`. +- returns: <[BrowserFetcher]> + +#### puppeteer.customQueryHandlerNames() + +- returns: <[Array]> A list with the names of all registered custom query handlers. + +#### puppeteer.defaultArgs([options]) + +- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: + - `headless` <[boolean]> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. + - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). + - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). + - `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. +- returns: <[Array]<[string]>> + +The default flags that Chromium will be launched with. + +#### puppeteer.devices + +- returns: <[Object]> + +Returns a list of devices to be used with [`page.emulate(options)`](#pageemulateoptions). Actual list of +devices can be found in [`src/common/DeviceDescriptors.ts`](https://github.com/puppeteer/puppeteer/blob/main/src/common/DeviceDescriptors.ts). + +```js +const puppeteer = require('puppeteer'); +const iPhone = puppeteer.devices['iPhone 6']; + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.emulate(iPhone); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +#### puppeteer.errors + +- returns: <[Object]> + - `TimeoutError` <[function]> A class of [TimeoutError]. + +Puppeteer methods might throw errors if they are unable to fulfill a request. For example, [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) +might fail if the selector doesn't match any nodes during the given timeframe. + +For certain types of errors Puppeteer uses specific error classes. +These classes are available via [`puppeteer.errors`](#puppeteererrors) + +An example of handling a timeout error: + +```js +try { + await page.waitForSelector('.foo'); +} catch (e) { + if (e instanceof puppeteer.errors.TimeoutError) { + // Do something if this is a timeout. + } +} +``` + +> **NOTE** The old way (Puppeteer versions <= v1.14.0) errors can be obtained with `require('puppeteer/Errors')`. + +#### puppeteer.executablePath() + +- returns: <[string]> A path where Puppeteer expects to find the bundled browser. The browser binary might not be there if the download was skipped with [`PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`](#environment-variables). + +> **NOTE** `puppeteer.executablePath()` is affected by the `PUPPETEER_EXECUTABLE_PATH` and `PUPPETEER_CHROMIUM_REVISION` env variables. See [Environment Variables](#environment-variables) for details. + +#### puppeteer.launch([options]) + +- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: + - `product` <[string]> Which browser to launch. At this time, this is either `chrome` or `firefox`. See also `PUPPETEER_PRODUCT`. + - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. + - `headless` <[boolean]> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. + - `executablePath` <[string]> Path to a browser executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. + - `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on. + - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `width` <[number]> page width in pixels. + - `height` <[number]> page height in pixels. + - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as DPR). Defaults to `1`. + - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`. + - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` + - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. + - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/), and here is the list of [Firefox flags](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options). + - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use [`puppeteer.defaultArgs()`](#puppeteerdefaultargsoptions). If an array is given, then filter out the given default arguments. Dangerous option; use with care. Defaults to `false`. + - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. + - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. + - `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`. + - `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. + - `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`. + - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). + - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`. + - `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + - `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`. + - `extraPrefsFirefox` <[Object]> Additional [preferences](https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/Preference_reference) that can be passed to Firefox (see `PUPPETEER_PRODUCT`) + - `targetFilter` Use this function to decide if Puppeteer should connect to the given target. If a `targetFilter` is provided, Puppeteer only connects to targets for which `targetFilter` returns `true`. By default, Puppeteer connects to all available targets. + - `waitForInitialPage` <[boolean]> Whether to wait for the initial page to be ready. Defaults to `true`. +- returns: <[Promise]<[Browser]>> Promise which resolves to browser instance. + +You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: + +```js +const browser = await puppeteer.launch({ + ignoreDefaultArgs: ['--mute-audio'], +}); +``` + +> **NOTE** Puppeteer can also be used to control the Chrome browser, but it works best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use `executablePath` option with extreme caution. +> +> If Google Chrome (rather than Chromium) is preferred, a [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested. +> +> In [puppeteer.launch([options])](#puppeteerlaunchoptions) above, any mention of Chromium also applies to Chrome. +> +> 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/+/lkgr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. + +#### puppeteer.networkConditions + +- returns: <[Object]> + +Returns a list of network conditions to be used with [`page.emulateNetworkConditions(networkConditions)`](#pageemulatenetworkconditionsnetworkconditions). Actual list of +conditions can be found in [`src/common/NetworkConditions.ts`](https://github.com/puppeteer/puppeteer/blob/main/src/common/NetworkConditions.ts). + +```js +const puppeteer = require('puppeteer'); +const slow3G = puppeteer.networkConditions['Slow 3G']; + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.emulateNetworkConditions(slow3G); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +#### puppeteer.product + +- returns: <[string]> returns the name of the browser that is under automation (`"chrome"` or `"firefox"`) + +The product is set by the `PUPPETEER_PRODUCT` environment variable or the `product` option in [puppeteer.launch([options])](#puppeteerlaunchoptions) and defaults to `chrome`. Firefox support is experimental and requires to install Puppeteer via `PUPPETEER_PRODUCT=firefox npm i puppeteer`. + +#### puppeteer.registerCustomQueryHandler(name, queryHandler) + +- `name` <[string]> The name that the custom query handler will be registered under. +- `queryHandler` <[CustomQueryHandler]> The [custom query handler](#interface-customqueryhandler) to register. + +Registers a [custom query handler](#interface-customqueryhandler). After registration, the handler can be used everywhere where a selector is expected by prepending the selection string with `/`. The name is only allowed to consist of lower and upper case Latin letters. + +Example: + +```js +puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return element.querySelector(`.${selector}`); + }, + queryAll: (element, selector) => { + return element.querySelectorAll(`.${selector}`); + }, +}); +const aHandle = await page.$('getByClass/…'); +``` + +#### puppeteer.unregisterCustomQueryHandler(name) + +- `name` <[string]> The name of the query handler to unregister. + +### class: BrowserFetcher + +BrowserFetcher can download and manage different versions of Chromium and Firefox. + +BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from [omahaproxy.appspot.com](http://omahaproxy.appspot.com/). + +In the Firefox case, BrowserFetcher downloads Firefox Nightly and operates on version numbers such as `"75"`. + +An example of using BrowserFetcher to download a specific version of Chromium and running +Puppeteer against it: + +```js +const browserFetcher = puppeteer.createBrowserFetcher(); +const revisionInfo = await browserFetcher.download('533271'); +const browser = await puppeteer.launch({ + executablePath: revisionInfo.executablePath, +}); +``` + +> **NOTE** BrowserFetcher is not designed to work concurrently with other +> instances of BrowserFetcher that share the same downloads directory. + +#### browserFetcher.canDownload(revision) + +- `revision` <[string]> a revision to check availability. +- returns: <[Promise]<[boolean]>> returns `true` if the revision could be downloaded from the host. + +The method initiates a HEAD request to check if the revision is available. + +#### browserFetcher.download(revision[, progressCallback]) + +- `revision` <[string]> a revision to download. +- `progressCallback` <[function]([number], [number])> A function that will be called with two arguments: + - `downloadedBytes` <[number]> how many bytes have been downloaded + - `totalBytes` <[number]> how large is the total download +- returns: <[Promise]<[Object]>> Resolves with revision information when the revision is downloaded and extracted + - `revision` <[string]> the revision the info was created from + - `folderPath` <[string]> path to the extracted revision folder + - `executablePath` <[string]> path to the revision executable + - `url` <[string]> URL this revision can be downloaded from + - `local` <[boolean]> whether the revision is locally available on disk + +The method initiates a GET request to download the revision from the host. + +#### browserFetcher.host() + +- returns: <[string]> The download host being used. + +#### browserFetcher.localRevisions() + +- returns: <[Promise]<[Array]<[string]>>> A list of all revisions (for the current `product`) available locally on disk. + +#### browserFetcher.platform() + +- returns: <[string]> One of `mac`, `linux`, `win32` or `win64`. + +#### browserFetcher.product() + +- returns: <[string]> One of `chrome` or `firefox`. + +#### browserFetcher.remove(revision) + +- `revision` <[string]> a revision to remove for the current `product`. The method will throw if the revision has not been downloaded. +- returns: <[Promise]> Resolves when the revision has been removed. + +#### browserFetcher.revisionInfo(revision) + +- `revision` <[string]> a revision to get info for. +- returns: <[Object]> + - `revision` <[string]> the revision the info was created from + - `folderPath` <[string]> path to the extracted revision folder + - `executablePath` <[string]> path to the revision executable + - `url` <[string]> URL this revision can be downloaded from + - `local` <[boolean]> whether the revision is locally available on disk + - `product` <[string]> one of `chrome` or `firefox` + +> **NOTE** Many BrowserFetcher methods, like `remove` and `revisionInfo` +> are affected by the choice of `product`. See [puppeteer.createBrowserFetcher([options])](#puppeteercreatebrowserfetcheroptions). + +### class: Browser + +- extends: [EventEmitter](#class-eventemitter) + +A Browser is created when Puppeteer connects to a Chromium instance, either through [`puppeteer.launch`](#puppeteerlaunchoptions) or [`puppeteer.connect`](#puppeteerconnectoptions). + +An example of using a [Browser] to create a [Page]: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await browser.close(); +})(); +``` + +An example of disconnecting from and reconnecting to a [Browser]: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + // Store the endpoint to be able to reconnect to Chromium + const browserWSEndpoint = browser.wsEndpoint(); + // Disconnect puppeteer from Chromium + browser.disconnect(); + + // Use the endpoint to reestablish a connection + const browser2 = await puppeteer.connect({ browserWSEndpoint }); + // Close Chromium + await browser2.close(); +})(); +``` + +#### event: 'disconnected' + +Emitted when Puppeteer gets disconnected from the Chromium instance. This might happen because of one of the following: + +- Chromium is closed or crashed +- The [`browser.disconnect`](#browserdisconnect) method was called + +#### event: 'targetchanged' + +- <[Target]> + +Emitted when the URL of a target changes. + +> **NOTE** This includes target changes in incognito browser contexts. + +#### event: 'targetcreated' + +- <[Target]> + +Emitted when a target is created, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browser.newPage`](#browsernewpage). + +> **NOTE** This includes target creations in incognito browser contexts. + +#### event: 'targetdestroyed' + +- <[Target]> + +Emitted when a target is destroyed, for example when a page is closed. + +> **NOTE** This includes target destructions in incognito browser contexts. + +#### browser.browserContexts() + +- returns: <[Array]<[BrowserContext]>> + +Returns an array of all open browser contexts. In a newly created browser, this will return +a single instance of [BrowserContext]. + +#### browser.close() + +- returns: <[Promise]> + +Closes Chromium and all of its pages (if any were opened). The [Browser] object itself is considered to be disposed and cannot be used anymore. + +#### browser.createIncognitoBrowserContext() + +- returns: <[Promise]<[BrowserContext]>> + +Creates a new incognito browser context. This won't share cookies/cache with other browser contexts. + +```js +(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'); +})(); +``` + +#### browser.defaultBrowserContext() + +- returns: <[BrowserContext]> + +Returns the default browser context. The default browser context can not be closed. + +#### browser.disconnect() + +Disconnects Puppeteer from the browser but leaves the Chromium process running. After calling `disconnect`, the [Browser] object is considered disposed and cannot be used anymore. + +#### browser.isConnected() + +- returns: <[boolean]> + +Indicates that the browser is connected. + +#### browser.newPage() + +- returns: <[Promise]<[Page]>> + +Promise which resolves to a new [Page] object. The [Page] is created in a default browser context. + +#### browser.pages() + +- returns: <[Promise]<[Array]<[Page]>>> 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 [target.page()](#targetpage). + +An array of all pages inside the Browser. In case of multiple browser contexts, +the method will return an array with all the pages in all browser contexts. + +#### browser.process() + +- returns: Spawned browser process. Returns `null` if the browser instance was created with [`puppeteer.connect`](#puppeteerconnectoptions) method. + +#### browser.target() + +- returns: <[Target]> + +A target associated with the browser. + +#### browser.targets() + +- returns: <[Array]<[Target]>> + +An array of all active targets inside the Browser. In case of multiple browser contexts, +the method will return an array with all the targets in all browser contexts. + +#### browser.userAgent() + +- returns: <[Promise]<[string]>> Promise which resolves to the browser's original user agent. + +> **NOTE** Pages can override browser user agent with [page.setUserAgent](#pagesetuseragentuseragent) + +#### browser.version() + +- returns: <[Promise]<[string]>> For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is similar to `Chrome/61.0.3153.0`. + +> **NOTE** the format of browser.version() might change with future releases of Chromium. + +#### browser.waitForTarget(predicate[, options]) + +- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target +- `options` <[Object]> + - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. +- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. + +This searches for a target in all browser contexts. + +An example of finding a target for a page opened via `window.open`: + +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browser.waitForTarget( + (target) => target.url() === 'https://www.example.com/' +); +``` + +#### browser.wsEndpoint() + +- returns: <[string]> Browser websocket URL. + +Browser websocket endpoint which can be used as an argument to +[puppeteer.connect](#puppeteerconnectoptions). The format is `ws://${host}:${port}/devtools/browser/` + +You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`. Learn more about the [devtools protocol](https://chromedevtools.github.io/devtools-protocol) and the [browser endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). + +### class: BrowserContext + +- extends: [EventEmitter](#class-eventemitter) + +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 `browser.newPage()` creates a page in the default browser context. + +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 `browser.createIncognitoBrowserContext()` method. +"Incognito" browser contexts don't write any browsing data to disk. + +```js +// 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(); +``` + +#### event: 'targetchanged' + +- <[Target]> + +Emitted when the URL of a target inside the browser context changes. + +#### event: 'targetcreated' + +- <[Target]> + +Emitted when a new target is created inside the browser context, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browserContext.newPage`](#browsercontextnewpage). + +#### event: 'targetdestroyed' + +- <[Target]> + +Emitted when a target inside the browser context is destroyed, for example when a page is closed. + +#### browserContext.browser() + +- returns: <[Browser]> + +The browser this browser context belongs to. + +#### browserContext.clearPermissionOverrides() + +- returns: <[Promise]> + +Clears all permission overrides for the browser context. + +```js +const context = browser.defaultBrowserContext(); +context.overridePermissions('https://example.com', ['clipboard-read']); +// do stuff .. +context.clearPermissionOverrides(); +``` + +#### browserContext.close() + +- returns: <[Promise]> + +Closes the browser context. All the targets that belong to the browser context +will be closed. + +> **NOTE** only incognito browser contexts can be closed. + +#### browserContext.isIncognito() + +- returns: <[boolean]> + +Returns whether BrowserContext is incognito. +The default browser context is the only non-incognito browser context. + +> **NOTE** the default browser context cannot be closed. + +#### browserContext.newPage() + +- returns: <[Promise]<[Page]>> + +Creates a new page in the browser context. + +#### browserContext.overridePermissions(origin, permissions) + +- `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". +- `permissions` <[Array]<[string]>> An array of permissions to grant. All permissions that are not listed here will be automatically denied. Permissions can be one of the following values: + - `'geolocation'` + - `'midi'` + - `'midi-sysex'` (system-exclusive midi) + - `'notifications'` + - `'push'` + - `'camera'` + - `'microphone'` + - `'background-sync'` + - `'ambient-light-sensor'` + - `'accelerometer'` + - `'gyroscope'` + - `'magnetometer'` + - `'accessibility-events'` + - `'clipboard-read'` + - `'clipboard-write'` + - `'payment-handler'` +- returns: <[Promise]> + +```js +const context = browser.defaultBrowserContext(); +await context.overridePermissions('https://html5demos.com', ['geolocation']); +``` + +#### browserContext.pages() + +- returns: <[Promise]<[Array]<[Page]>>> 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 [target.page()](#targetpage). + +An array of all pages inside the browser context. + +#### browserContext.targets() + +- returns: <[Array]<[Target]>> + +An array of all active targets inside the browser context. + +#### browserContext.waitForTarget(predicate[, options]) + +- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target +- `options` <[Object]> + - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. +- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. + +This searches for a target in this specific browser context. + +An example of finding a target for a page opened via `window.open`: + +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browserContext.waitForTarget( + (target) => target.url() === 'https://www.example.com/' +); +``` + +### class: Page + +- extends: [EventEmitter](#class-eventemitter) + +Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium. One [Browser] instance might have multiple [Page] instances. + +This example creates a page, navigates it to a URL, and then saves a screenshot: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({ path: 'screenshot.png' }); + await browser.close(); +})(); +``` + +The Page class emits various events (described below) which can be handled using +any of the [`EventEmitter`](#class-eventemitter) methods, such as `on`, `once` +or `off`. + +This example logs a message for a single page `load` event: + +```js +page.once('load', () => console.log('Page loaded!')); +``` + +To unsubscribe from events use the `off` method: + +```js +function logRequest(interceptedRequest) { + console.log('A request was made:', interceptedRequest.url()); +} +page.on('request', logRequest); +// Sometime later... +page.off('request', logRequest); +``` + +#### event: 'close' + +Emitted when the page closes. + +#### event: 'console' + +- <[ConsoleMessage]> + +Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. + +The arguments passed into `console.log` appear as arguments on the event handler. + +An example of handling `console` event: + +```js +page.on('console', (msg) => { + for (let i = 0; i < msg.args().length; ++i) + console.log(`${i}: ${msg.args()[i]}`); +}); +page.evaluate(() => console.log('hello', 5, { foo: 'bar' })); +``` + +#### event: 'dialog' + +- <[Dialog]> + +Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Puppeteer can respond to the dialog via [Dialog]'s [accept](#dialogacceptprompttext) or [dismiss](#dialogdismiss) methods. + +#### event: 'domcontentloaded' + +Emitted when the JavaScript [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched. + +#### event: 'error' + +- <[Error]> + +Emitted when the page crashes. + +> **NOTE** `error` event has a special meaning in Node, see [error events](https://nodejs.org/api/events.html#events_error_events) for details. + +#### event: 'frameattached' + +- <[Frame]> + +Emitted when a frame is attached. + +#### event: 'framedetached' + +- <[Frame]> + +Emitted when a frame is detached. + +#### event: 'framenavigated' + +- <[Frame]> + +Emitted when a frame is navigated to a new URL. + +#### event: 'load' + +Emitted when the JavaScript [`load`](https://developer.mozilla.org/en-US/docs/Web/Events/load) event is dispatched. + +#### event: 'metrics' + +- <[Object]> + - `title` <[string]> The title passed to `console.timeStamp`. + - `metrics` <[Object]> Object containing metrics as key/value pairs. The values + of metrics are of <[number]> type. + +Emitted when the JavaScript code makes a call to `console.timeStamp`. For the list +of metrics see `page.metrics`. + +#### event: 'pageerror' + +- <[Error]> The exception message + +Emitted when an uncaught exception happens within the page. + +#### event: 'popup' + +- <[Page]> Page corresponding to "popup" window + +Emitted when the page opens a new tab or window. + +```js +const [popup] = await Promise.all([ + new Promise((resolve) => page.once('popup', resolve)), + page.click('a[target=_blank]'), +]); +``` + +```js +const [popup] = await Promise.all([ + new Promise((resolve) => page.once('popup', resolve)), + page.evaluate(() => window.open('https://example.com')), +]); +``` + +#### event: 'request' + +- <[HTTPRequest]> + +Emitted when a page issues a request. The [HTTPRequest] object is read-only. +In order to intercept and mutate requests, see `page.setRequestInterception`. + +#### event: 'requestfailed' + +- <[HTTPRequest]> + +Emitted when a request fails, for example by timing out. + +> **NOTE** HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete with [`'requestfinished'`](#event-requestfinished) event and not with [`'requestfailed'`](#event-requestfailed). + +#### event: 'requestfinished' + +- <[HTTPRequest]> + +Emitted when a request finishes successfully. + +#### event: 'response' + +- <[HTTPResponse]> + +Emitted when a [HTTPResponse] is received. + +#### event: 'workercreated' + +- <[WebWorker]> + +Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is spawned by the page. + +#### event: 'workerdestroyed' + +- <[WebWorker]> + +Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated. + +#### page.$(selector) + +- `selector` <[string]> A [selector] to query page for +- returns: <[Promise]> + +The method runs `document.querySelector` within the page. If no element matches the selector, the return value resolves to `null`. + +Shortcut for [page.mainFrame().$(selector)](#frameselector). + +#### page.$$(selector) + +- `selector` <[string]> A [selector] to query page for +- returns: <[Promise]<[Array]<[ElementHandle]>>> + +The method runs `document.querySelectorAll` within the page. If no elements match the selector, the return value resolves to `[]`. + +Shortcut for [page.mainFrame().$$(selector)](#frameselector-1). + +#### page.$$eval(selector, pageFunction[, ...args]) + +- `selector` <[string]> A [selector] to query page for +- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context +- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` + +This method runs `Array.from(document.querySelectorAll(selector))` within the page and passes it as the first argument to `pageFunction`. + +If `pageFunction` returns a [Promise], then `page.$$eval` would wait for the promise to resolve and return its value. + +Examples: + +```js +const divCount = await page.$$eval('div', (divs) => divs.length); +``` + +```js +const options = await page.$$eval('div > span.options', (options) => + options.map((option) => option.textContent) +); +``` + +#### page.$eval(selector, pageFunction[, ...args]) + +- `selector` <[string]> A [selector] to query page for +- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context +- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` + +This method runs `document.querySelector` within the page and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. + +If `pageFunction` returns a [Promise], then `page.$eval` would wait for the promise to resolve and return its value. + +Examples: + +```js +const searchValue = await page.$eval('#search', (el) => el.value); +const preloadHref = await page.$eval('link[rel=preload]', (el) => el.href); +const html = await page.$eval('.main-container', (e) => e.outerHTML); +``` + +Shortcut for [page.mainFrame().$eval(selector, pageFunction)](#frameevalselector-pagefunction-args). + +#### page.$x(expression) + +- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). +- returns: <[Promise]<[Array]<[ElementHandle]>>> + +The method evaluates the XPath expression relative to the page document as its context node. If there are no such elements, the method resolves to an empty array. + +Shortcut for [page.mainFrame().$x(expression)](#framexexpression) + +#### page.accessibility + +- returns: <[Accessibility]> + +#### page.addScriptTag(options) + +- `options` <[Object]> + - `url` <[string]> URL of a script to be added. + - `path` <[string]> Path to the JavaScript file to be injected into frame. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). + - `content` <[string]> Raw JavaScript content to be injected into frame. + - `type` <[string]> Script type. Use 'module' in order to load a JavaScript ES6 module. See [script](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script) for more details. +- returns: <[Promise]<[ElementHandle]>> which resolves to the added tag when the script's onload fires or when the script content was injected into frame. + +Adds a ` - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/index.js b/docs/index.js deleted file mode 100644 index 61dcd13..0000000 --- a/docs/index.js +++ /dev/null @@ -1,194 +0,0 @@ -/* THIS FILE IS GENERATED BY build.js */ - -window.__WEBSITE_VERSION__ = "0.3.7+388f182"; -!function(){"use strict";Element.prototype.scrollIntoViewIfNeeded||(Element.prototype.scrollIntoViewIfNeeded=function(e){e=0===arguments.length||!!e;var t=this.parentNode,r=window.getComputedStyle(t,null),n=parseInt(r.getPropertyValue("border-top-width")),i=parseInt(r.getPropertyValue("border-left-width")),s=this.offsetTop-t.offsetTopt.scrollTop+t.clientHeight,a=this.offsetLeft-t.offsetLeftt.scrollLeft+t.clientWidth,u=s&&!o;(s||o)&&e&&(t.scrollTop=this.offsetTop-t.offsetTop-t.clientHeight/2-n+this.clientHeight/2),(a||l)&&e&&(t.scrollLeft=this.offsetLeft-t.offsetLeft-t.clientWidth/2-i+this.clientWidth/2),(s||o||a||l)&&!e&&this.scrollIntoView(u)});const e=new Map,t=new Set(["async","autofocus","autoplay","checked","contenteditable","controls","default","defer","disabled","formNoValidate","frameborder","hidden","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","readonly","required","reversed","scoped","selected","typemustmatch"]);function r(r,...n){let o=e.get(r);o||(o=function(e){const t=document.createElement("template");let r="";for(let t=0;t1||s.length>1)&&a.push({node:e,nameParts:n,valueParts:s,isSimpleValue:o,attr:r})}else if(e.nodeType===Node.TEXT_NODE&&i.test(e.data)){const t=e.data.split(i);e.data=t[0];const r=e.nextSibling;for(let n=1;n{let t=e[0];for(let r=1;r`,this.element.tabIndex=0}show(e,t){this.element.innerHTML="",this.element.scrollTop=0,this.element.appendChild(e),t&&t.scrollIntoView()}}class a{constructor(){this.element=r``,this.element.addEventListener("click",this._onClick.bind(this),!1),this.glasspane=r``,this.glasspane.addEventListener("click",e=>{this.hideOnMobile(),e.stopPropagation(),e.preventDefault()},!1),this._selectedItem=null}_onClick(e){let t=e.target;for(;t&&t.parentElement!==this.element;)t=t.parentElement;t&&this._selectedItem!==t&&(this.hideOnMobile(),this._selectedItem&&this._selectedItem.classList.remove("selected"),this._selectedItem=t,this._selectedItem.classList.add("selected"))}setElements(e){this.element.innerHTML="";for(const t of e)this.element.appendChild(r` - ${t} - `)}hideOnMobile(){this.element.classList.remove("show-on-mobile"),this.glasspane.classList.remove("show-on-mobile")}toggleOnMobile(){this.element.classList.toggle("show-on-mobile"),this.glasspane.classList.toggle("show-on-mobile")}setSelected(e){if(this._selectedItem&&(this._selectedItem.classList.remove("selected"),this._selectedItem=null),!e)return;const t=e.parentElement;t&&t.parentElement===this.element&&(this._selectedItem=t,this._selectedItem.classList.add("selected"))}}class l{constructor(){this.element=r` - - - - - - `}left(){return this.element.$(".left")}middle(){return this.element.$(".middle")}right(){return this.element.$(".right")}}const u="^[]{}()\\.^$*+?|-,";class c{constructor(e){this._filterRegex=c._createFilterRegex(e),this._query=e,this._queryUpperCase=e.toUpperCase(),this._score=new Int32Array(2e3),this._sequence=new Int32Array(2e3),this._dataUpperCase=""}query(){return this._query}score(e,t){if(!e||!this._query||!this._filterRegex.test(e))return 0;var r=this._query.length,n=e.length;(!this._score||this._score.length=l?(s[o*n+a]=c+1,i[o*n+a]=u+p):(s[o*n+a]=0,i[o*n+a]=l)}return t&&this._restoreMatchIndexes(s,r,n,t),i[r*n-1]}_restoreMatchIndexes(e,t,r,n){for(var i=t-1,s=r-1;i>=0&&s>=0;)switch(e[i*r+s]){case 0:--s;break;default:n.push(s),--i,--s}n.reverse()}_match(e,t,r,n,i){if(this._queryUpperCase[r]!==this._dataUpperCase[n])return 0;var s=10;return e[r]===t[n]&&e[r]===this._queryUpperCase[r]&&(s+=6),i||n&&!/[^\w$]/.test(t[n-1])||(s+=2),s+=4*i}static _createFilterRegex(e){const t=u;let r="";for(let n=0;n - - - - - `,this._contentElement=this.element.$("search-results"),this._items=[],this._visible=!1,this._defaultValue="",this._gotoHomeItem=r`Navigate Home`,this._showOtherItem=r``,this._selectedElement=null,this.input=this.element.$("input"),this.input.addEventListener("keydown",e=>{"Escape"===e.key||27===e.keyCode?(e.preventDefault(),e.stopPropagation(),this.cancelSearch()):"ArrowDown"===e.key?this._selectNext(e):"ArrowUp"===e.key?this._selectPrevious(e):"Enter"===e.key&&(e.preventDefault(),e.stopPropagation(),this._selectedElement,this._selectedElement.click())},!1),this.input.addEventListener("input",()=>{this.search(this.input.value)},!1),this.input.addEventListener("focus",()=>{this._defaultValue=this.input.value},!1),document.addEventListener("keydown",e=>{this.input!==document.activeElement&&(8===e.keyCode||46===e.keyCode?this.input.focus():!/^\S$/.test(e.key)||e.metaKey||e.ctrlKey||e.altKey||(this.input.focus(),"."!==e.key&&(this.input.value="")))},!1),document.addEventListener("paste",e=>{this.input!==document.activeElement&&this.input.focus()},!1),document.addEventListener("click",e=>{if(!this._visible)return;if(this.input.contains(e.target))return;let t=e.target;for(;t&&t.parentElement!==this._contentElement;)t=t.parentElement;if(t)if(t===this._gotoHomeItem)e.preventDefault(),this.cancelSearch(),app.navigateHome();else if(t===this._showOtherItem){for(const e of this._remainingResults){const t=this._renderResult(e);this._contentElement.appendChild(t)}this._selectElement(this._showOtherItem.nextSibling),this._showOtherItem.remove(),this.input.focus(),e.preventDefault()}else e.preventDefault(),this.cancelSearch(),app.navigateURL(t[h._symbol].url());else this.cancelSearch()},!1)}toggleSearch(){this._visible?this.cancelSearch():this.search(this._defaultValue)}setItems(e){this._items=e}setInputValue(e){this.input.value=e,this.input.focus(),this.input.selectionStart=e.length,this.input.selectionEnd=e.length,this._defaultValue=e}search(e){this._setVisible(!0);const t=[];if(this._remainingResults=[],e){const r=new c(e);for(const e of this._items){let n=[],i=r.score(e.text(),n);0!==i&&t.push({item:e,score:i,matches:n})}if(0===t.length)return void(this._contentElement.innerHTML="No Results");t.sort((e,t)=>{const r=t.score-e.score;if(r)return r;const n=e.matches[0]-t.matches[0];return n||e.item.text().length-t.item.text().length})}else for(const e of this._items)t.push({item:e,score:0,matches:[]});this._contentElement.innerHTML="",this._contentElement.scrollTop=0,e||this._contentElement.appendChild(this._gotoHomeItem);for(let e=0;e0&&(this._showOtherItem.textContent=`Show Remaining ${this._remainingResults.length} Results.`,this._contentElement.appendChild(this._showOtherItem)),this._selectElement(this._contentElement.firstChild,!0)}cancelSearch(){this.input.blur(),this._setVisible(!1),this.input.value=this._defaultValue,app.focusContent()}_selectNext(e){if(!this._selectedElement)return;e.preventDefault();let t=this._selectedElement.nextSibling;t||(t=this._contentElement.firstChild),this._selectElement(t)}_selectPrevious(e){if(!this._selectedElement)return;e.preventDefault();let t=this._selectedElement.previousSibling;t||(t=this._contentElement.lastChild),this._selectElement(t)}_selectElement(e,t){this._selectedElement&&this._selectedElement.classList.remove("selected"),this._selectedElement=e,this._selectedElement&&(t||this._selectedElement.scrollIntoViewIfNeeded(!1),this._selectedElement.classList.add("selected"))}_renderResult(e){const t=e.item.iconElement(),n=e.item.titleElement(e.matches),i=e.item.subtitleElement(),s=r` - - ${t?r`${t}`:""} - ${n} - ${i?r`${i}`:""} - - `;return s[h._symbol]=e.item,s}_setVisible(e){if(e!==this._visible)if(this._visible=e,e){const e=this.input.getBoundingClientRect();this.element.style.setProperty("--search-input-x",e.x+"px"),document.body.appendChild(this.element)}else this.element.remove()}}h._symbol=Symbol("SearchComponent._symbol"),h.Item=class{text(){}url(){}iconElement(){}titleElement(e){}subtitleElement(){}};class d{constructor(){this._eventListeners=new Map}on(e,t){let r=this._eventListeners.get(e);return r||(r=new Set,this._eventListeners.set(e,r)),r.add(t),{emitter:this,eventName:e,listener:t}}removeListener(e,t){let r=this._eventListeners.get(e);r&&r.size&&r.delete(t)}emit(e,...t){let r=this._eventListeners.get(e);if(r&&r.size){r=new Set(r);for(const e of r)e.call(null,...t)}}static removeEventListeners(e){for(const t of e)t.emitter.removeListener(t.eventName,t.listener);e.splice(0,e.length)}}class m extends d{constructor(){super(),this.element=r``,this._selectedItem=null,document.body.addEventListener("keydown",e=>{this.element.parentElement&&"Escape"===e.key&&(this.hide(),e.preventDefault(),e.stopPropagation())},!1),this.element.addEventListener("click",()=>this.hide(),!1)}_selectItem(e){this._selectedItem&&this._selectedItem.classList.remove("selected"),this._selectedItem=e,this._selectedItem&&this._selectedItem.classList.add("selected")}show(e,t){this.element.innerHTML="",this.element.appendChild(r` - - -

Settings

- -
- ${e.versionDescriptions().map(n=>{const i=r` - - ${n.name} - ${n.description} - ${function(e){if(!e)return"N/A";const t=e.getDate(),r=e.getMonth(),n=e.getFullYear();return["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][r]+" "+t+", "+n}(n.date)} - - `;return i[m._Symbol]={product:e,versionName:n.name},i})} - - ${e.settingsFooterElement()} - -
WebSite Version:${window.__WEBSITE_VERSION__||"tip-of-tree"} File a bug!
-
-
- `),this.element.$("settings-content").addEventListener("click",e=>{if(e.stopPropagation(),"A"===e.target.tagName)return void e.stopPropagation();if(e.preventDefault(),!window.getSelection().isCollapsed)return;if(e.target.classList.contains("settings-close-icon"))return void this.hide();let t=e.target;for(;t&&"PRODUCT-VERSION"!==t.tagName;)t=t.parentElement;if(!t)return;this._selectItem(t);const{product:r,versionName:n}=t[m._Symbol];this.hide(),this.emit(m.Events.VersionSelected,r,n)},!1),document.body.appendChild(this.element),this._selectedItem=this.element.$("product-version.selected"),this._selectedItem&&this._selectedItem.scrollIntoViewIfNeeded()}hide(){this.element.remove(),this.element.innerHTML="",this._selectedItem=null}}m._Symbol=Symbol("SettingsComponent._Symbol"),m.Events={VersionSelected:"VersionSelected"};class f{constructor(e){this._container=e,this._content=new o,this._sidebar=new a,this._toolbar=new l,this._search=new h,this._settings=new m,this._settings.on(m.Events.VersionSelected,(e,t)=>{this.navigate(t,f.urlContentID())}),this._settingsButton=r` - - - - `,this._settingsButton.addEventListener("click",()=>{this._sidebar.hideOnMobile(),this._settings.show(this._product,this._version)},!1),this._menuButton=r` - - - - `,this._menuButton.addEventListener("click",()=>{this._sidebar.toggleOnMobile()},!1),this._searchButton=r` - - - - `,this._searchButton.addEventListener("click",e=>{this._search.toggleSearch(),this._sidebar.hideOnMobile(),e.stopPropagation(),e.preventDefault()},!1),this._homeButton=r` - - - - `,this._homeButton.addEventListener("click",()=>{this._sidebar.hideOnMobile(),this.navigateURL("")},!1),this._titleElement=r``,this._product=null,this._version=null,window.addEventListener("popstate",this._doNavigation.bind(this),!1)}static urlVersionName(){return new URLSearchParams(window.location.hash.substring(1)).get("version")}static urlContentID(){return new URLSearchParams(window.location.hash.substring(1)).get("show")}_doNavigation(){if(gtag("config","UA-106086244-2",{page_path:window.location.href.substring(window.location.origin.length)}),!this._product)return;this._sidebar.hideOnMobile();const e=f.urlVersionName()||this._product.defaultVersionName();let t=this._version,n=null;this._version&&this._version.name()===e||(t=this._product.getVersion(e)),t?n=t.content(f.urlContentID())||this._product.create404():(t=this._product.getVersion(this._product.defaultVersionName()),n=this._product.create404("Version "+e+" is not found")),this._version=t,this._sidebar.setElements(this._version.sidebarElements()),this._search.setItems(this._version.searchItems()),this._titleElement.textContent="",this._titleElement.appendChild(r` - ${this._product.name()} - ${this._version.name()}Search: - `),this._titleElement.$("app-title-version-name").addEventListener("click",()=>{this._sidebar.hideOnMobile(),this._settings.show(this._product,this._version)},!1),this._sidebar.setSelected(n.selectedSidebarElement),this._search.setInputValue(n.title),this._content.show(n.element,n.scrollAnchor),this._content.element.focus(),n.title?document.title=n.title:document.title=this._product.name()+" "+this._version.name()}initialize(e){this._product=e,this._container.appendChild(this._content.element),this._container.appendChild(this._sidebar.glasspane),this._container.appendChild(this._sidebar.element),this._container.appendChild(this._toolbar.element),this._toolbar.left().appendChild(this._menuButton),this._toolbar.left().appendChild(this._homeButton),this._toolbar.left().appendChild(this._titleElement),this._toolbar.left().appendChild(this._search.input),this._toolbar.middle().appendChild(this._searchButton),this._toolbar.middle().appendChild(this._settingsButton);for(const t of e.toolbarElements())this._toolbar.right().appendChild(t);this._doNavigation()}navigate(e,t){window.location.hash=this.linkURL(this._product.name(),e,t)}navigateHome(){this._version?this.navigate(this._version.name()):this.navigate(this._product.defaultVersionName())}navigateURL(e){window.location=e}linkURL(e,t,r){let n=`#?product=${e}&version=${t}`;return r&&(n+=`&show=${r}`),n}focusContent(){this._content.element.focus()}setLoadingScreen(e,t){this._loadingScreen&&(this._loadingScreen.remove(),this._loadingScreen=null),e&&(this._loadingScreen=r` - - -
${t}
- -
-
-
-
-
-
-
-
-
- `,document.body.appendChild(this._loadingScreen))}}f.Product=class{name(){}toolbarElements(){return[]}defaultVersionName(){}versionDescriptions(){return[]}settingsFooterElement(){}getVersion(e){}},f.ProductVersion=class{name(){}searchItems(){return[]}sidebarElements(){return[]}content(e){return null}};class g{constructor(e="keyval-store",t="keyval"){this.storeName=t,this._dbp=new Promise((r,n)=>{const i=indexedDB.open(e,1);i.onerror=(()=>n(i.error)),i.onsuccess=(()=>r(i.result)),i.onupgradeneeded=(()=>{i.result.createObjectStore(t)})})}_withIDBStore(e,t){return this._dbp.then(r=>new Promise((n,i)=>{const s=r.transaction(this.storeName,e);s.oncomplete=(()=>n()),s.onabort=s.onerror=(()=>i(s.error)),t(s.objectStore(this.storeName))}))}}let b;function v(){return b||(b=new g),b}var E;!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).commonmark=e()}(function(){return function e(t,r,n){function i(o,a){if(!r[o]){if(!t[o]){var l="function"==typeof require&&require;if(!a&&l)return l(o,!0);if(s)return s(o,!0);var u=new Error("Cannot find module '"+o+"'");throw u.code="MODULE_NOT_FOUND",u}var c=r[o]={exports:{}};t[o][0].call(c.exports,function(e){return i(t[o][1][e]||e)},c,c.exports,e,t,r,n)}return r[o].exports}for(var s="function"==typeof require&&require,o=0;o|$)/i,/^/,/\?>/,/>/,/\]\]>/],c=/^(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,}|(?:-[ \t]*){3,})[ \t]*$/,p=/^[#`~*+_=<>0-9-]/,h=/[^ \t\f\v\r\n]/,d=/^[*+-]/,m=/^(\d{1,9})([.)])/,f=/^#{1,6}(?:[ \t]+|$)/,g=/^`{3,}(?!.*`)|^~{3,}(?!.*~)/,b=/^(?:`{3,}|~{3,})(?= *$)/,v=/^(?:=+|-+)[ \t]*$/,E=/\r\n|\n|\r/,C=function(e){return 32===e||9===e},_=function(e,t){return t=t._listData.markerOffset+t._listData.padding))return 1;e.advanceOffset(t._listData.markerOffset+t._listData.padding,!0)}return 0},finalize:function(){},canContain:function(e){return"item"!==e},acceptsLines:!1},heading:{continue:function(){return 1},finalize:function(){},canContain:function(){return!1},acceptsLines:!1},thematic_break:{continue:function(){return 1},finalize:function(){},canContain:function(){return!1},acceptsLines:!1},code_block:{continue:function(e,t){var r=e.currentLine,n=e.indent;if(t._isFenced){var i=n<=3&&r.charAt(e.nextNonspace)===t._fenceChar&&r.slice(e.nextNonspace).match(b);if(i&&i[0].length>=t._fenceLength)return e.finalize(t,e.lineNumber),2;for(var s=t._fenceOffset;s>0&&C(_(r,e.offset));)e.advanceOffset(1,!0),s--}else if(n>=4)e.advanceOffset(4,!0);else{if(!e.blank)return 1;e.advanceNextNonspace()}return 0},finalize:function(e,t){if(t._isFenced){var r=t._string_content,n=r.indexOf("\n"),s=r.slice(0,n),o=r.slice(n+1);t.info=i(s.trim()),t._literal=o}else t._literal=t._string_content.replace(/(\n *)+$/,"\n");t._string_content=null},canContain:function(){return!1},acceptsLines:!0},html_block:{continue:function(e,t){return!e.blank||6!==t._htmlBlockType&&7!==t._htmlBlockType?0:1},finalize:function(e,t){t._literal=t._string_content.replace(/(\n *)+$/,""),t._string_content=null},canContain:function(){return!1},acceptsLines:!0},paragraph:{continue:function(e){return e.blank?1:0},finalize:function(e,t){for(var r,n=!1;91===_(t._string_content,0)&&(r=e.inlineParser.parseReference(t._string_content,e.refmap));)t._string_content=t._string_content.slice(r),n=!0;var i;n&&(i=t._string_content,!h.test(i))&&t.unlink()},canContain:function(){return!1},acceptsLines:!0}},D=[function(e){return e.indented||62!==_(e.currentLine,e.nextNonspace)?0:(e.advanceNextNonspace(),e.advanceOffset(1,!1),C(_(e.currentLine,e.offset))&&e.advanceOffset(1,!0),e.closeUnmatchedBlocks(),e.addChild("block_quote",e.nextNonspace),1)},function(e){var t;if(!e.indented&&(t=e.currentLine.slice(e.nextNonspace).match(f))){e.advanceNextNonspace(),e.advanceOffset(t[0].length,!1),e.closeUnmatchedBlocks();var r=e.addChild("heading",e.nextNonspace);return r.level=t[0].trim().length,r._string_content=e.currentLine.slice(e.offset).replace(/^[ \t]*#+[ \t]*$/,"").replace(/[ \t]+#+[ \t]*$/,""),e.advanceOffset(e.currentLine.length-e.offset),2}return 0},function(e){var t;if(!e.indented&&(t=e.currentLine.slice(e.nextNonspace).match(g))){var r=t[0].length;e.closeUnmatchedBlocks();var n=e.addChild("code_block",e.nextNonspace);return n._isFenced=!0,n._fenceLength=r,n._fenceChar=t[0][0],n._fenceOffset=e.indent,e.advanceNextNonspace(),e.advanceOffset(r,!1),2}return 0},function(e,t){if(!e.indented&&60===_(e.currentLine,e.nextNonspace)){var r,n=e.currentLine.slice(e.nextNonspace);for(r=1;r<=7;r++)if(l[r].test(n)&&(r<7||"paragraph"!==t.type))return e.closeUnmatchedBlocks(),e.addChild("html_block",e.offset)._htmlBlockType=r,2}return 0},function(e,t){var r;if(!e.indented&&"paragraph"===t.type&&(r=e.currentLine.slice(e.nextNonspace).match(v))){e.closeUnmatchedBlocks();var i=new n("heading",t.sourcepos);return i.level="="===r[0][0]?1:2,i._string_content=t._string_content,t.insertAfter(i),t.unlink(),e.tip=i,e.advanceOffset(e.currentLine.length-e.offset,!1),2}return 0},function(e){return!e.indented&&c.test(e.currentLine.slice(e.nextNonspace))?(e.closeUnmatchedBlocks(),e.addChild("thematic_break",e.nextNonspace),e.advanceOffset(e.currentLine.length-e.offset,!1),2):0},function(e,t){var r,n,i;return e.indented&&"list"!==t.type||!(r=function(e,t){var r,n,i,s,o=e.currentLine.slice(e.nextNonspace),a={type:null,tight:!0,bulletChar:null,start:null,delimiter:null,padding:null,markerOffset:e.indent};if(r=o.match(d))a.type="bullet",a.bulletChar=r[0][0];else{if(!(r=o.match(m))||"paragraph"===t.type&&"1"!==r[1])return null;a.type="ordered",a.start=parseInt(r[1]),a.delimiter=r[2]}if(-1!==(n=_(e.currentLine,e.nextNonspace+r[0].length))&&9!==n&&32!==n)return null;if("paragraph"===t.type&&!e.currentLine.slice(e.nextNonspace+r[0].length).match(h))return null;e.advanceNextNonspace(),e.advanceOffset(r[0].length,!0),i=e.column,s=e.offset;do{e.advanceOffset(1,!0),n=_(e.currentLine,e.offset)}while(e.column-i<5&&C(n));var l=-1===_(e.currentLine,e.offset),u=e.column-i;return u>=5||u<1||l?(a.padding=r[0].length+1,e.column=i,e.offset=s,C(_(e.currentLine,e.offset))&&e.advanceOffset(1,!0)):a.padding=r[0].length+u,a}(e,t))?0:(e.closeUnmatchedBlocks(),"list"===e.tip.type&&(n=t._listData,i=r,n.type===i.type&&n.delimiter===i.delimiter&&n.bulletChar===i.bulletChar)||((t=e.addChild("list",e.nextNonspace))._listData=r),(t=e.addChild("item",e.nextNonspace))._listData=r,1)},function(e){return e.indented&&"paragraph"!==e.tip.type&&!e.blank?(e.advanceOffset(4,!0),e.closeUnmatchedBlocks(),e.addChild("code_block",e.offset),2):0}],L=function(e,t){for(var r,n,i,s=this.currentLine;e>0&&(i=s[this.offset]);)"\t"===i?(r=4-this.column%4,t?(this.partiallyConsumedTab=r>e,n=r>e?e:r,this.column+=n,this.offset+=this.partiallyConsumedTab?0:1,e-=n):(this.partiallyConsumedTab=!1,this.column+=r,this.offset+=1,e-=1)):(this.partiallyConsumedTab=!1,this.offset+=1,this.column+=1,e-=1)},S=function(){this.offset=this.nextNonspace,this.column=this.nextNonspaceColumn,this.partiallyConsumedTab=!1},F=function(){for(var e,t=this.currentLine,r=this.offset,n=this.column;""!==(e=t.charAt(r));)if(" "===e)r++,n++;else{if("\t"!==e)break;r++,n+=4-n%4}this.blank="\n"===e||"\r"===e||""===e,this.nextNonspace=r,this.nextNonspaceColumn=n,this.indent=this.nextNonspaceColumn-this.column,this.indented=this.indent>=4},N=function(e){var t,r,n=!0,i=this.doc;for(this.oldtip=this.tip,this.offset=0,this.column=0,this.blank=!1,this.partiallyConsumedTab=!1,this.lineNumber+=1,-1!==e.indexOf("\0")&&(e=e.replace(/\0/g,"�")),this.currentLine=e;(r=i._lastChild)&&r._open;){switch(i=r,this.findNextNonspace(),this.blocks[i.type].continue(this,i)){case 0:break;case 1:n=!1;break;case 2:return void(this.lastLineLength=e.length);default:throw"continue returned illegal value, must be 0, 1, or 2"}if(!n){i=i._parent;break}}this.allClosed=i===this.oldtip,this.lastMatchedContainer=i;for(var s="paragraph"!==i.type&&k[i.type].acceptsLines,o=this.blockStarts,a=o.length;!s;){if(this.findNextNonspace(),!this.indented&&!p.test(e.slice(this.nextNonspace))){this.advanceNextNonspace();break}for(var l=0;l=1&&i._htmlBlockType<=5&&u[i._htmlBlockType].test(this.currentLine.slice(this.offset))&&this.finalize(i,this.lineNumber)):this.offset`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>|]|\x3c!----\x3e|\x3c!--(?:-?[^>-])(?:-?[^-])*--\x3e|[<][?].*?[?][>]|]*>|)","i"),l=/[\\&]/,u="[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]",c=new RegExp("\\\\"+u+"|"+o,"gi"),p=new RegExp('[&<>"]',"g"),h=new RegExp(o+'|[&<>"]',"gi"),d=function(e){return 92===e.charCodeAt(0)?e.charAt(1):s(e)},m=function(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case'"':return""";default:return e}};t.exports={unescapeString:function(e){return l.test(e)?e.replace(c,d):e},normalizeURI:function(e){try{return n(i(e))}catch(t){return e}},escapeXml:function(e,t){return p.test(e)?t?e.replace(h,m):e.replace(p,m):e},reHtmlTag:a,OPENTAG:"<[A-Za-z][A-Za-z0-9-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>",CLOSETAG:"]",ENTITY:o,ESCAPABLE:u}},{entities:11,"mdurl/decode":19,"mdurl/encode":20}],3:[function(e,t,r){if(String.fromCodePoint)t.exports=function(e){try{return String.fromCodePoint(e)}catch(e){if(e instanceof RangeError)return String.fromCharCode(65533);throw e}};else{var n=String.fromCharCode,i=Math.floor;t.exports=function(){var e,t,r=[],s=-1,o=arguments.length;if(!o)return"";for(var a="";++s1114111||i(l)!==l)return String.fromCharCode(65533);l<=65535?r.push(l):(e=55296+((l-=65536)>>10),t=l%1024+56320,r.push(e,t)),(s+1===o||r.length>16384)&&(a+=n.apply(null,r),r.length=0)}return a}}},{}],4:[function(e,t,r){t.exports.Node=e("./node"),t.exports.Parser=e("./blocks"),t.exports.HtmlRenderer=e("./render/html"),t.exports.XmlRenderer=e("./render/xml")},{"./blocks":1,"./node":6,"./render/html":8,"./render/xml":10}],5:[function(e,t,r){var n=e("./node"),i=e("./common"),s=e("./normalize-reference"),o=i.normalizeURI,a=i.unescapeString,l=e("./from-code-point.js"),u=e("entities").decodeHTML;e("string.prototype.repeat");var c=i.ESCAPABLE,p="\\\\"+c,h=i.ENTITY,d=i.reHtmlTag,m=new RegExp(/[!"#$%&'()*+,\-./:;<=>?@\[\]^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]/),f=new RegExp('^(?:"('+p+'|[^"\\x00])*"|\'('+p+"|[^'\\x00])*'|\\(("+p+"|[^)\\x00])*\\))"),g=new RegExp("^(?:[<](?:[^ <>\\t\\n\\\\\\x00]|"+p+"|\\\\)*[>])"),b=new RegExp("^"+c),v=new RegExp("^"+h,"i"),E=/`+/,C=/^`+/,_=/\.\.\./g,y=/--+/g,w=/^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/,A=/^<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>/i,x=/^ *(?:\n *)?/,k=/^[ \t\n\x0b\x0c\x0d]/,D=/[ \t\n\x0b\x0c\x0d]+/g,L=/^\s/,S=/ *$/,F=/^ */,N=/^ *(?:\n|$)/,T=new RegExp("^\\[(?:[^\\\\\\[\\]]|"+p+"|\\\\){0,1000}\\]"),q=/^[^\n`\[\]\\!<&*_'"]+/m,B=function(e){var t=new n("text");return t._literal=e,t},I=function(e){var t=e.exec(this.subject.slice(this.pos));return null===t?null:(this.pos+=t.index+t[0].length,t[0])},O=function(){return this.pos=2&&t.numdelims>=2?2:1,s=t.node,o=r.node,t.numdelims-=l,r.numdelims-=l,s._literal=s._literal.slice(0,s._literal.length-l),o._literal=o._literal.slice(0,o._literal.length-l);var b=new n(1===l?"emph":"strong");for(u=s._next;u&&u!==o;)c=u._next,u.unlink(),b.appendChild(u),u=c;s.insertAfter(b),d=r,(h=t).next!==d&&(h.next=d,d.previous=h),0===t.numdelims&&(s.unlink(),this.removeDelimiter(t)),0===r.numdelims&&(o.unlink(),a=r.next,this.removeDelimiter(r),r=a)}else r=r.next;else 39===g?(r.node._literal="’",p&&(t.node._literal="‘"),r=r.next):34===g&&(r.node._literal="”",p&&(t.node.literal="“"),r=r.next);p||f||(m[g]=i.previous,i.can_open||this.removeDelimiter(i))}else r=r.next}for(;null!==this.delimiters&&this.delimiters!==e;)this.removeDelimiter(this.delimiters)},G=function(){var e=this.match(f);return null===e?null:a(e.substr(1,e.length-2))},W=function(){var e=this.match(g);if(null===e){for(var t,r=this.pos,n=0;-1!==(t=this.peek());)if(92===t)this.pos+=1,-1!==this.peek()&&(this.pos+=1);else if(40===t)this.pos+=1,n+=1;else if(41===t){if(n<1)break;this.pos+=1,n-=1}else{if(null!==k.exec(l(t)))break;this.pos+=1}return e=this.subject.substr(r,this.pos-r),o(a(e))}return o(a(e.substr(1,e.length-2)))},Z=function(){var e=this.match(T);return null===e||e.length>1001||/[^\\]\\\]$/.exec(e)?0:e.length},X=function(e){var t=this.pos;this.pos+=1;var r=B("[");return e.appendChild(r),this.addBracket(r,t,!1),!0},J=function(e){var t=this.pos;if(this.pos+=1,91===this.peek()){this.pos+=1;var r=B("![");e.appendChild(r),this.addBracket(r,t+1,!0)}else e.appendChild(B("!"));return!0},Y=function(e){var t,r,i,o,a,l,u=!1;if(this.pos+=1,t=this.pos,null===(l=this.brackets))return e.appendChild(B("]")),!0;if(!l.active)return e.appendChild(B("]")),this.removeBracket(),!0;r=l.image;var c=this.pos;if(40===this.peek()&&(this.pos++,this.spnl()&&null!==(i=this.parseLinkDestination())&&this.spnl()&&(k.test(this.subject.charAt(this.pos-1))&&(o=this.parseLinkTitle()),1)&&this.spnl()&&41===this.peek()?(this.pos+=1,u=!0):this.pos=c),!u){var p=this.pos,h=this.parseLinkLabel();if(h>2?a=this.subject.slice(p,p+h):l.bracketAfter||(a=this.subject.slice(l.index,t)),0===h&&(this.pos=c),a){var d=this.refmap[s(a)];d&&(i=d.destination,o=d.title,u=!0)}}if(u){var m,f,g=new n(r?"image":"link");for(g._destination=i,g._title=o||"",m=l.node._next;m;)f=m._next,m.unlink(),g.appendChild(m),m=f;if(e.appendChild(g),this.processEmphasis(l.previousDelimiter),this.removeBracket(),l.node.unlink(),!r)for(l=this.brackets;null!==l;)l.image||(l.active=!1),l=l.previous;return!0}return this.removeBracket(),this.pos=t,e.appendChild(B("]")),!0},K=function(e,t,r){null!==this.brackets&&(this.brackets.bracketAfter=!0),this.brackets={node:e,previous:this.brackets,previousDelimiter:this.delimiters,index:t,image:r,active:!0}},Q=function(){this.brackets=this.brackets.previous},ee=function(e){var t;return!!(t=this.match(v))&&(e.appendChild(B(u(t))),!0)},te=function(e){var t;return!!(t=this.match(q))&&(e.appendChild(this.options.smart?B(t.replace(_,"…").replace(y,function(e){var t=0,r=0;return e.length%3==0?r=e.length/3:e.length%2==0?t=e.length/2:e.length%3==2?(t=1,r=(e.length-2)/3):(t=2,r=(e.length-4)/3),"—".repeat(r)+"–".repeat(t)})):B(t)),!0)},re=function(e){this.pos+=1;var t=e._lastChild;if(t&&"text"===t.type&&" "===t._literal[t._literal.length-1]){var r=" "===t._literal[t._literal.length-2];t._literal=t._literal.replace(S,""),e.appendChild(new n(r?"linebreak":"softbreak"))}else e.appendChild(new n("softbreak"));return this.match(F),!0},ne=function(e,t){this.subject=e;var r,n,i,o,a=this.pos=0;if(0===(o=this.parseLinkLabel()))return 0;if(r=this.subject.substr(0,o),58!==this.peek())return this.pos=a,0;if(this.pos++,this.spnl(),null===(n=this.parseLinkDestination())||0===n.length)return this.pos=a,0;var l=this.pos;this.spnl(),null===(i=this.parseLinkTitle())&&(i="",this.pos=l);var u=!0;if(null===this.match(N)&&(""===i?u=!1:(i="",this.pos=l,u=null!==this.match(N))),!u)return this.pos=a,0;var c=s(r);return""===c?(this.pos=a,0):(t[c]||(t[c]={destination:n,title:i}),this.pos-a)},ie=function(e){var t=!1,r=this.peek();if(-1===r)return!1;switch(r){case 10:t=this.parseNewline(e);break;case 92:t=this.parseBackslash(e);break;case 96:t=this.parseBackticks(e);break;case 42:case 95:t=this.handleDelim(r,e);break;case 39:case 34:t=this.options.smart&&this.handleDelim(r,e);break;case 91:t=this.parseOpenBracket(e);break;case 33:t=this.parseBang(e);break;case 93:t=this.parseCloseBracket(e);break;case 60:t=this.parseAutolink(e)||this.parseHtmlTag(e);break;case 38:t=this.parseEntity(e);break;default:t=this.parseString(e)}return t||(this.pos+=1,e.appendChild(B(l(r)))),!0},se=function(e){for(this.subject=e._string_content.trim(),this.pos=0,this.delimiters=null,this.brackets=null;this.parseInline(e););e._string_content=null,this.processEmphasis(null)};t.exports=function(e){return{subject:"",delimiters:null,brackets:null,pos:0,refmap:{},match:I,peek:O,spnl:M,parseBackticks:V,parseBackslash:$,parseAutolink:R,parseHtmlTag:U,scanDelims:j,handleDelim:P,parseLinkTitle:G,parseLinkDestination:W,parseLinkLabel:Z,parseOpenBracket:X,parseBang:J,parseCloseBracket:Y,addBracket:K,removeBracket:Q,parseEntity:ee,parseString:te,parseNewline:re,parseReference:ne,parseInline:ie,processEmphasis:H,removeDelimiter:z,options:e||{},parse:se}}},{"./common":2,"./from-code-point.js":3,"./node":6,"./normalize-reference":7,entities:11,"string.prototype.repeat":21}],6:[function(e,t,r){function n(e){switch(e._type){case"document":case"block_quote":case"list":case"item":case"paragraph":case"heading":case"emph":case"strong":case"link":case"image":case"custom_inline":case"custom_block":return!0;default:return!1}}var i=function(e,t){this.current=e,this.entering=!0===t},s=function(){var e=this.current,t=this.entering;if(null===e)return null;var r=n(e);return t&&r?e._firstChild?(this.current=e._firstChild,this.entering=!0):this.entering=!1:e===this.root?this.current=null:null===e._next?(this.current=e._parent,this.entering=!1):(this.current=e._next,this.entering=!0),{entering:t,node:e}},o=function(e,t){this._type=e,this._parent=null,this._firstChild=null,this._lastChild=null,this._prev=null,this._next=null,this._sourcepos=t,this._lastLineBlank=!1,this._open=!0,this._string_content=null,this._literal=null,this._listData={},this._info=null,this._destination=null,this._title=null,this._isFenced=!1,this._fenceChar=null,this._fenceLength=0,this._fenceOffset=null,this._level=null,this._onEnter=null,this._onExit=null},a=o.prototype;Object.defineProperty(a,"isContainer",{get:function(){return n(this)}}),Object.defineProperty(a,"type",{get:function(){return this._type}}),Object.defineProperty(a,"firstChild",{get:function(){return this._firstChild}}),Object.defineProperty(a,"lastChild",{get:function(){return this._lastChild}}),Object.defineProperty(a,"next",{get:function(){return this._next}}),Object.defineProperty(a,"prev",{get:function(){return this._prev}}),Object.defineProperty(a,"parent",{get:function(){return this._parent}}),Object.defineProperty(a,"sourcepos",{get:function(){return this._sourcepos}}),Object.defineProperty(a,"literal",{get:function(){return this._literal},set:function(e){this._literal=e}}),Object.defineProperty(a,"destination",{get:function(){return this._destination},set:function(e){this._destination=e}}),Object.defineProperty(a,"title",{get:function(){return this._title},set:function(e){this._title=e}}),Object.defineProperty(a,"info",{get:function(){return this._info},set:function(e){this._info=e}}),Object.defineProperty(a,"level",{get:function(){return this._level},set:function(e){this._level=e}}),Object.defineProperty(a,"listType",{get:function(){return this._listData.type},set:function(e){this._listData.type=e}}),Object.defineProperty(a,"listTight",{get:function(){return this._listData.tight},set:function(e){this._listData.tight=e}}),Object.defineProperty(a,"listStart",{get:function(){return this._listData.start},set:function(e){this._listData.start=e}}),Object.defineProperty(a,"listDelimiter",{get:function(){return this._listData.delimiter},set:function(e){this._listData.delimiter=e}}),Object.defineProperty(a,"onEnter",{get:function(){return this._onEnter},set:function(e){this._onEnter=e}}),Object.defineProperty(a,"onExit",{get:function(){return this._onExit},set:function(e){this._onExit=e}}),o.prototype.appendChild=function(e){e.unlink(),e._parent=this,this._lastChild?(this._lastChild._next=e,e._prev=this._lastChild,this._lastChild=e):(this._firstChild=e,this._lastChild=e)},o.prototype.prependChild=function(e){e.unlink(),e._parent=this,this._firstChild?(this._firstChild._prev=e,e._next=this._firstChild,this._firstChild=e):(this._firstChild=e,this._lastChild=e)},o.prototype.unlink=function(){this._prev?this._prev._next=this._next:this._parent&&(this._parent._firstChild=this._next),this._next?this._next._prev=this._prev:this._parent&&(this._parent._lastChild=this._prev),this._parent=null,this._next=null,this._prev=null},o.prototype.insertAfter=function(e){e.unlink(),e._next=this._next,e._next&&(e._next._prev=e),e._prev=this,this._next=e,e._parent=this._parent,e._next||(e._parent._lastChild=e)},o.prototype.insertBefore=function(e){e.unlink(),e._prev=this._prev,e._prev&&(e._prev._next=e),e._next=this,this._prev=e,e._parent=this._parent,e._prev||(e._parent._firstChild=e)},o.prototype.walker=function(){return new function(e){return{current:e,root:e,entering:!0,next:s,resumeAt:i}}(this)},t.exports=o},{}],7:[function(e,t,r){var n=/[ \t\r\n]+|[A-Z\xB5\xC0-\xD6\xD8-\xDF\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u0149\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178\u0179\u017B\u017D\u017F\u0181\u0182\u0184\u0186\u0187\u0189-\u018B\u018E-\u0191\u0193\u0194\u0196-\u0198\u019C\u019D\u019F\u01A0\u01A2\u01A4\u01A6\u01A7\u01A9\u01AC\u01AE\u01AF\u01B1-\u01B3\u01B5\u01B7\u01B8\u01BC\u01C4\u01C5\u01C7\u01C8\u01CA\u01CB\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F0-\u01F2\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A\u023B\u023D\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0345\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03AB\u03B0\u03C2\u03CF-\u03D1\u03D5\u03D6\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F0\u03F1\u03F4\u03F5\u03F7\u03F9\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u0587\u10A0-\u10C5\u10C7\u10CD\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E96-\u1E9B\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F50\u1F52\u1F54\u1F56\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1F80-\u1FAF\u1FB2-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD2\u1FD3\u1FD6-\u1FDB\u1FE2-\u1FE4\u1FE6-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2126\u212A\u212B\u2132\u2160-\u216F\u2183\u24B6-\u24CF\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AD\uA7B0\uA7B1\uFB00-\uFB06\uFB13-\uFB17\uFF21-\uFF3A]|\uD801[\uDC00-\uDC27]|\uD806[\uDCA0-\uDCBF]/g,i={A:"a",B:"b",C:"c",D:"d",E:"e",F:"f",G:"g",H:"h",I:"i",J:"j",K:"k",L:"l",M:"m",N:"n",O:"o",P:"p",Q:"q",R:"r",S:"s",T:"t",U:"u",V:"v",W:"w",X:"x",Y:"y",Z:"z","µ":"μ","À":"à","Á":"á","Â":"â","Ã":"ã","Ä":"ä","Å":"å","Æ":"æ","Ç":"ç","È":"è","É":"é","Ê":"ê","Ë":"ë","Ì":"ì","Í":"í","Î":"î","Ï":"ï","Ð":"ð","Ñ":"ñ","Ò":"ò","Ó":"ó","Ô":"ô","Õ":"õ","Ö":"ö","Ø":"ø","Ù":"ù","Ú":"ú","Û":"û","Ü":"ü","Ý":"ý","Þ":"þ","Ā":"ā","Ă":"ă","Ą":"ą","Ć":"ć","Ĉ":"ĉ","Ċ":"ċ","Č":"č","Ď":"ď","Đ":"đ","Ē":"ē","Ĕ":"ĕ","Ė":"ė","Ę":"ę","Ě":"ě","Ĝ":"ĝ","Ğ":"ğ","Ġ":"ġ","Ģ":"ģ","Ĥ":"ĥ","Ħ":"ħ","Ĩ":"ĩ","Ī":"ī","Ĭ":"ĭ","Į":"į","IJ":"ij","Ĵ":"ĵ","Ķ":"ķ","Ĺ":"ĺ","Ļ":"ļ","Ľ":"ľ","Ŀ":"ŀ","Ł":"ł","Ń":"ń","Ņ":"ņ","Ň":"ň","Ŋ":"ŋ","Ō":"ō","Ŏ":"ŏ","Ő":"ő","Œ":"œ","Ŕ":"ŕ","Ŗ":"ŗ","Ř":"ř","Ś":"ś","Ŝ":"ŝ","Ş":"ş","Š":"š","Ţ":"ţ","Ť":"ť","Ŧ":"ŧ","Ũ":"ũ","Ū":"ū","Ŭ":"ŭ","Ů":"ů","Ű":"ű","Ų":"ų","Ŵ":"ŵ","Ŷ":"ŷ","Ÿ":"ÿ","Ź":"ź","Ż":"ż","Ž":"ž","ſ":"s","Ɓ":"ɓ","Ƃ":"ƃ","Ƅ":"ƅ","Ɔ":"ɔ","Ƈ":"ƈ","Ɖ":"ɖ","Ɗ":"ɗ","Ƌ":"ƌ","Ǝ":"ǝ","Ə":"ə","Ɛ":"ɛ","Ƒ":"ƒ","Ɠ":"ɠ","Ɣ":"ɣ","Ɩ":"ɩ","Ɨ":"ɨ","Ƙ":"ƙ","Ɯ":"ɯ","Ɲ":"ɲ","Ɵ":"ɵ","Ơ":"ơ","Ƣ":"ƣ","Ƥ":"ƥ","Ʀ":"ʀ","Ƨ":"ƨ","Ʃ":"ʃ","Ƭ":"ƭ","Ʈ":"ʈ","Ư":"ư","Ʊ":"ʊ","Ʋ":"ʋ","Ƴ":"ƴ","Ƶ":"ƶ","Ʒ":"ʒ","Ƹ":"ƹ","Ƽ":"ƽ","DŽ":"dž","Dž":"dž","LJ":"lj","Lj":"lj","NJ":"nj","Nj":"nj","Ǎ":"ǎ","Ǐ":"ǐ","Ǒ":"ǒ","Ǔ":"ǔ","Ǖ":"ǖ","Ǘ":"ǘ","Ǚ":"ǚ","Ǜ":"ǜ","Ǟ":"ǟ","Ǡ":"ǡ","Ǣ":"ǣ","Ǥ":"ǥ","Ǧ":"ǧ","Ǩ":"ǩ","Ǫ":"ǫ","Ǭ":"ǭ","Ǯ":"ǯ","DZ":"dz","Dz":"dz","Ǵ":"ǵ","Ƕ":"ƕ","Ƿ":"ƿ","Ǹ":"ǹ","Ǻ":"ǻ","Ǽ":"ǽ","Ǿ":"ǿ","Ȁ":"ȁ","Ȃ":"ȃ","Ȅ":"ȅ","Ȇ":"ȇ","Ȉ":"ȉ","Ȋ":"ȋ","Ȍ":"ȍ","Ȏ":"ȏ","Ȑ":"ȑ","Ȓ":"ȓ","Ȕ":"ȕ","Ȗ":"ȗ","Ș":"ș","Ț":"ț","Ȝ":"ȝ","Ȟ":"ȟ","Ƞ":"ƞ","Ȣ":"ȣ","Ȥ":"ȥ","Ȧ":"ȧ","Ȩ":"ȩ","Ȫ":"ȫ","Ȭ":"ȭ","Ȯ":"ȯ","Ȱ":"ȱ","Ȳ":"ȳ","Ⱥ":"ⱥ","Ȼ":"ȼ","Ƚ":"ƚ","Ⱦ":"ⱦ","Ɂ":"ɂ","Ƀ":"ƀ","Ʉ":"ʉ","Ʌ":"ʌ","Ɇ":"ɇ","Ɉ":"ɉ","Ɋ":"ɋ","Ɍ":"ɍ","Ɏ":"ɏ","ͅ":"ι","Ͱ":"ͱ","Ͳ":"ͳ","Ͷ":"ͷ","Ϳ":"ϳ","Ά":"ά","Έ":"έ","Ή":"ή","Ί":"ί","Ό":"ό","Ύ":"ύ","Ώ":"ώ","Α":"α","Β":"β","Γ":"γ","Δ":"δ","Ε":"ε","Ζ":"ζ","Η":"η","Θ":"θ","Ι":"ι","Κ":"κ","Λ":"λ","Μ":"μ","Ν":"ν","Ξ":"ξ","Ο":"ο","Π":"π","Ρ":"ρ","Σ":"σ","Τ":"τ","Υ":"υ","Φ":"φ","Χ":"χ","Ψ":"ψ","Ω":"ω","Ϊ":"ϊ","Ϋ":"ϋ","ς":"σ","Ϗ":"ϗ","ϐ":"β","ϑ":"θ","ϕ":"φ","ϖ":"π","Ϙ":"ϙ","Ϛ":"ϛ","Ϝ":"ϝ","Ϟ":"ϟ","Ϡ":"ϡ","Ϣ":"ϣ","Ϥ":"ϥ","Ϧ":"ϧ","Ϩ":"ϩ","Ϫ":"ϫ","Ϭ":"ϭ","Ϯ":"ϯ","ϰ":"κ","ϱ":"ρ","ϴ":"θ","ϵ":"ε","Ϸ":"ϸ","Ϲ":"ϲ","Ϻ":"ϻ","Ͻ":"ͻ","Ͼ":"ͼ","Ͽ":"ͽ","Ѐ":"ѐ","Ё":"ё","Ђ":"ђ","Ѓ":"ѓ","Є":"є","Ѕ":"ѕ","І":"і","Ї":"ї","Ј":"ј","Љ":"љ","Њ":"њ","Ћ":"ћ","Ќ":"ќ","Ѝ":"ѝ","Ў":"ў","Џ":"џ","А":"а","Б":"б","В":"в","Г":"г","Д":"д","Е":"е","Ж":"ж","З":"з","И":"и","Й":"й","К":"к","Л":"л","М":"м","Н":"н","О":"о","П":"п","Р":"р","С":"с","Т":"т","У":"у","Ф":"ф","Х":"х","Ц":"ц","Ч":"ч","Ш":"ш","Щ":"щ","Ъ":"ъ","Ы":"ы","Ь":"ь","Э":"э","Ю":"ю","Я":"я","Ѡ":"ѡ","Ѣ":"ѣ","Ѥ":"ѥ","Ѧ":"ѧ","Ѩ":"ѩ","Ѫ":"ѫ","Ѭ":"ѭ","Ѯ":"ѯ","Ѱ":"ѱ","Ѳ":"ѳ","Ѵ":"ѵ","Ѷ":"ѷ","Ѹ":"ѹ","Ѻ":"ѻ","Ѽ":"ѽ","Ѿ":"ѿ","Ҁ":"ҁ","Ҋ":"ҋ","Ҍ":"ҍ","Ҏ":"ҏ","Ґ":"ґ","Ғ":"ғ","Ҕ":"ҕ","Җ":"җ","Ҙ":"ҙ","Қ":"қ","Ҝ":"ҝ","Ҟ":"ҟ","Ҡ":"ҡ","Ң":"ң","Ҥ":"ҥ","Ҧ":"ҧ","Ҩ":"ҩ","Ҫ":"ҫ","Ҭ":"ҭ","Ү":"ү","Ұ":"ұ","Ҳ":"ҳ","Ҵ":"ҵ","Ҷ":"ҷ","Ҹ":"ҹ","Һ":"һ","Ҽ":"ҽ","Ҿ":"ҿ","Ӏ":"ӏ","Ӂ":"ӂ","Ӄ":"ӄ","Ӆ":"ӆ","Ӈ":"ӈ","Ӊ":"ӊ","Ӌ":"ӌ","Ӎ":"ӎ","Ӑ":"ӑ","Ӓ":"ӓ","Ӕ":"ӕ","Ӗ":"ӗ","Ә":"ә","Ӛ":"ӛ","Ӝ":"ӝ","Ӟ":"ӟ","Ӡ":"ӡ","Ӣ":"ӣ","Ӥ":"ӥ","Ӧ":"ӧ","Ө":"ө","Ӫ":"ӫ","Ӭ":"ӭ","Ӯ":"ӯ","Ӱ":"ӱ","Ӳ":"ӳ","Ӵ":"ӵ","Ӷ":"ӷ","Ӹ":"ӹ","Ӻ":"ӻ","Ӽ":"ӽ","Ӿ":"ӿ","Ԁ":"ԁ","Ԃ":"ԃ","Ԅ":"ԅ","Ԇ":"ԇ","Ԉ":"ԉ","Ԋ":"ԋ","Ԍ":"ԍ","Ԏ":"ԏ","Ԑ":"ԑ","Ԓ":"ԓ","Ԕ":"ԕ","Ԗ":"ԗ","Ԙ":"ԙ","Ԛ":"ԛ","Ԝ":"ԝ","Ԟ":"ԟ","Ԡ":"ԡ","Ԣ":"ԣ","Ԥ":"ԥ","Ԧ":"ԧ","Ԩ":"ԩ","Ԫ":"ԫ","Ԭ":"ԭ","Ԯ":"ԯ","Ա":"ա","Բ":"բ","Գ":"գ","Դ":"դ","Ե":"ե","Զ":"զ","Է":"է","Ը":"ը","Թ":"թ","Ժ":"ժ","Ի":"ի","Լ":"լ","Խ":"խ","Ծ":"ծ","Կ":"կ","Հ":"հ","Ձ":"ձ","Ղ":"ղ","Ճ":"ճ","Մ":"մ","Յ":"յ","Ն":"ն","Շ":"շ","Ո":"ո","Չ":"չ","Պ":"պ","Ջ":"ջ","Ռ":"ռ","Ս":"ս","Վ":"վ","Տ":"տ","Ր":"ր","Ց":"ց","Ւ":"ւ","Փ":"փ","Ք":"ք","Օ":"օ","Ֆ":"ֆ","Ⴀ":"ⴀ","Ⴁ":"ⴁ","Ⴂ":"ⴂ","Ⴃ":"ⴃ","Ⴄ":"ⴄ","Ⴅ":"ⴅ","Ⴆ":"ⴆ","Ⴇ":"ⴇ","Ⴈ":"ⴈ","Ⴉ":"ⴉ","Ⴊ":"ⴊ","Ⴋ":"ⴋ","Ⴌ":"ⴌ","Ⴍ":"ⴍ","Ⴎ":"ⴎ","Ⴏ":"ⴏ","Ⴐ":"ⴐ","Ⴑ":"ⴑ","Ⴒ":"ⴒ","Ⴓ":"ⴓ","Ⴔ":"ⴔ","Ⴕ":"ⴕ","Ⴖ":"ⴖ","Ⴗ":"ⴗ","Ⴘ":"ⴘ","Ⴙ":"ⴙ","Ⴚ":"ⴚ","Ⴛ":"ⴛ","Ⴜ":"ⴜ","Ⴝ":"ⴝ","Ⴞ":"ⴞ","Ⴟ":"ⴟ","Ⴠ":"ⴠ","Ⴡ":"ⴡ","Ⴢ":"ⴢ","Ⴣ":"ⴣ","Ⴤ":"ⴤ","Ⴥ":"ⴥ","Ⴧ":"ⴧ","Ⴭ":"ⴭ","Ḁ":"ḁ","Ḃ":"ḃ","Ḅ":"ḅ","Ḇ":"ḇ","Ḉ":"ḉ","Ḋ":"ḋ","Ḍ":"ḍ","Ḏ":"ḏ","Ḑ":"ḑ","Ḓ":"ḓ","Ḕ":"ḕ","Ḗ":"ḗ","Ḙ":"ḙ","Ḛ":"ḛ","Ḝ":"ḝ","Ḟ":"ḟ","Ḡ":"ḡ","Ḣ":"ḣ","Ḥ":"ḥ","Ḧ":"ḧ","Ḩ":"ḩ","Ḫ":"ḫ","Ḭ":"ḭ","Ḯ":"ḯ","Ḱ":"ḱ","Ḳ":"ḳ","Ḵ":"ḵ","Ḷ":"ḷ","Ḹ":"ḹ","Ḻ":"ḻ","Ḽ":"ḽ","Ḿ":"ḿ","Ṁ":"ṁ","Ṃ":"ṃ","Ṅ":"ṅ","Ṇ":"ṇ","Ṉ":"ṉ","Ṋ":"ṋ","Ṍ":"ṍ","Ṏ":"ṏ","Ṑ":"ṑ","Ṓ":"ṓ","Ṕ":"ṕ","Ṗ":"ṗ","Ṙ":"ṙ","Ṛ":"ṛ","Ṝ":"ṝ","Ṟ":"ṟ","Ṡ":"ṡ","Ṣ":"ṣ","Ṥ":"ṥ","Ṧ":"ṧ","Ṩ":"ṩ","Ṫ":"ṫ","Ṭ":"ṭ","Ṯ":"ṯ","Ṱ":"ṱ","Ṳ":"ṳ","Ṵ":"ṵ","Ṷ":"ṷ","Ṹ":"ṹ","Ṻ":"ṻ","Ṽ":"ṽ","Ṿ":"ṿ","Ẁ":"ẁ","Ẃ":"ẃ","Ẅ":"ẅ","Ẇ":"ẇ","Ẉ":"ẉ","Ẋ":"ẋ","Ẍ":"ẍ","Ẏ":"ẏ","Ẑ":"ẑ","Ẓ":"ẓ","Ẕ":"ẕ","ẛ":"ṡ","Ạ":"ạ","Ả":"ả","Ấ":"ấ","Ầ":"ầ","Ẩ":"ẩ","Ẫ":"ẫ","Ậ":"ậ","Ắ":"ắ","Ằ":"ằ","Ẳ":"ẳ","Ẵ":"ẵ","Ặ":"ặ","Ẹ":"ẹ","Ẻ":"ẻ","Ẽ":"ẽ","Ế":"ế","Ề":"ề","Ể":"ể","Ễ":"ễ","Ệ":"ệ","Ỉ":"ỉ","Ị":"ị","Ọ":"ọ","Ỏ":"ỏ","Ố":"ố","Ồ":"ồ","Ổ":"ổ","Ỗ":"ỗ","Ộ":"ộ","Ớ":"ớ","Ờ":"ờ","Ở":"ở","Ỡ":"ỡ","Ợ":"ợ","Ụ":"ụ","Ủ":"ủ","Ứ":"ứ","Ừ":"ừ","Ử":"ử","Ữ":"ữ","Ự":"ự","Ỳ":"ỳ","Ỵ":"ỵ","Ỷ":"ỷ","Ỹ":"ỹ","Ỻ":"ỻ","Ỽ":"ỽ","Ỿ":"ỿ","Ἀ":"ἀ","Ἁ":"ἁ","Ἂ":"ἂ","Ἃ":"ἃ","Ἄ":"ἄ","Ἅ":"ἅ","Ἆ":"ἆ","Ἇ":"ἇ","Ἐ":"ἐ","Ἑ":"ἑ","Ἒ":"ἒ","Ἓ":"ἓ","Ἔ":"ἔ","Ἕ":"ἕ","Ἠ":"ἠ","Ἡ":"ἡ","Ἢ":"ἢ","Ἣ":"ἣ","Ἤ":"ἤ","Ἥ":"ἥ","Ἦ":"ἦ","Ἧ":"ἧ","Ἰ":"ἰ","Ἱ":"ἱ","Ἲ":"ἲ","Ἳ":"ἳ","Ἴ":"ἴ","Ἵ":"ἵ","Ἶ":"ἶ","Ἷ":"ἷ","Ὀ":"ὀ","Ὁ":"ὁ","Ὂ":"ὂ","Ὃ":"ὃ","Ὄ":"ὄ","Ὅ":"ὅ","Ὑ":"ὑ","Ὓ":"ὓ","Ὕ":"ὕ","Ὗ":"ὗ","Ὠ":"ὠ","Ὡ":"ὡ","Ὢ":"ὢ","Ὣ":"ὣ","Ὤ":"ὤ","Ὥ":"ὥ","Ὦ":"ὦ","Ὧ":"ὧ","Ᾰ":"ᾰ","Ᾱ":"ᾱ","Ὰ":"ὰ","Ά":"ά","ι":"ι","Ὲ":"ὲ","Έ":"έ","Ὴ":"ὴ","Ή":"ή","Ῐ":"ῐ","Ῑ":"ῑ","Ὶ":"ὶ","Ί":"ί","Ῠ":"ῠ","Ῡ":"ῡ","Ὺ":"ὺ","Ύ":"ύ","Ῥ":"ῥ","Ὸ":"ὸ","Ό":"ό","Ὼ":"ὼ","Ώ":"ώ","Ω":"ω","K":"k","Å":"å","Ⅎ":"ⅎ","Ⅰ":"ⅰ","Ⅱ":"ⅱ","Ⅲ":"ⅲ","Ⅳ":"ⅳ","Ⅴ":"ⅴ","Ⅵ":"ⅵ","Ⅶ":"ⅶ","Ⅷ":"ⅷ","Ⅸ":"ⅸ","Ⅹ":"ⅹ","Ⅺ":"ⅺ","Ⅻ":"ⅻ","Ⅼ":"ⅼ","Ⅽ":"ⅽ","Ⅾ":"ⅾ","Ⅿ":"ⅿ","Ↄ":"ↄ","Ⓐ":"ⓐ","Ⓑ":"ⓑ","Ⓒ":"ⓒ","Ⓓ":"ⓓ","Ⓔ":"ⓔ","Ⓕ":"ⓕ","Ⓖ":"ⓖ","Ⓗ":"ⓗ","Ⓘ":"ⓘ","Ⓙ":"ⓙ","Ⓚ":"ⓚ","Ⓛ":"ⓛ","Ⓜ":"ⓜ","Ⓝ":"ⓝ","Ⓞ":"ⓞ","Ⓟ":"ⓟ","Ⓠ":"ⓠ","Ⓡ":"ⓡ","Ⓢ":"ⓢ","Ⓣ":"ⓣ","Ⓤ":"ⓤ","Ⓥ":"ⓥ","Ⓦ":"ⓦ","Ⓧ":"ⓧ","Ⓨ":"ⓨ","Ⓩ":"ⓩ","Ⰰ":"ⰰ","Ⰱ":"ⰱ","Ⰲ":"ⰲ","Ⰳ":"ⰳ","Ⰴ":"ⰴ","Ⰵ":"ⰵ","Ⰶ":"ⰶ","Ⰷ":"ⰷ","Ⰸ":"ⰸ","Ⰹ":"ⰹ","Ⰺ":"ⰺ","Ⰻ":"ⰻ","Ⰼ":"ⰼ","Ⰽ":"ⰽ","Ⰾ":"ⰾ","Ⰿ":"ⰿ","Ⱀ":"ⱀ","Ⱁ":"ⱁ","Ⱂ":"ⱂ","Ⱃ":"ⱃ","Ⱄ":"ⱄ","Ⱅ":"ⱅ","Ⱆ":"ⱆ","Ⱇ":"ⱇ","Ⱈ":"ⱈ","Ⱉ":"ⱉ","Ⱊ":"ⱊ","Ⱋ":"ⱋ","Ⱌ":"ⱌ","Ⱍ":"ⱍ","Ⱎ":"ⱎ","Ⱏ":"ⱏ","Ⱐ":"ⱐ","Ⱑ":"ⱑ","Ⱒ":"ⱒ","Ⱓ":"ⱓ","Ⱔ":"ⱔ","Ⱕ":"ⱕ","Ⱖ":"ⱖ","Ⱗ":"ⱗ","Ⱘ":"ⱘ","Ⱙ":"ⱙ","Ⱚ":"ⱚ","Ⱛ":"ⱛ","Ⱜ":"ⱜ","Ⱝ":"ⱝ","Ⱞ":"ⱞ","Ⱡ":"ⱡ","Ɫ":"ɫ","Ᵽ":"ᵽ","Ɽ":"ɽ","Ⱨ":"ⱨ","Ⱪ":"ⱪ","Ⱬ":"ⱬ","Ɑ":"ɑ","Ɱ":"ɱ","Ɐ":"ɐ","Ɒ":"ɒ","Ⱳ":"ⱳ","Ⱶ":"ⱶ","Ȿ":"ȿ","Ɀ":"ɀ","Ⲁ":"ⲁ","Ⲃ":"ⲃ","Ⲅ":"ⲅ","Ⲇ":"ⲇ","Ⲉ":"ⲉ","Ⲋ":"ⲋ","Ⲍ":"ⲍ","Ⲏ":"ⲏ","Ⲑ":"ⲑ","Ⲓ":"ⲓ","Ⲕ":"ⲕ","Ⲗ":"ⲗ","Ⲙ":"ⲙ","Ⲛ":"ⲛ","Ⲝ":"ⲝ","Ⲟ":"ⲟ","Ⲡ":"ⲡ","Ⲣ":"ⲣ","Ⲥ":"ⲥ","Ⲧ":"ⲧ","Ⲩ":"ⲩ","Ⲫ":"ⲫ","Ⲭ":"ⲭ","Ⲯ":"ⲯ","Ⲱ":"ⲱ","Ⲳ":"ⲳ","Ⲵ":"ⲵ","Ⲷ":"ⲷ","Ⲹ":"ⲹ","Ⲻ":"ⲻ","Ⲽ":"ⲽ","Ⲿ":"ⲿ","Ⳁ":"ⳁ","Ⳃ":"ⳃ","Ⳅ":"ⳅ","Ⳇ":"ⳇ","Ⳉ":"ⳉ","Ⳋ":"ⳋ","Ⳍ":"ⳍ","Ⳏ":"ⳏ","Ⳑ":"ⳑ","Ⳓ":"ⳓ","Ⳕ":"ⳕ","Ⳗ":"ⳗ","Ⳙ":"ⳙ","Ⳛ":"ⳛ","Ⳝ":"ⳝ","Ⳟ":"ⳟ","Ⳡ":"ⳡ","Ⳣ":"ⳣ","Ⳬ":"ⳬ","Ⳮ":"ⳮ","Ⳳ":"ⳳ","Ꙁ":"ꙁ","Ꙃ":"ꙃ","Ꙅ":"ꙅ","Ꙇ":"ꙇ","Ꙉ":"ꙉ","Ꙋ":"ꙋ","Ꙍ":"ꙍ","Ꙏ":"ꙏ","Ꙑ":"ꙑ","Ꙓ":"ꙓ","Ꙕ":"ꙕ","Ꙗ":"ꙗ","Ꙙ":"ꙙ","Ꙛ":"ꙛ","Ꙝ":"ꙝ","Ꙟ":"ꙟ","Ꙡ":"ꙡ","Ꙣ":"ꙣ","Ꙥ":"ꙥ","Ꙧ":"ꙧ","Ꙩ":"ꙩ","Ꙫ":"ꙫ","Ꙭ":"ꙭ","Ꚁ":"ꚁ","Ꚃ":"ꚃ","Ꚅ":"ꚅ","Ꚇ":"ꚇ","Ꚉ":"ꚉ","Ꚋ":"ꚋ","Ꚍ":"ꚍ","Ꚏ":"ꚏ","Ꚑ":"ꚑ","Ꚓ":"ꚓ","Ꚕ":"ꚕ","Ꚗ":"ꚗ","Ꚙ":"ꚙ","Ꚛ":"ꚛ","Ꜣ":"ꜣ","Ꜥ":"ꜥ","Ꜧ":"ꜧ","Ꜩ":"ꜩ","Ꜫ":"ꜫ","Ꜭ":"ꜭ","Ꜯ":"ꜯ","Ꜳ":"ꜳ","Ꜵ":"ꜵ","Ꜷ":"ꜷ","Ꜹ":"ꜹ","Ꜻ":"ꜻ","Ꜽ":"ꜽ","Ꜿ":"ꜿ","Ꝁ":"ꝁ","Ꝃ":"ꝃ","Ꝅ":"ꝅ","Ꝇ":"ꝇ","Ꝉ":"ꝉ","Ꝋ":"ꝋ","Ꝍ":"ꝍ","Ꝏ":"ꝏ","Ꝑ":"ꝑ","Ꝓ":"ꝓ","Ꝕ":"ꝕ","Ꝗ":"ꝗ","Ꝙ":"ꝙ","Ꝛ":"ꝛ","Ꝝ":"ꝝ","Ꝟ":"ꝟ","Ꝡ":"ꝡ","Ꝣ":"ꝣ","Ꝥ":"ꝥ","Ꝧ":"ꝧ","Ꝩ":"ꝩ","Ꝫ":"ꝫ","Ꝭ":"ꝭ","Ꝯ":"ꝯ","Ꝺ":"ꝺ","Ꝼ":"ꝼ","Ᵹ":"ᵹ","Ꝿ":"ꝿ","Ꞁ":"ꞁ","Ꞃ":"ꞃ","Ꞅ":"ꞅ","Ꞇ":"ꞇ","Ꞌ":"ꞌ","Ɥ":"ɥ","Ꞑ":"ꞑ","Ꞓ":"ꞓ","Ꞗ":"ꞗ","Ꞙ":"ꞙ","Ꞛ":"ꞛ","Ꞝ":"ꞝ","Ꞟ":"ꞟ","Ꞡ":"ꞡ","Ꞣ":"ꞣ","Ꞥ":"ꞥ","Ꞧ":"ꞧ","Ꞩ":"ꞩ","Ɦ":"ɦ","Ɜ":"ɜ","Ɡ":"ɡ","Ɬ":"ɬ","Ʞ":"ʞ","Ʇ":"ʇ","A":"a","B":"b","C":"c","D":"d","E":"e","F":"f","G":"g","H":"h","I":"i","J":"j","K":"k","L":"l","M":"m","N":"n","O":"o","P":"p","Q":"q","R":"r","S":"s","T":"t","U":"u","V":"v","W":"w","X":"x","Y":"y","Z":"z","𐐀":"𐐨","𐐁":"𐐩","𐐂":"𐐪","𐐃":"𐐫","𐐄":"𐐬","𐐅":"𐐭","𐐆":"𐐮","𐐇":"𐐯","𐐈":"𐐰","𐐉":"𐐱","𐐊":"𐐲","𐐋":"𐐳","𐐌":"𐐴","𐐍":"𐐵","𐐎":"𐐶","𐐏":"𐐷","𐐐":"𐐸","𐐑":"𐐹","𐐒":"𐐺","𐐓":"𐐻","𐐔":"𐐼","𐐕":"𐐽","𐐖":"𐐾","𐐗":"𐐿","𐐘":"𐑀","𐐙":"𐑁","𐐚":"𐑂","𐐛":"𐑃","𐐜":"𐑄","𐐝":"𐑅","𐐞":"𐑆","𐐟":"𐑇","𐐠":"𐑈","𐐡":"𐑉","𐐢":"𐑊","𐐣":"𐑋","𐐤":"𐑌","𐐥":"𐑍","𐐦":"𐑎","𐐧":"𐑏","𑢠":"𑣀","𑢡":"𑣁","𑢢":"𑣂","𑢣":"𑣃","𑢤":"𑣄","𑢥":"𑣅","𑢦":"𑣆","𑢧":"𑣇","𑢨":"𑣈","𑢩":"𑣉","𑢪":"𑣊","𑢫":"𑣋","𑢬":"𑣌","𑢭":"𑣍","𑢮":"𑣎","𑢯":"𑣏","𑢰":"𑣐","𑢱":"𑣑","𑢲":"𑣒","𑢳":"𑣓","𑢴":"𑣔","𑢵":"𑣕","𑢶":"𑣖","𑢷":"𑣗","𑢸":"𑣘","𑢹":"𑣙","𑢺":"𑣚","𑢻":"𑣛","𑢼":"𑣜","𑢽":"𑣝","𑢾":"𑣞","𑢿":"𑣟","ß":"ss","İ":"i̇","ʼn":"ʼn","ǰ":"ǰ","ΐ":"ΐ","ΰ":"ΰ","և":"եւ","ẖ":"ẖ","ẗ":"ẗ","ẘ":"ẘ","ẙ":"ẙ","ẚ":"aʾ","ẞ":"ss","ὐ":"ὐ","ὒ":"ὒ","ὔ":"ὔ","ὖ":"ὖ","ᾀ":"ἀι","ᾁ":"ἁι","ᾂ":"ἂι","ᾃ":"ἃι","ᾄ":"ἄι","ᾅ":"ἅι","ᾆ":"ἆι","ᾇ":"ἇι","ᾈ":"ἀι","ᾉ":"ἁι","ᾊ":"ἂι","ᾋ":"ἃι","ᾌ":"ἄι","ᾍ":"ἅι","ᾎ":"ἆι","ᾏ":"ἇι","ᾐ":"ἠι","ᾑ":"ἡι","ᾒ":"ἢι","ᾓ":"ἣι","ᾔ":"ἤι","ᾕ":"ἥι","ᾖ":"ἦι","ᾗ":"ἧι","ᾘ":"ἠι","ᾙ":"ἡι","ᾚ":"ἢι","ᾛ":"ἣι","ᾜ":"ἤι","ᾝ":"ἥι","ᾞ":"ἦι","ᾟ":"ἧι","ᾠ":"ὠι","ᾡ":"ὡι","ᾢ":"ὢι","ᾣ":"ὣι","ᾤ":"ὤι","ᾥ":"ὥι","ᾦ":"ὦι","ᾧ":"ὧι","ᾨ":"ὠι","ᾩ":"ὡι","ᾪ":"ὢι","ᾫ":"ὣι","ᾬ":"ὤι","ᾭ":"ὥι","ᾮ":"ὦι","ᾯ":"ὧι","ᾲ":"ὰι","ᾳ":"αι","ᾴ":"άι","ᾶ":"ᾶ","ᾷ":"ᾶι","ᾼ":"αι","ῂ":"ὴι","ῃ":"ηι","ῄ":"ήι","ῆ":"ῆ","ῇ":"ῆι","ῌ":"ηι","ῒ":"ῒ","ΐ":"ΐ","ῖ":"ῖ","ῗ":"ῗ","ῢ":"ῢ","ΰ":"ΰ","ῤ":"ῤ","ῦ":"ῦ","ῧ":"ῧ","ῲ":"ὼι","ῳ":"ωι","ῴ":"ώι","ῶ":"ῶ","ῷ":"ῶι","ῼ":"ωι","ff":"ff","fi":"fi","fl":"fl","ffi":"ffi","ffl":"ffl","ſt":"st","st":"st","ﬓ":"մն","ﬔ":"մե","ﬕ":"մի","ﬖ":"վն","ﬗ":"մխ"};t.exports=function(e){return e.slice(1,e.length-1).trim().replace(n,function(e){return i[e]||" "})}},{}],8:[function(e,t,r){function n(e){(e=e||{}).softbreak=e.softbreak||"\n",this.disableTags=0,this.lastOut="\n",this.options=e}var i=e("./renderer"),s=/^javascript:|vbscript:|file:|data:/i,o=/^data:image\/(?:png|gif|jpeg|webp)/i,a=function(e){return s.test(e)&&!o.test(e)};(n.prototype=Object.create(i.prototype)).text=function(e){this.out(e.literal)},n.prototype.html_inline=function(e){this.lit(this.options.safe?"\x3c!-- raw HTML omitted --\x3e":e.literal)},n.prototype.html_block=function(e){this.cr(),this.lit(this.options.safe?"\x3c!-- raw HTML omitted --\x3e":e.literal),this.cr()},n.prototype.softbreak=function(){this.lit(this.options.softbreak)},n.prototype.linebreak=function(){this.tag("br",[],!0),this.cr()},n.prototype.link=function(e,t){var r=this.attrs(e);t?(this.options.safe&&a(e.destination)||r.push(["href",this.esc(e.destination,!0)]),e.title&&r.push(["title",this.esc(e.title,!0)]),this.tag("a",r)):this.tag("/a")},n.prototype.image=function(e,t){t?(0===this.disableTags&&this.lit(this.options.safe&&a(e.destination)?'':'<img src='))},n.prototype.emph=function(e,t){this.tag(t?"em":"/em")},n.prototype.strong=function(e,t){this.tag(t?"strong":"/strong")},n.prototype.paragraph=function(e,t){var r=e.parent.parent,n=this.attrs(e);null!==r&&"list"===r.type&&r.listTight||(t?(this.cr(),this.tag("p",n)):(this.tag("/p"),this.cr()))},n.prototype.heading=function(e,t){var r="h"+e.level,n=this.attrs(e);t?(this.cr(),this.tag(r,n)):(this.tag("/"+r),this.cr())},n.prototype.code=function(e){this.tag("code"),this.out(e.literal),this.tag("/code")},n.prototype.code_block=function(e){var t=e.info?e.info.split(/\s+/):[],r=this.attrs(e);t.length>0&&t[0].length>0&&r.push(["class","language-"+this.esc(t[0],!0)]),this.cr(),this.tag("pre"),this.tag("code",r),this.out(e.literal),this.tag("/code"),this.tag("/pre"),this.cr()},n.prototype.thematic_break=function(e){var t=this.attrs(e);this.cr(),this.tag("hr",t,!0),this.cr()},n.prototype.block_quote=function(e,t){var r=this.attrs(e);t?(this.cr(),this.tag("blockquote",r),this.cr()):(this.cr(),this.tag("/blockquote"),this.cr())},n.prototype.list=function(e,t){var r="bullet"===e.listType?"ul":"ol",n=this.attrs(e);if(t){var i=e.listStart;null!==i&&1!==i&&n.push(["start",i.toString()]),this.cr(),this.tag(r,n),this.cr()}else this.cr(),this.tag("/"+r),this.cr()},n.prototype.item=function(e,t){var r=this.attrs(e);t?this.tag("li",r):(this.tag("/li"),this.cr())},n.prototype.custom_inline=function(e,t){t&&e.onEnter?this.lit(e.onEnter):!t&&e.onExit&&this.lit(e.onExit)},n.prototype.custom_block=function(e,t){this.cr(),t&&e.onEnter?this.lit(e.onEnter):!t&&e.onExit&&this.lit(e.onExit),this.cr()},n.prototype.esc=e("../common").escapeXml,n.prototype.out=function(e){this.lit(this.esc(e,!1))},n.prototype.tag=function(e,t,r){if(!(this.disableTags>0)){if(this.buffer+="<"+e,t&&t.length>0)for(var n,i=0;void 0!==(n=t[i]);)this.buffer+=" "+n[0]+'="'+n[1]+'"',i++;r&&(this.buffer+=" /"),this.buffer+=">",this.lastOut=">"}},n.prototype.attrs=function(e){var t=[];if(this.options.sourcepos){var r=e.sourcepos;r&&t.push(["data-sourcepos",String(r[0][0])+":"+String(r[0][1])+"-"+String(r[1][0])+":"+String(r[1][1])])}return t},t.exports=n},{"../common":2,"./renderer":9}],9:[function(e,t,r){function n(){}n.prototype.render=function(e){var t,r,n=e.walker();for(this.buffer="",this.lastOut="\n";t=n.next();)this[r=t.node.type]&&this[r](t.node,t.entering);return this.buffer},n.prototype.out=function(e){this.lit(e)},n.prototype.lit=function(e){this.buffer+=e,this.lastOut=e},n.prototype.cr=function(){"\n"!==this.lastOut&&this.lit("\n")},n.prototype.esc=function(e){return e},t.exports=n},{}],10:[function(e,t,r){function n(e){e=e||{},this.disableTags=0,this.lastOut="\n",this.indentLevel=0,this.indent=" ",this.options=e}var i=e("./renderer"),s=/\<[^>]*\>/;(n.prototype=Object.create(i.prototype)).render=function(e){this.buffer="";var t,r,n,i,s,o,a,l,u=e.walker(),c=this.options;for(c.time&&console.time("rendering"),this.buffer+='\n',this.buffer+='\n';n=u.next();)if(s=n.entering,l=(i=n.node).type,o=i.isContainer,a="thematic_break"===l||"linebreak"===l||"softbreak"===l,r=l.replace(/([a-z])([A-Z])/g,"$1_$2").toLowerCase(),s){switch(t=[],l){case"document":t.push(["xmlns","http://commonmark.org/xml/1.0"]);break;case"list":null!==i.listType&&t.push(["type",i.listType.toLowerCase()]),null!==i.listStart&&t.push(["start",String(i.listStart)]),null!==i.listTight&&t.push(["tight",i.listTight?"true":"false"]);var p=i.listDelimiter;if(null!==p){var h;h="."===p?"period":"paren",t.push(["delimiter",h])}break;case"code_block":i.info&&t.push(["info",i.info]);break;case"heading":t.push(["level",String(i.level)]);break;case"link":case"image":t.push(["destination",i.destination]),t.push(["title",i.title]);break;case"custom_inline":case"custom_block":t.push(["on_enter",i.onEnter]),t.push(["on_exit",i.onExit])}if(c.sourcepos){var d=i.sourcepos;d&&t.push(["sourcepos",String(d[0][0])+":"+String(d[0][1])+"-"+String(d[1][0])+":"+String(d[1][1])])}if(this.cr(),this.out(this.tag(r,t,a)),o)this.indentLevel+=1;else if(!o&&!a){var m=i.literal;m&&this.out(this.esc(m)),this.out(this.tag("/"+r))}}else this.indentLevel-=1,this.cr(),this.out(this.tag("/"+r));return c.time&&console.timeEnd("rendering"),this.buffer+="\n"},n.prototype.out=function(e){this.buffer+=this.disableTags>0?e.replace(s,""):e,this.lastOut=e},n.prototype.cr=function(){if("\n"!==this.lastOut){this.buffer+="\n",this.lastOut="\n";for(var e=this.indentLevel;e>0;e--)this.buffer+=this.indent}},n.prototype.tag=function(e,t,r){var n="<"+e;if(t&&t.length>0)for(var i,s=0;void 0!==(i=t[s]);)n+=" "+i[0]+'="'+this.esc(i[1])+'"',s++;return r&&(n+=" /"),n+">"},n.prototype.esc=e("../common").escapeXml,t.exports=n},{"../common":2,"./renderer":9}],11:[function(e,t,r){var n=e("./lib/encode.js"),i=e("./lib/decode.js");r.decode=function(e,t){return(!t||t<=0?i.XML:i.HTML)(e)},r.decodeStrict=function(e,t){return(!t||t<=0?i.XML:i.HTMLStrict)(e)},r.encode=function(e,t){return(!t||t<=0?n.XML:n.HTML)(e)},r.encodeXML=n.XML,r.encodeHTML4=r.encodeHTML5=r.encodeHTML=n.HTML,r.decodeXML=r.decodeXMLStrict=i.XML,r.decodeHTML4=r.decodeHTML5=r.decodeHTML=i.HTML,r.decodeHTML4Strict=r.decodeHTML5Strict=r.decodeHTMLStrict=i.HTMLStrict,r.escape=n.escape},{"./lib/decode.js":12,"./lib/encode.js":14}],12:[function(e,t,r){function n(e){var t=Object.keys(e).join("|"),r=s(e),n=new RegExp("&(?:"+(t+="|#[xX][\\da-fA-F]+|#\\d+")+");","g");return function(e){return String(e).replace(n,r)}}function i(e,t){return e=55296&&e<=57343||e>1114111)return"�";e in n&&(e=n[e]);var t="";return e>65535&&(e-=65536,t+=String.fromCharCode(e>>>10&1023|55296),e=56320|1023&e),t+String.fromCharCode(e)}},{"../maps/decode.json":15}],14:[function(e,t,r){function n(e){return Object.keys(e).sort().reduce(function(t,r){return t[e[r]]="&"+r+";",t},{})}function i(e){var t=[],r=[];return Object.keys(e).forEach(function(e){1===e.length?t.push("\\"+e):r.push(e)}),r.unshift("["+t.join("")+"]"),new RegExp(r.join("|"),"g")}function s(e){return"&#x"+e.charCodeAt(0).toString(16).toUpperCase()+";"}function o(e){return"&#x"+(1024*(e.charCodeAt(0)-55296)+e.charCodeAt(1)-56320+65536).toString(16).toUpperCase()+";"}function a(e,t){function r(t){return e[t]}return function(e){return e.replace(t,r).replace(d,o).replace(h,s)}}var l=n(e("../maps/xml.json")),u=i(l);r.XML=a(l,u);var c=n(e("../maps/entities.json")),p=i(c);r.HTML=a(c,p);var h=/[^\0-\x7F]/g,d=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,m=i(l);r.escape=function(e){return e.replace(m,s).replace(d,o).replace(h,s)}},{"../maps/entities.json":16,"../maps/xml.json":18}],15:[function(e,t,r){t.exports={0:65533,128:8364,130:8218,131:402,132:8222,133:8230,134:8224,135:8225,136:710,137:8240,138:352,139:8249,140:338,142:381,145:8216,146:8217,147:8220,148:8221,149:8226,150:8211,151:8212,152:732,153:8482,154:353,155:8250,156:339,158:382,159:376}},{}],16:[function(e,t,r){t.exports={Aacute:"Á",aacute:"á",Abreve:"Ă",abreve:"ă",ac:"∾",acd:"∿",acE:"∾̳",Acirc:"Â",acirc:"â",acute:"´",Acy:"А",acy:"а",AElig:"Æ",aelig:"æ",af:"⁡",Afr:"𝔄",afr:"𝔞",Agrave:"À",agrave:"à",alefsym:"ℵ",aleph:"ℵ",Alpha:"Α",alpha:"α",Amacr:"Ā",amacr:"ā",amalg:"⨿",amp:"&",AMP:"&",andand:"⩕",And:"⩓",and:"∧",andd:"⩜",andslope:"⩘",andv:"⩚",ang:"∠",ange:"⦤",angle:"∠",angmsdaa:"⦨",angmsdab:"⦩",angmsdac:"⦪",angmsdad:"⦫",angmsdae:"⦬",angmsdaf:"⦭",angmsdag:"⦮",angmsdah:"⦯",angmsd:"∡",angrt:"∟",angrtvb:"⊾",angrtvbd:"⦝",angsph:"∢",angst:"Å",angzarr:"⍼",Aogon:"Ą",aogon:"ą",Aopf:"𝔸",aopf:"𝕒",apacir:"⩯",ap:"≈",apE:"⩰",ape:"≊",apid:"≋",apos:"'",ApplyFunction:"⁡",approx:"≈",approxeq:"≊",Aring:"Å",aring:"å",Ascr:"𝒜",ascr:"𝒶",Assign:"≔",ast:"*",asymp:"≈",asympeq:"≍",Atilde:"Ã",atilde:"ã",Auml:"Ä",auml:"ä",awconint:"∳",awint:"⨑",backcong:"≌",backepsilon:"϶",backprime:"‵",backsim:"∽",backsimeq:"⋍",Backslash:"∖",Barv:"⫧",barvee:"⊽",barwed:"⌅",Barwed:"⌆",barwedge:"⌅",bbrk:"⎵",bbrktbrk:"⎶",bcong:"≌",Bcy:"Б",bcy:"б",bdquo:"„",becaus:"∵",because:"∵",Because:"∵",bemptyv:"⦰",bepsi:"϶",bernou:"ℬ",Bernoullis:"ℬ",Beta:"Β",beta:"β",beth:"ℶ",between:"≬",Bfr:"𝔅",bfr:"𝔟",bigcap:"⋂",bigcirc:"◯",bigcup:"⋃",bigodot:"⨀",bigoplus:"⨁",bigotimes:"⨂",bigsqcup:"⨆",bigstar:"★",bigtriangledown:"▽",bigtriangleup:"△",biguplus:"⨄",bigvee:"⋁",bigwedge:"⋀",bkarow:"⤍",blacklozenge:"⧫",blacksquare:"▪",blacktriangle:"▴",blacktriangledown:"▾",blacktriangleleft:"◂",blacktriangleright:"▸",blank:"␣",blk12:"▒",blk14:"░",blk34:"▓",block:"█",bne:"=⃥",bnequiv:"≡⃥",bNot:"⫭",bnot:"⌐",Bopf:"𝔹",bopf:"𝕓",bot:"⊥",bottom:"⊥",bowtie:"⋈",boxbox:"⧉",boxdl:"┐",boxdL:"╕",boxDl:"╖",boxDL:"╗",boxdr:"┌",boxdR:"╒",boxDr:"╓",boxDR:"╔",boxh:"─",boxH:"═",boxhd:"┬",boxHd:"╤",boxhD:"╥",boxHD:"╦",boxhu:"┴",boxHu:"╧",boxhU:"╨",boxHU:"╩",boxminus:"⊟",boxplus:"⊞",boxtimes:"⊠",boxul:"┘",boxuL:"╛",boxUl:"╜",boxUL:"╝",boxur:"└",boxuR:"╘",boxUr:"╙",boxUR:"╚",boxv:"│",boxV:"║",boxvh:"┼",boxvH:"╪",boxVh:"╫",boxVH:"╬",boxvl:"┤",boxvL:"╡",boxVl:"╢",boxVL:"╣",boxvr:"├",boxvR:"╞",boxVr:"╟",boxVR:"╠",bprime:"‵",breve:"˘",Breve:"˘",brvbar:"¦",bscr:"𝒷",Bscr:"ℬ",bsemi:"⁏",bsim:"∽",bsime:"⋍",bsolb:"⧅",bsol:"\\",bsolhsub:"⟈",bull:"•",bullet:"•",bump:"≎",bumpE:"⪮",bumpe:"≏",Bumpeq:"≎",bumpeq:"≏",Cacute:"Ć",cacute:"ć",capand:"⩄",capbrcup:"⩉",capcap:"⩋",cap:"∩",Cap:"⋒",capcup:"⩇",capdot:"⩀",CapitalDifferentialD:"ⅅ",caps:"∩︀",caret:"⁁",caron:"ˇ",Cayleys:"ℭ",ccaps:"⩍",Ccaron:"Č",ccaron:"č",Ccedil:"Ç",ccedil:"ç",Ccirc:"Ĉ",ccirc:"ĉ",Cconint:"∰",ccups:"⩌",ccupssm:"⩐",Cdot:"Ċ",cdot:"ċ",cedil:"¸",Cedilla:"¸",cemptyv:"⦲",cent:"¢",centerdot:"·",CenterDot:"·",cfr:"𝔠",Cfr:"ℭ",CHcy:"Ч",chcy:"ч",check:"✓",checkmark:"✓",Chi:"Χ",chi:"χ",circ:"ˆ",circeq:"≗",circlearrowleft:"↺",circlearrowright:"↻",circledast:"⊛",circledcirc:"⊚",circleddash:"⊝",CircleDot:"⊙",circledR:"®",circledS:"Ⓢ",CircleMinus:"⊖",CirclePlus:"⊕",CircleTimes:"⊗",cir:"○",cirE:"⧃",cire:"≗",cirfnint:"⨐",cirmid:"⫯",cirscir:"⧂",ClockwiseContourIntegral:"∲",CloseCurlyDoubleQuote:"”",CloseCurlyQuote:"’",clubs:"♣",clubsuit:"♣",colon:":",Colon:"∷",Colone:"⩴",colone:"≔",coloneq:"≔",comma:",",commat:"@",comp:"∁",compfn:"∘",complement:"∁",complexes:"ℂ",cong:"≅",congdot:"⩭",Congruent:"≡",conint:"∮",Conint:"∯",ContourIntegral:"∮",copf:"𝕔",Copf:"ℂ",coprod:"∐",Coproduct:"∐",copy:"©",COPY:"©",copysr:"℗",CounterClockwiseContourIntegral:"∳",crarr:"↵",cross:"✗",Cross:"⨯",Cscr:"𝒞",cscr:"𝒸",csub:"⫏",csube:"⫑",csup:"⫐",csupe:"⫒",ctdot:"⋯",cudarrl:"⤸",cudarrr:"⤵",cuepr:"⋞",cuesc:"⋟",cularr:"↶",cularrp:"⤽",cupbrcap:"⩈",cupcap:"⩆",CupCap:"≍",cup:"∪",Cup:"⋓",cupcup:"⩊",cupdot:"⊍",cupor:"⩅",cups:"∪︀",curarr:"↷",curarrm:"⤼",curlyeqprec:"⋞",curlyeqsucc:"⋟",curlyvee:"⋎",curlywedge:"⋏",curren:"¤",curvearrowleft:"↶",curvearrowright:"↷",cuvee:"⋎",cuwed:"⋏",cwconint:"∲",cwint:"∱",cylcty:"⌭",dagger:"†",Dagger:"‡",daleth:"ℸ",darr:"↓",Darr:"↡",dArr:"⇓",dash:"‐",Dashv:"⫤",dashv:"⊣",dbkarow:"⤏",dblac:"˝",Dcaron:"Ď",dcaron:"ď",Dcy:"Д",dcy:"д",ddagger:"‡",ddarr:"⇊",DD:"ⅅ",dd:"ⅆ",DDotrahd:"⤑",ddotseq:"⩷",deg:"°",Del:"∇",Delta:"Δ",delta:"δ",demptyv:"⦱",dfisht:"⥿",Dfr:"𝔇",dfr:"𝔡",dHar:"⥥",dharl:"⇃",dharr:"⇂",DiacriticalAcute:"´",DiacriticalDot:"˙",DiacriticalDoubleAcute:"˝",DiacriticalGrave:"`",DiacriticalTilde:"˜",diam:"⋄",diamond:"⋄",Diamond:"⋄",diamondsuit:"♦",diams:"♦",die:"¨",DifferentialD:"ⅆ",digamma:"ϝ",disin:"⋲",div:"÷",divide:"÷",divideontimes:"⋇",divonx:"⋇",DJcy:"Ђ",djcy:"ђ",dlcorn:"⌞",dlcrop:"⌍",dollar:"$",Dopf:"𝔻",dopf:"𝕕",Dot:"¨",dot:"˙",DotDot:"⃜",doteq:"≐",doteqdot:"≑",DotEqual:"≐",dotminus:"∸",dotplus:"∔",dotsquare:"⊡",doublebarwedge:"⌆",DoubleContourIntegral:"∯",DoubleDot:"¨",DoubleDownArrow:"⇓",DoubleLeftArrow:"⇐",DoubleLeftRightArrow:"⇔",DoubleLeftTee:"⫤",DoubleLongLeftArrow:"⟸",DoubleLongLeftRightArrow:"⟺",DoubleLongRightArrow:"⟹",DoubleRightArrow:"⇒",DoubleRightTee:"⊨",DoubleUpArrow:"⇑",DoubleUpDownArrow:"⇕",DoubleVerticalBar:"∥",DownArrowBar:"⤓",downarrow:"↓",DownArrow:"↓",Downarrow:"⇓",DownArrowUpArrow:"⇵",DownBreve:"̑",downdownarrows:"⇊",downharpoonleft:"⇃",downharpoonright:"⇂",DownLeftRightVector:"⥐",DownLeftTeeVector:"⥞",DownLeftVectorBar:"⥖",DownLeftVector:"↽",DownRightTeeVector:"⥟",DownRightVectorBar:"⥗",DownRightVector:"⇁",DownTeeArrow:"↧",DownTee:"⊤",drbkarow:"⤐",drcorn:"⌟",drcrop:"⌌",Dscr:"𝒟",dscr:"𝒹",DScy:"Ѕ",dscy:"ѕ",dsol:"⧶",Dstrok:"Đ",dstrok:"đ",dtdot:"⋱",dtri:"▿",dtrif:"▾",duarr:"⇵",duhar:"⥯",dwangle:"⦦",DZcy:"Џ",dzcy:"џ",dzigrarr:"⟿",Eacute:"É",eacute:"é",easter:"⩮",Ecaron:"Ě",ecaron:"ě",Ecirc:"Ê",ecirc:"ê",ecir:"≖",ecolon:"≕",Ecy:"Э",ecy:"э",eDDot:"⩷",Edot:"Ė",edot:"ė",eDot:"≑",ee:"ⅇ",efDot:"≒",Efr:"𝔈",efr:"𝔢",eg:"⪚",Egrave:"È",egrave:"è",egs:"⪖",egsdot:"⪘",el:"⪙",Element:"∈",elinters:"⏧",ell:"ℓ",els:"⪕",elsdot:"⪗",Emacr:"Ē",emacr:"ē",empty:"∅",emptyset:"∅",EmptySmallSquare:"◻",emptyv:"∅",EmptyVerySmallSquare:"▫",emsp13:" ",emsp14:" ",emsp:" ",ENG:"Ŋ",eng:"ŋ",ensp:" ",Eogon:"Ę",eogon:"ę",Eopf:"𝔼",eopf:"𝕖",epar:"⋕",eparsl:"⧣",eplus:"⩱",epsi:"ε",Epsilon:"Ε",epsilon:"ε",epsiv:"ϵ",eqcirc:"≖",eqcolon:"≕",eqsim:"≂",eqslantgtr:"⪖",eqslantless:"⪕",Equal:"⩵",equals:"=",EqualTilde:"≂",equest:"≟",Equilibrium:"⇌",equiv:"≡",equivDD:"⩸",eqvparsl:"⧥",erarr:"⥱",erDot:"≓",escr:"ℯ",Escr:"ℰ",esdot:"≐",Esim:"⩳",esim:"≂",Eta:"Η",eta:"η",ETH:"Ð",eth:"ð",Euml:"Ë",euml:"ë",euro:"€",excl:"!",exist:"∃",Exists:"∃",expectation:"ℰ",exponentiale:"ⅇ",ExponentialE:"ⅇ",fallingdotseq:"≒",Fcy:"Ф",fcy:"ф",female:"♀",ffilig:"ffi",fflig:"ff",ffllig:"ffl",Ffr:"𝔉",ffr:"𝔣",filig:"fi",FilledSmallSquare:"◼",FilledVerySmallSquare:"▪",fjlig:"fj",flat:"♭",fllig:"fl",fltns:"▱",fnof:"ƒ",Fopf:"𝔽",fopf:"𝕗",forall:"∀",ForAll:"∀",fork:"⋔",forkv:"⫙",Fouriertrf:"ℱ",fpartint:"⨍",frac12:"½",frac13:"⅓",frac14:"¼",frac15:"⅕",frac16:"⅙",frac18:"⅛",frac23:"⅔",frac25:"⅖",frac34:"¾",frac35:"⅗",frac38:"⅜",frac45:"⅘",frac56:"⅚",frac58:"⅝",frac78:"⅞",frasl:"⁄",frown:"⌢",fscr:"𝒻",Fscr:"ℱ",gacute:"ǵ",Gamma:"Γ",gamma:"γ",Gammad:"Ϝ",gammad:"ϝ",gap:"⪆",Gbreve:"Ğ",gbreve:"ğ",Gcedil:"Ģ",Gcirc:"Ĝ",gcirc:"ĝ",Gcy:"Г",gcy:"г",Gdot:"Ġ",gdot:"ġ",ge:"≥",gE:"≧",gEl:"⪌",gel:"⋛",geq:"≥",geqq:"≧",geqslant:"⩾",gescc:"⪩",ges:"⩾",gesdot:"⪀",gesdoto:"⪂",gesdotol:"⪄",gesl:"⋛︀",gesles:"⪔",Gfr:"𝔊",gfr:"𝔤",gg:"≫",Gg:"⋙",ggg:"⋙",gimel:"ℷ",GJcy:"Ѓ",gjcy:"ѓ",gla:"⪥",gl:"≷",glE:"⪒",glj:"⪤",gnap:"⪊",gnapprox:"⪊",gne:"⪈",gnE:"≩",gneq:"⪈",gneqq:"≩",gnsim:"⋧",Gopf:"𝔾",gopf:"𝕘",grave:"`",GreaterEqual:"≥",GreaterEqualLess:"⋛",GreaterFullEqual:"≧",GreaterGreater:"⪢",GreaterLess:"≷",GreaterSlantEqual:"⩾",GreaterTilde:"≳",Gscr:"𝒢",gscr:"ℊ",gsim:"≳",gsime:"⪎",gsiml:"⪐",gtcc:"⪧",gtcir:"⩺",gt:">",GT:">",Gt:"≫",gtdot:"⋗",gtlPar:"⦕",gtquest:"⩼",gtrapprox:"⪆",gtrarr:"⥸",gtrdot:"⋗",gtreqless:"⋛",gtreqqless:"⪌",gtrless:"≷",gtrsim:"≳",gvertneqq:"≩︀",gvnE:"≩︀",Hacek:"ˇ",hairsp:" ",half:"½",hamilt:"ℋ",HARDcy:"Ъ",hardcy:"ъ",harrcir:"⥈",harr:"↔",hArr:"⇔",harrw:"↭",Hat:"^",hbar:"ℏ",Hcirc:"Ĥ",hcirc:"ĥ",hearts:"♥",heartsuit:"♥",hellip:"…",hercon:"⊹",hfr:"𝔥",Hfr:"ℌ",HilbertSpace:"ℋ",hksearow:"⤥",hkswarow:"⤦",hoarr:"⇿",homtht:"∻",hookleftarrow:"↩",hookrightarrow:"↪",hopf:"𝕙",Hopf:"ℍ",horbar:"―",HorizontalLine:"─",hscr:"𝒽",Hscr:"ℋ",hslash:"ℏ",Hstrok:"Ħ",hstrok:"ħ",HumpDownHump:"≎",HumpEqual:"≏",hybull:"⁃",hyphen:"‐",Iacute:"Í",iacute:"í",ic:"⁣",Icirc:"Î",icirc:"î",Icy:"И",icy:"и",Idot:"İ",IEcy:"Е",iecy:"е",iexcl:"¡",iff:"⇔",ifr:"𝔦",Ifr:"ℑ",Igrave:"Ì",igrave:"ì",ii:"ⅈ",iiiint:"⨌",iiint:"∭",iinfin:"⧜",iiota:"℩",IJlig:"IJ",ijlig:"ij",Imacr:"Ī",imacr:"ī",image:"ℑ",ImaginaryI:"ⅈ",imagline:"ℐ",imagpart:"ℑ",imath:"ı",Im:"ℑ",imof:"⊷",imped:"Ƶ",Implies:"⇒",incare:"℅",in:"∈",infin:"∞",infintie:"⧝",inodot:"ı",intcal:"⊺",int:"∫",Int:"∬",integers:"ℤ",Integral:"∫",intercal:"⊺",Intersection:"⋂",intlarhk:"⨗",intprod:"⨼",InvisibleComma:"⁣",InvisibleTimes:"⁢",IOcy:"Ё",iocy:"ё",Iogon:"Į",iogon:"į",Iopf:"𝕀",iopf:"𝕚",Iota:"Ι",iota:"ι",iprod:"⨼",iquest:"¿",iscr:"𝒾",Iscr:"ℐ",isin:"∈",isindot:"⋵",isinE:"⋹",isins:"⋴",isinsv:"⋳",isinv:"∈",it:"⁢",Itilde:"Ĩ",itilde:"ĩ",Iukcy:"І",iukcy:"і",Iuml:"Ï",iuml:"ï",Jcirc:"Ĵ",jcirc:"ĵ",Jcy:"Й",jcy:"й",Jfr:"𝔍",jfr:"𝔧",jmath:"ȷ",Jopf:"𝕁",jopf:"𝕛",Jscr:"𝒥",jscr:"𝒿",Jsercy:"Ј",jsercy:"ј",Jukcy:"Є",jukcy:"є",Kappa:"Κ",kappa:"κ",kappav:"ϰ",Kcedil:"Ķ",kcedil:"ķ",Kcy:"К",kcy:"к",Kfr:"𝔎",kfr:"𝔨",kgreen:"ĸ",KHcy:"Х",khcy:"х",KJcy:"Ќ",kjcy:"ќ",Kopf:"𝕂",kopf:"𝕜",Kscr:"𝒦",kscr:"𝓀",lAarr:"⇚",Lacute:"Ĺ",lacute:"ĺ",laemptyv:"⦴",lagran:"ℒ",Lambda:"Λ",lambda:"λ",lang:"⟨",Lang:"⟪",langd:"⦑",langle:"⟨",lap:"⪅",Laplacetrf:"ℒ",laquo:"«",larrb:"⇤",larrbfs:"⤟",larr:"←",Larr:"↞",lArr:"⇐",larrfs:"⤝",larrhk:"↩",larrlp:"↫",larrpl:"⤹",larrsim:"⥳",larrtl:"↢",latail:"⤙",lAtail:"⤛",lat:"⪫",late:"⪭",lates:"⪭︀",lbarr:"⤌",lBarr:"⤎",lbbrk:"❲",lbrace:"{",lbrack:"[",lbrke:"⦋",lbrksld:"⦏",lbrkslu:"⦍",Lcaron:"Ľ",lcaron:"ľ",Lcedil:"Ļ",lcedil:"ļ",lceil:"⌈",lcub:"{",Lcy:"Л",lcy:"л",ldca:"⤶",ldquo:"“",ldquor:"„",ldrdhar:"⥧",ldrushar:"⥋",ldsh:"↲",le:"≤",lE:"≦",LeftAngleBracket:"⟨",LeftArrowBar:"⇤",leftarrow:"←",LeftArrow:"←",Leftarrow:"⇐",LeftArrowRightArrow:"⇆",leftarrowtail:"↢",LeftCeiling:"⌈",LeftDoubleBracket:"⟦",LeftDownTeeVector:"⥡",LeftDownVectorBar:"⥙",LeftDownVector:"⇃",LeftFloor:"⌊",leftharpoondown:"↽",leftharpoonup:"↼",leftleftarrows:"⇇",leftrightarrow:"↔",LeftRightArrow:"↔",Leftrightarrow:"⇔",leftrightarrows:"⇆",leftrightharpoons:"⇋",leftrightsquigarrow:"↭",LeftRightVector:"⥎",LeftTeeArrow:"↤",LeftTee:"⊣",LeftTeeVector:"⥚",leftthreetimes:"⋋",LeftTriangleBar:"⧏",LeftTriangle:"⊲",LeftTriangleEqual:"⊴",LeftUpDownVector:"⥑",LeftUpTeeVector:"⥠",LeftUpVectorBar:"⥘",LeftUpVector:"↿",LeftVectorBar:"⥒",LeftVector:"↼",lEg:"⪋",leg:"⋚",leq:"≤",leqq:"≦",leqslant:"⩽",lescc:"⪨",les:"⩽",lesdot:"⩿",lesdoto:"⪁",lesdotor:"⪃",lesg:"⋚︀",lesges:"⪓",lessapprox:"⪅",lessdot:"⋖",lesseqgtr:"⋚",lesseqqgtr:"⪋",LessEqualGreater:"⋚",LessFullEqual:"≦",LessGreater:"≶",lessgtr:"≶",LessLess:"⪡",lesssim:"≲",LessSlantEqual:"⩽",LessTilde:"≲",lfisht:"⥼",lfloor:"⌊",Lfr:"𝔏",lfr:"𝔩",lg:"≶",lgE:"⪑",lHar:"⥢",lhard:"↽",lharu:"↼",lharul:"⥪",lhblk:"▄",LJcy:"Љ",ljcy:"љ",llarr:"⇇",ll:"≪",Ll:"⋘",llcorner:"⌞",Lleftarrow:"⇚",llhard:"⥫",lltri:"◺",Lmidot:"Ŀ",lmidot:"ŀ",lmoustache:"⎰",lmoust:"⎰",lnap:"⪉",lnapprox:"⪉",lne:"⪇",lnE:"≨",lneq:"⪇",lneqq:"≨",lnsim:"⋦",loang:"⟬",loarr:"⇽",lobrk:"⟦",longleftarrow:"⟵",LongLeftArrow:"⟵",Longleftarrow:"⟸",longleftrightarrow:"⟷",LongLeftRightArrow:"⟷",Longleftrightarrow:"⟺",longmapsto:"⟼",longrightarrow:"⟶",LongRightArrow:"⟶",Longrightarrow:"⟹",looparrowleft:"↫",looparrowright:"↬",lopar:"⦅",Lopf:"𝕃",lopf:"𝕝",loplus:"⨭",lotimes:"⨴",lowast:"∗",lowbar:"_",LowerLeftArrow:"↙",LowerRightArrow:"↘",loz:"◊",lozenge:"◊",lozf:"⧫",lpar:"(",lparlt:"⦓",lrarr:"⇆",lrcorner:"⌟",lrhar:"⇋",lrhard:"⥭",lrm:"‎",lrtri:"⊿",lsaquo:"‹",lscr:"𝓁",Lscr:"ℒ",lsh:"↰",Lsh:"↰",lsim:"≲",lsime:"⪍",lsimg:"⪏",lsqb:"[",lsquo:"‘",lsquor:"‚",Lstrok:"Ł",lstrok:"ł",ltcc:"⪦",ltcir:"⩹",lt:"<",LT:"<",Lt:"≪",ltdot:"⋖",lthree:"⋋",ltimes:"⋉",ltlarr:"⥶",ltquest:"⩻",ltri:"◃",ltrie:"⊴",ltrif:"◂",ltrPar:"⦖",lurdshar:"⥊",luruhar:"⥦",lvertneqq:"≨︀",lvnE:"≨︀",macr:"¯",male:"♂",malt:"✠",maltese:"✠",Map:"⤅",map:"↦",mapsto:"↦",mapstodown:"↧",mapstoleft:"↤",mapstoup:"↥",marker:"▮",mcomma:"⨩",Mcy:"М",mcy:"м",mdash:"—",mDDot:"∺",measuredangle:"∡",MediumSpace:" ",Mellintrf:"ℳ",Mfr:"𝔐",mfr:"𝔪",mho:"℧",micro:"µ",midast:"*",midcir:"⫰",mid:"∣",middot:"·",minusb:"⊟",minus:"−",minusd:"∸",minusdu:"⨪",MinusPlus:"∓",mlcp:"⫛",mldr:"…",mnplus:"∓",models:"⊧",Mopf:"𝕄",mopf:"𝕞",mp:"∓",mscr:"𝓂",Mscr:"ℳ",mstpos:"∾",Mu:"Μ",mu:"μ",multimap:"⊸",mumap:"⊸",nabla:"∇",Nacute:"Ń",nacute:"ń",nang:"∠⃒",nap:"≉",napE:"⩰̸",napid:"≋̸",napos:"ʼn",napprox:"≉",natural:"♮",naturals:"ℕ",natur:"♮",nbsp:" ",nbump:"≎̸",nbumpe:"≏̸",ncap:"⩃",Ncaron:"Ň",ncaron:"ň",Ncedil:"Ņ",ncedil:"ņ",ncong:"≇",ncongdot:"⩭̸",ncup:"⩂",Ncy:"Н",ncy:"н",ndash:"–",nearhk:"⤤",nearr:"↗",neArr:"⇗",nearrow:"↗",ne:"≠",nedot:"≐̸",NegativeMediumSpace:"​",NegativeThickSpace:"​",NegativeThinSpace:"​",NegativeVeryThinSpace:"​",nequiv:"≢",nesear:"⤨",nesim:"≂̸",NestedGreaterGreater:"≫",NestedLessLess:"≪",NewLine:"\n",nexist:"∄",nexists:"∄",Nfr:"𝔑",nfr:"𝔫",ngE:"≧̸",nge:"≱",ngeq:"≱",ngeqq:"≧̸",ngeqslant:"⩾̸",nges:"⩾̸",nGg:"⋙̸",ngsim:"≵",nGt:"≫⃒",ngt:"≯",ngtr:"≯",nGtv:"≫̸",nharr:"↮",nhArr:"⇎",nhpar:"⫲",ni:"∋",nis:"⋼",nisd:"⋺",niv:"∋",NJcy:"Њ",njcy:"њ",nlarr:"↚",nlArr:"⇍",nldr:"‥",nlE:"≦̸",nle:"≰",nleftarrow:"↚",nLeftarrow:"⇍",nleftrightarrow:"↮",nLeftrightarrow:"⇎",nleq:"≰",nleqq:"≦̸",nleqslant:"⩽̸",nles:"⩽̸",nless:"≮",nLl:"⋘̸",nlsim:"≴",nLt:"≪⃒",nlt:"≮",nltri:"⋪",nltrie:"⋬",nLtv:"≪̸",nmid:"∤",NoBreak:"⁠",NonBreakingSpace:" ",nopf:"𝕟",Nopf:"ℕ",Not:"⫬",not:"¬",NotCongruent:"≢",NotCupCap:"≭",NotDoubleVerticalBar:"∦",NotElement:"∉",NotEqual:"≠",NotEqualTilde:"≂̸",NotExists:"∄",NotGreater:"≯",NotGreaterEqual:"≱",NotGreaterFullEqual:"≧̸",NotGreaterGreater:"≫̸",NotGreaterLess:"≹",NotGreaterSlantEqual:"⩾̸",NotGreaterTilde:"≵",NotHumpDownHump:"≎̸",NotHumpEqual:"≏̸",notin:"∉",notindot:"⋵̸",notinE:"⋹̸",notinva:"∉",notinvb:"⋷",notinvc:"⋶",NotLeftTriangleBar:"⧏̸",NotLeftTriangle:"⋪",NotLeftTriangleEqual:"⋬",NotLess:"≮",NotLessEqual:"≰",NotLessGreater:"≸",NotLessLess:"≪̸",NotLessSlantEqual:"⩽̸",NotLessTilde:"≴",NotNestedGreaterGreater:"⪢̸",NotNestedLessLess:"⪡̸",notni:"∌",notniva:"∌",notnivb:"⋾",notnivc:"⋽",NotPrecedes:"⊀",NotPrecedesEqual:"⪯̸",NotPrecedesSlantEqual:"⋠",NotReverseElement:"∌",NotRightTriangleBar:"⧐̸",NotRightTriangle:"⋫",NotRightTriangleEqual:"⋭",NotSquareSubset:"⊏̸",NotSquareSubsetEqual:"⋢",NotSquareSuperset:"⊐̸",NotSquareSupersetEqual:"⋣",NotSubset:"⊂⃒",NotSubsetEqual:"⊈",NotSucceeds:"⊁",NotSucceedsEqual:"⪰̸",NotSucceedsSlantEqual:"⋡",NotSucceedsTilde:"≿̸",NotSuperset:"⊃⃒",NotSupersetEqual:"⊉",NotTilde:"≁",NotTildeEqual:"≄",NotTildeFullEqual:"≇",NotTildeTilde:"≉",NotVerticalBar:"∤",nparallel:"∦",npar:"∦",nparsl:"⫽⃥",npart:"∂̸",npolint:"⨔",npr:"⊀",nprcue:"⋠",nprec:"⊀",npreceq:"⪯̸",npre:"⪯̸",nrarrc:"⤳̸",nrarr:"↛",nrArr:"⇏",nrarrw:"↝̸",nrightarrow:"↛",nRightarrow:"⇏",nrtri:"⋫",nrtrie:"⋭",nsc:"⊁",nsccue:"⋡",nsce:"⪰̸",Nscr:"𝒩",nscr:"𝓃",nshortmid:"∤",nshortparallel:"∦",nsim:"≁",nsime:"≄",nsimeq:"≄",nsmid:"∤",nspar:"∦",nsqsube:"⋢",nsqsupe:"⋣",nsub:"⊄",nsubE:"⫅̸",nsube:"⊈",nsubset:"⊂⃒",nsubseteq:"⊈",nsubseteqq:"⫅̸",nsucc:"⊁",nsucceq:"⪰̸",nsup:"⊅",nsupE:"⫆̸",nsupe:"⊉",nsupset:"⊃⃒",nsupseteq:"⊉",nsupseteqq:"⫆̸",ntgl:"≹",Ntilde:"Ñ",ntilde:"ñ",ntlg:"≸",ntriangleleft:"⋪",ntrianglelefteq:"⋬",ntriangleright:"⋫",ntrianglerighteq:"⋭",Nu:"Ν",nu:"ν",num:"#",numero:"№",numsp:" ",nvap:"≍⃒",nvdash:"⊬",nvDash:"⊭",nVdash:"⊮",nVDash:"⊯",nvge:"≥⃒",nvgt:">⃒",nvHarr:"⤄",nvinfin:"⧞",nvlArr:"⤂",nvle:"≤⃒",nvlt:"<⃒",nvltrie:"⊴⃒",nvrArr:"⤃",nvrtrie:"⊵⃒",nvsim:"∼⃒",nwarhk:"⤣",nwarr:"↖",nwArr:"⇖",nwarrow:"↖",nwnear:"⤧",Oacute:"Ó",oacute:"ó",oast:"⊛",Ocirc:"Ô",ocirc:"ô",ocir:"⊚",Ocy:"О",ocy:"о",odash:"⊝",Odblac:"Ő",odblac:"ő",odiv:"⨸",odot:"⊙",odsold:"⦼",OElig:"Œ",oelig:"œ",ofcir:"⦿",Ofr:"𝔒",ofr:"𝔬",ogon:"˛",Ograve:"Ò",ograve:"ò",ogt:"⧁",ohbar:"⦵",ohm:"Ω",oint:"∮",olarr:"↺",olcir:"⦾",olcross:"⦻",oline:"‾",olt:"⧀",Omacr:"Ō",omacr:"ō",Omega:"Ω",omega:"ω",Omicron:"Ο",omicron:"ο",omid:"⦶",ominus:"⊖",Oopf:"𝕆",oopf:"𝕠",opar:"⦷",OpenCurlyDoubleQuote:"“",OpenCurlyQuote:"‘",operp:"⦹",oplus:"⊕",orarr:"↻",Or:"⩔",or:"∨",ord:"⩝",order:"ℴ",orderof:"ℴ",ordf:"ª",ordm:"º",origof:"⊶",oror:"⩖",orslope:"⩗",orv:"⩛",oS:"Ⓢ",Oscr:"𝒪",oscr:"ℴ",Oslash:"Ø",oslash:"ø",osol:"⊘",Otilde:"Õ",otilde:"õ",otimesas:"⨶",Otimes:"⨷",otimes:"⊗",Ouml:"Ö",ouml:"ö",ovbar:"⌽",OverBar:"‾",OverBrace:"⏞",OverBracket:"⎴",OverParenthesis:"⏜",para:"¶",parallel:"∥",par:"∥",parsim:"⫳",parsl:"⫽",part:"∂",PartialD:"∂",Pcy:"П",pcy:"п",percnt:"%",period:".",permil:"‰",perp:"⊥",pertenk:"‱",Pfr:"𝔓",pfr:"𝔭",Phi:"Φ",phi:"φ",phiv:"ϕ",phmmat:"ℳ",phone:"☎",Pi:"Π",pi:"π",pitchfork:"⋔",piv:"ϖ",planck:"ℏ",planckh:"ℎ",plankv:"ℏ",plusacir:"⨣",plusb:"⊞",pluscir:"⨢",plus:"+",plusdo:"∔",plusdu:"⨥",pluse:"⩲",PlusMinus:"±",plusmn:"±",plussim:"⨦",plustwo:"⨧",pm:"±",Poincareplane:"ℌ",pointint:"⨕",popf:"𝕡",Popf:"ℙ",pound:"£",prap:"⪷",Pr:"⪻",pr:"≺",prcue:"≼",precapprox:"⪷",prec:"≺",preccurlyeq:"≼",Precedes:"≺",PrecedesEqual:"⪯",PrecedesSlantEqual:"≼",PrecedesTilde:"≾",preceq:"⪯",precnapprox:"⪹",precneqq:"⪵",precnsim:"⋨",pre:"⪯",prE:"⪳",precsim:"≾",prime:"′",Prime:"″",primes:"ℙ",prnap:"⪹",prnE:"⪵",prnsim:"⋨",prod:"∏",Product:"∏",profalar:"⌮",profline:"⌒",profsurf:"⌓",prop:"∝",Proportional:"∝",Proportion:"∷",propto:"∝",prsim:"≾",prurel:"⊰",Pscr:"𝒫",pscr:"𝓅",Psi:"Ψ",psi:"ψ",puncsp:" ",Qfr:"𝔔",qfr:"𝔮",qint:"⨌",qopf:"𝕢",Qopf:"ℚ",qprime:"⁗",Qscr:"𝒬",qscr:"𝓆",quaternions:"ℍ",quatint:"⨖",quest:"?",questeq:"≟",quot:'"',QUOT:'"',rAarr:"⇛",race:"∽̱",Racute:"Ŕ",racute:"ŕ",radic:"√",raemptyv:"⦳",rang:"⟩",Rang:"⟫",rangd:"⦒",range:"⦥",rangle:"⟩",raquo:"»",rarrap:"⥵",rarrb:"⇥",rarrbfs:"⤠",rarrc:"⤳",rarr:"→",Rarr:"↠",rArr:"⇒",rarrfs:"⤞",rarrhk:"↪",rarrlp:"↬",rarrpl:"⥅",rarrsim:"⥴",Rarrtl:"⤖",rarrtl:"↣",rarrw:"↝",ratail:"⤚",rAtail:"⤜",ratio:"∶",rationals:"ℚ",rbarr:"⤍",rBarr:"⤏",RBarr:"⤐",rbbrk:"❳",rbrace:"}",rbrack:"]",rbrke:"⦌",rbrksld:"⦎",rbrkslu:"⦐",Rcaron:"Ř",rcaron:"ř",Rcedil:"Ŗ",rcedil:"ŗ",rceil:"⌉",rcub:"}",Rcy:"Р",rcy:"р",rdca:"⤷",rdldhar:"⥩",rdquo:"”",rdquor:"”",rdsh:"↳",real:"ℜ",realine:"ℛ",realpart:"ℜ",reals:"ℝ",Re:"ℜ",rect:"▭",reg:"®",REG:"®",ReverseElement:"∋",ReverseEquilibrium:"⇋",ReverseUpEquilibrium:"⥯",rfisht:"⥽",rfloor:"⌋",rfr:"𝔯",Rfr:"ℜ",rHar:"⥤",rhard:"⇁",rharu:"⇀",rharul:"⥬",Rho:"Ρ",rho:"ρ",rhov:"ϱ",RightAngleBracket:"⟩",RightArrowBar:"⇥",rightarrow:"→",RightArrow:"→",Rightarrow:"⇒",RightArrowLeftArrow:"⇄",rightarrowtail:"↣",RightCeiling:"⌉",RightDoubleBracket:"⟧",RightDownTeeVector:"⥝",RightDownVectorBar:"⥕",RightDownVector:"⇂",RightFloor:"⌋",rightharpoondown:"⇁",rightharpoonup:"⇀",rightleftarrows:"⇄",rightleftharpoons:"⇌",rightrightarrows:"⇉",rightsquigarrow:"↝",RightTeeArrow:"↦",RightTee:"⊢",RightTeeVector:"⥛",rightthreetimes:"⋌",RightTriangleBar:"⧐",RightTriangle:"⊳",RightTriangleEqual:"⊵",RightUpDownVector:"⥏",RightUpTeeVector:"⥜",RightUpVectorBar:"⥔",RightUpVector:"↾",RightVectorBar:"⥓",RightVector:"⇀",ring:"˚",risingdotseq:"≓",rlarr:"⇄",rlhar:"⇌",rlm:"‏",rmoustache:"⎱",rmoust:"⎱",rnmid:"⫮",roang:"⟭",roarr:"⇾",robrk:"⟧",ropar:"⦆",ropf:"𝕣",Ropf:"ℝ",roplus:"⨮",rotimes:"⨵",RoundImplies:"⥰",rpar:")",rpargt:"⦔",rppolint:"⨒",rrarr:"⇉",Rrightarrow:"⇛",rsaquo:"›",rscr:"𝓇",Rscr:"ℛ",rsh:"↱",Rsh:"↱",rsqb:"]",rsquo:"’",rsquor:"’",rthree:"⋌",rtimes:"⋊",rtri:"▹",rtrie:"⊵",rtrif:"▸",rtriltri:"⧎",RuleDelayed:"⧴",ruluhar:"⥨",rx:"℞",Sacute:"Ś",sacute:"ś",sbquo:"‚",scap:"⪸",Scaron:"Š",scaron:"š",Sc:"⪼",sc:"≻",sccue:"≽",sce:"⪰",scE:"⪴",Scedil:"Ş",scedil:"ş",Scirc:"Ŝ",scirc:"ŝ",scnap:"⪺",scnE:"⪶",scnsim:"⋩",scpolint:"⨓",scsim:"≿",Scy:"С",scy:"с",sdotb:"⊡",sdot:"⋅",sdote:"⩦",searhk:"⤥",searr:"↘",seArr:"⇘",searrow:"↘",sect:"§",semi:";",seswar:"⤩",setminus:"∖",setmn:"∖",sext:"✶",Sfr:"𝔖",sfr:"𝔰",sfrown:"⌢",sharp:"♯",SHCHcy:"Щ",shchcy:"щ",SHcy:"Ш",shcy:"ш",ShortDownArrow:"↓",ShortLeftArrow:"←",shortmid:"∣",shortparallel:"∥",ShortRightArrow:"→",ShortUpArrow:"↑",shy:"­",Sigma:"Σ",sigma:"σ",sigmaf:"ς",sigmav:"ς",sim:"∼",simdot:"⩪",sime:"≃",simeq:"≃",simg:"⪞",simgE:"⪠",siml:"⪝",simlE:"⪟",simne:"≆",simplus:"⨤",simrarr:"⥲",slarr:"←",SmallCircle:"∘",smallsetminus:"∖",smashp:"⨳",smeparsl:"⧤",smid:"∣",smile:"⌣",smt:"⪪",smte:"⪬",smtes:"⪬︀",SOFTcy:"Ь",softcy:"ь",solbar:"⌿",solb:"⧄",sol:"/",Sopf:"𝕊",sopf:"𝕤",spades:"♠",spadesuit:"♠",spar:"∥",sqcap:"⊓",sqcaps:"⊓︀",sqcup:"⊔",sqcups:"⊔︀",Sqrt:"√",sqsub:"⊏",sqsube:"⊑",sqsubset:"⊏",sqsubseteq:"⊑",sqsup:"⊐",sqsupe:"⊒",sqsupset:"⊐",sqsupseteq:"⊒",square:"□",Square:"□",SquareIntersection:"⊓",SquareSubset:"⊏",SquareSubsetEqual:"⊑",SquareSuperset:"⊐",SquareSupersetEqual:"⊒",SquareUnion:"⊔",squarf:"▪",squ:"□",squf:"▪",srarr:"→",Sscr:"𝒮",sscr:"𝓈",ssetmn:"∖",ssmile:"⌣",sstarf:"⋆",Star:"⋆",star:"☆",starf:"★",straightepsilon:"ϵ",straightphi:"ϕ",strns:"¯",sub:"⊂",Sub:"⋐",subdot:"⪽",subE:"⫅",sube:"⊆",subedot:"⫃",submult:"⫁",subnE:"⫋",subne:"⊊",subplus:"⪿",subrarr:"⥹",subset:"⊂",Subset:"⋐",subseteq:"⊆",subseteqq:"⫅",SubsetEqual:"⊆",subsetneq:"⊊",subsetneqq:"⫋",subsim:"⫇",subsub:"⫕",subsup:"⫓",succapprox:"⪸",succ:"≻",succcurlyeq:"≽",Succeeds:"≻",SucceedsEqual:"⪰",SucceedsSlantEqual:"≽",SucceedsTilde:"≿",succeq:"⪰",succnapprox:"⪺",succneqq:"⪶",succnsim:"⋩",succsim:"≿",SuchThat:"∋",sum:"∑",Sum:"∑",sung:"♪",sup1:"¹",sup2:"²",sup3:"³",sup:"⊃",Sup:"⋑",supdot:"⪾",supdsub:"⫘",supE:"⫆",supe:"⊇",supedot:"⫄",Superset:"⊃",SupersetEqual:"⊇",suphsol:"⟉",suphsub:"⫗",suplarr:"⥻",supmult:"⫂",supnE:"⫌",supne:"⊋",supplus:"⫀",supset:"⊃",Supset:"⋑",supseteq:"⊇",supseteqq:"⫆",supsetneq:"⊋",supsetneqq:"⫌",supsim:"⫈",supsub:"⫔",supsup:"⫖",swarhk:"⤦",swarr:"↙",swArr:"⇙",swarrow:"↙",swnwar:"⤪",szlig:"ß",Tab:"\t",target:"⌖",Tau:"Τ",tau:"τ",tbrk:"⎴",Tcaron:"Ť",tcaron:"ť",Tcedil:"Ţ",tcedil:"ţ",Tcy:"Т",tcy:"т",tdot:"⃛",telrec:"⌕",Tfr:"𝔗",tfr:"𝔱",there4:"∴",therefore:"∴",Therefore:"∴",Theta:"Θ",theta:"θ",thetasym:"ϑ",thetav:"ϑ",thickapprox:"≈",thicksim:"∼",ThickSpace:"  ",ThinSpace:" ",thinsp:" ",thkap:"≈",thksim:"∼",THORN:"Þ",thorn:"þ",tilde:"˜",Tilde:"∼",TildeEqual:"≃",TildeFullEqual:"≅",TildeTilde:"≈",timesbar:"⨱",timesb:"⊠",times:"×",timesd:"⨰",tint:"∭",toea:"⤨",topbot:"⌶",topcir:"⫱",top:"⊤",Topf:"𝕋",topf:"𝕥",topfork:"⫚",tosa:"⤩",tprime:"‴",trade:"™",TRADE:"™",triangle:"▵",triangledown:"▿",triangleleft:"◃",trianglelefteq:"⊴",triangleq:"≜",triangleright:"▹",trianglerighteq:"⊵",tridot:"◬",trie:"≜",triminus:"⨺",TripleDot:"⃛",triplus:"⨹",trisb:"⧍",tritime:"⨻",trpezium:"⏢",Tscr:"𝒯",tscr:"𝓉",TScy:"Ц",tscy:"ц",TSHcy:"Ћ",tshcy:"ћ",Tstrok:"Ŧ",tstrok:"ŧ",twixt:"≬",twoheadleftarrow:"↞",twoheadrightarrow:"↠",Uacute:"Ú",uacute:"ú",uarr:"↑",Uarr:"↟",uArr:"⇑",Uarrocir:"⥉",Ubrcy:"Ў",ubrcy:"ў",Ubreve:"Ŭ",ubreve:"ŭ",Ucirc:"Û",ucirc:"û",Ucy:"У",ucy:"у",udarr:"⇅",Udblac:"Ű",udblac:"ű",udhar:"⥮",ufisht:"⥾",Ufr:"𝔘",ufr:"𝔲",Ugrave:"Ù",ugrave:"ù",uHar:"⥣",uharl:"↿",uharr:"↾",uhblk:"▀",ulcorn:"⌜",ulcorner:"⌜",ulcrop:"⌏",ultri:"◸",Umacr:"Ū",umacr:"ū",uml:"¨",UnderBar:"_",UnderBrace:"⏟",UnderBracket:"⎵",UnderParenthesis:"⏝",Union:"⋃",UnionPlus:"⊎",Uogon:"Ų",uogon:"ų",Uopf:"𝕌",uopf:"𝕦",UpArrowBar:"⤒",uparrow:"↑",UpArrow:"↑",Uparrow:"⇑",UpArrowDownArrow:"⇅",updownarrow:"↕",UpDownArrow:"↕",Updownarrow:"⇕",UpEquilibrium:"⥮",upharpoonleft:"↿",upharpoonright:"↾",uplus:"⊎",UpperLeftArrow:"↖",UpperRightArrow:"↗",upsi:"υ",Upsi:"ϒ",upsih:"ϒ",Upsilon:"Υ",upsilon:"υ",UpTeeArrow:"↥",UpTee:"⊥",upuparrows:"⇈",urcorn:"⌝",urcorner:"⌝",urcrop:"⌎",Uring:"Ů",uring:"ů",urtri:"◹",Uscr:"𝒰",uscr:"𝓊",utdot:"⋰",Utilde:"Ũ",utilde:"ũ",utri:"▵",utrif:"▴",uuarr:"⇈",Uuml:"Ü",uuml:"ü",uwangle:"⦧",vangrt:"⦜",varepsilon:"ϵ",varkappa:"ϰ",varnothing:"∅",varphi:"ϕ",varpi:"ϖ",varpropto:"∝",varr:"↕",vArr:"⇕",varrho:"ϱ",varsigma:"ς",varsubsetneq:"⊊︀",varsubsetneqq:"⫋︀",varsupsetneq:"⊋︀",varsupsetneqq:"⫌︀",vartheta:"ϑ",vartriangleleft:"⊲",vartriangleright:"⊳",vBar:"⫨",Vbar:"⫫",vBarv:"⫩",Vcy:"В",vcy:"в",vdash:"⊢",vDash:"⊨",Vdash:"⊩",VDash:"⊫",Vdashl:"⫦",veebar:"⊻",vee:"∨",Vee:"⋁",veeeq:"≚",vellip:"⋮",verbar:"|",Verbar:"‖",vert:"|",Vert:"‖",VerticalBar:"∣",VerticalLine:"|",VerticalSeparator:"❘",VerticalTilde:"≀",VeryThinSpace:" ",Vfr:"𝔙",vfr:"𝔳",vltri:"⊲",vnsub:"⊂⃒",vnsup:"⊃⃒",Vopf:"𝕍",vopf:"𝕧",vprop:"∝",vrtri:"⊳",Vscr:"𝒱",vscr:"𝓋",vsubnE:"⫋︀",vsubne:"⊊︀",vsupnE:"⫌︀",vsupne:"⊋︀",Vvdash:"⊪",vzigzag:"⦚",Wcirc:"Ŵ",wcirc:"ŵ",wedbar:"⩟",wedge:"∧",Wedge:"⋀",wedgeq:"≙",weierp:"℘",Wfr:"𝔚",wfr:"𝔴",Wopf:"𝕎",wopf:"𝕨",wp:"℘",wr:"≀",wreath:"≀",Wscr:"𝒲",wscr:"𝓌",xcap:"⋂",xcirc:"◯",xcup:"⋃",xdtri:"▽",Xfr:"𝔛",xfr:"𝔵",xharr:"⟷",xhArr:"⟺",Xi:"Ξ",xi:"ξ",xlarr:"⟵",xlArr:"⟸",xmap:"⟼",xnis:"⋻",xodot:"⨀",Xopf:"𝕏",xopf:"𝕩",xoplus:"⨁",xotime:"⨂",xrarr:"⟶",xrArr:"⟹",Xscr:"𝒳",xscr:"𝓍",xsqcup:"⨆",xuplus:"⨄",xutri:"△",xvee:"⋁",xwedge:"⋀",Yacute:"Ý",yacute:"ý",YAcy:"Я",yacy:"я",Ycirc:"Ŷ",ycirc:"ŷ",Ycy:"Ы",ycy:"ы",yen:"¥",Yfr:"𝔜",yfr:"𝔶",YIcy:"Ї",yicy:"ї",Yopf:"𝕐",yopf:"𝕪",Yscr:"𝒴",yscr:"𝓎",YUcy:"Ю",yucy:"ю",yuml:"ÿ",Yuml:"Ÿ",Zacute:"Ź",zacute:"ź",Zcaron:"Ž",zcaron:"ž",Zcy:"З",zcy:"з",Zdot:"Ż",zdot:"ż",zeetrf:"ℨ",ZeroWidthSpace:"​",Zeta:"Ζ",zeta:"ζ",zfr:"𝔷",Zfr:"ℨ",ZHcy:"Ж",zhcy:"ж",zigrarr:"⇝",zopf:"𝕫",Zopf:"ℤ",Zscr:"𝒵",zscr:"𝓏",zwj:"‍",zwnj:"‌"}},{}],17:[function(e,t,r){t.exports={Aacute:"Á",aacute:"á",Acirc:"Â",acirc:"â",acute:"´",AElig:"Æ",aelig:"æ",Agrave:"À",agrave:"à",amp:"&",AMP:"&",Aring:"Å",aring:"å",Atilde:"Ã",atilde:"ã",Auml:"Ä",auml:"ä",brvbar:"¦",Ccedil:"Ç",ccedil:"ç",cedil:"¸",cent:"¢",copy:"©",COPY:"©",curren:"¤",deg:"°",divide:"÷",Eacute:"É",eacute:"é",Ecirc:"Ê",ecirc:"ê",Egrave:"È",egrave:"è",ETH:"Ð",eth:"ð",Euml:"Ë",euml:"ë",frac12:"½",frac14:"¼",frac34:"¾",gt:">",GT:">",Iacute:"Í",iacute:"í",Icirc:"Î",icirc:"î",iexcl:"¡",Igrave:"Ì",igrave:"ì",iquest:"¿",Iuml:"Ï",iuml:"ï",laquo:"«",lt:"<",LT:"<",macr:"¯",micro:"µ",middot:"·",nbsp:" ",not:"¬",Ntilde:"Ñ",ntilde:"ñ",Oacute:"Ó",oacute:"ó",Ocirc:"Ô",ocirc:"ô",Ograve:"Ò",ograve:"ò",ordf:"ª",ordm:"º",Oslash:"Ø",oslash:"ø",Otilde:"Õ",otilde:"õ",Ouml:"Ö",ouml:"ö",para:"¶",plusmn:"±",pound:"£",quot:'"',QUOT:'"',raquo:"»",reg:"®",REG:"®",sect:"§",shy:"­",sup1:"¹",sup2:"²",sup3:"³",szlig:"ß",THORN:"Þ",thorn:"þ",times:"×",Uacute:"Ú",uacute:"ú",Ucirc:"Û",ucirc:"û",Ugrave:"Ù",ugrave:"ù",uml:"¨",Uuml:"Ü",uuml:"ü",Yacute:"Ý",yacute:"ý",yen:"¥",yuml:"ÿ"}},{}],18:[function(e,t,r){t.exports={amp:"&",apos:"'",gt:">",lt:"<",quot:'"'}},{}],19:[function(e,t,r){function n(e,t){var r;return"string"!=typeof t&&(t=n.defaultChars),r=function(e){var t,r,n=i[e];if(n)return n;for(n=i[e]=[],t=0;t<128;t++)r=String.fromCharCode(t),n.push(r);for(t=0;t=55296&&l<=57343?"���":String.fromCharCode(l),t+=6):240==(248&i)&&t+91114111?u+="����":(l-=65536,u+=String.fromCharCode(55296+(l>>10),56320+(1023&l))),t+=9):u+="�";return u})}var i={};n.defaultChars=";/?:@&=+$,#",n.componentChars="",t.exports=n},{}],20:[function(e,t,r){function n(e,t,r){var s,o,a,l,u,c="";for("string"!=typeof t&&(r=t,t=n.defaultChars),void 0===r&&(r=!0),u=function(e){var t,r,n=i[e];if(n)return n;for(n=i[e]=[],t=0;t<128;t++)r=String.fromCharCode(t),n.push(/^[0-9a-z]$/i.test(r)?r:"%"+("0"+t.toString(16).toUpperCase()).slice(-2));for(t=0;t=55296&&a<=57343){if(a>=55296&&a<=56319&&s+1=56320&&l<=57343){c+=encodeURIComponent(e[s]+e[s+1]),s++;continue}c+="%EF%BF%BD"}else c+=encodeURIComponent(e[s]);return c}var i={};n.defaultChars=";/?:@&=+$,-_.!~*'()#",n.componentChars="-_.!~*'()",t.exports=n},{}],21:[function(e,t,r){var n,i;String.prototype.repeat||(n=function(){try{var e={},t=Object.defineProperty,r=t(e,e,e)&&t}catch(e){}return r}(),i=function(e){if(null==this)throw TypeError();var t=String(this),r=e?Number(e):0;if(r!=r&&(r=0),r<0||r==1/0)throw RangeError();for(var n="";r;)r%2==1&&(n+=t),r>1&&(t+=t),r>>=1;return n},n?n(String.prototype,"repeat",{value:i,configurable:!0,writable:!0}):String.prototype.repeat=i)},{}]},{},[4])(4)}),window.CodeMirror={},function(){function e(e){this.pos=this.start=0,this.string=e,this.lineStart=0}e.prototype={eol:function(){return this.pos>=this.string.length},sol:function(){return 0==this.pos},peek:function(){return this.string.charAt(this.pos)||null},next:function(){if(this.post},eatSpace:function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},skipToEnd:function(){this.pos=this.string.length},skipTo:function(e){var t=this.string.indexOf(e,this.pos);if(t>-1)return this.pos=t,!0},backUp:function(e){this.pos-=e},column:function(){return this.start-this.lineStart},indentation:function(){return 0},match:function(e,t,r){if("string"!=typeof e){var n=this.string.slice(this.pos).match(e);return n&&n.index>0?null:(n&&!1!==t&&(this.pos+=n[0].length),n)}var i=function(e){return r?e.toLowerCase():e};if(i(this.string.substr(this.pos,e.length))==i(e))return!1!==t&&(this.pos+=e.length),!0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}},lookAhead:function(){return null}},CodeMirror.StringStream=e,CodeMirror.startState=function(e,t,r){return!e.startState||e.startState(t,r)};var t=CodeMirror.modes={},r=CodeMirror.mimeModes={};CodeMirror.defineMode=function(e,r){arguments.length>2&&(r.dependencies=Array.prototype.slice.call(arguments,2)),t[e]=r},CodeMirror.defineMIME=function(e,t){r[e]=t},CodeMirror.resolveMode=function(e){return"string"==typeof e&&r.hasOwnProperty(e)?e=r[e]:e&&"string"==typeof e.name&&r.hasOwnProperty(e.name)&&(e=r[e.name]),"string"==typeof e?{name:e}:e||{name:"null"}},CodeMirror.getMode=function(e,r){r=CodeMirror.resolveMode(r);var n=t[r.name];if(!n)throw new Error("Unknown mode: "+r);return n(e,r)},CodeMirror.registerHelper=CodeMirror.registerGlobalHelper=Math.min,CodeMirror.defineMode("null",function(){return{token:function(e){e.skipToEnd()}}}),CodeMirror.defineMIME("text/plain","null"),CodeMirror.runMode=function(e,t,r,n){var i=CodeMirror.getMode({indentUnit:2},t);if(1==r.nodeType){var s=n&&n.tabSize||4,o=r,a=0;o.innerHTML="",r=function(e,t){if("\n"==e)return o.appendChild(document.createElement("br")),void(a=0);for(var r="",n=0;;){var i=e.indexOf("\t",n);if(-1==i){r+=e.slice(n),a+=e.length-n;break}a+=i-n,r+=e.slice(n,i);var l=s-a%s;a+=l;for(var u=0;u!?|~^@]/,d=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;function m(e,t,r){return n=e,i=r,t}function f(e,t){var r,n=e.next();if('"'==n||"'"==n)return t.tokenize=(r=n,function(e,t){var n,i=!1;if(a&&"@"==e.peek()&&e.match(d))return t.tokenize=f,m("jsonld-keyword","meta");for(;null!=(n=e.next())&&(n!=r||i);)i=!i&&"\\"==n;return i||(t.tokenize=f),m("string","string")}),t.tokenize(e,t);if("."==n&&e.match(/^\d+(?:[eE][+\-]?\d+)?/))return m("number","number");if("."==n&&e.match(".."))return m("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(n))return m(n);if("="==n&&e.eat(">"))return m("=>","operator");if("0"==n&&e.eat(/x/i))return e.eatWhile(/[\da-f]/i),m("number","number");if("0"==n&&e.eat(/o/i))return e.eatWhile(/[0-7]/i),m("number","number");if("0"==n&&e.eat(/b/i))return e.eatWhile(/[01]/i),m("number","number");if(/\d/.test(n))return e.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),m("number","number");if("/"==n)return e.eat("*")?(t.tokenize=g,g(e,t)):e.eat("/")?(e.skipToEnd(),m("comment","comment")):je(e,t,1)?(function(e){for(var t,r=!1,n=!1;null!=(t=e.next());){if(!r){if("/"==t&&!n)return;"["==t?n=!0:n&&"]"==t&&(n=!1)}r=!r&&"\\"==t}}(e),e.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/),m("regexp","string-2")):(e.eat("="),m("operator","operator",e.current()));if("`"==n)return t.tokenize=b,b(e,t);if("#"==n)return e.skipToEnd(),m("error","error");if(h.test(n))return">"==n&&t.lexical&&">"==t.lexical.type||(e.eat("=")?"!"!=n&&"="!=n||e.eat("="):/[<>*+\-]/.test(n)&&(e.eat(n),">"==n&&e.eat(n))),m("operator","operator",e.current());if(c.test(n)){e.eatWhile(c);var i=e.current();if("."!=t.lastType){if(p.propertyIsEnumerable(i)){var s=p[i];return m(s.type,s.style,i)}if("async"==i&&e.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/,!1))return m("async","keyword",i)}return m("variable","variable",i)}}function g(e,t){for(var r,n=!1;r=e.next();){if("/"==r&&n){t.tokenize=f;break}n="*"==r}return m("comment","comment")}function b(e,t){for(var r,n=!1;null!=(r=e.next());){if(!n&&("`"==r||"$"==r&&e.eat("{"))){t.tokenize=f;break}n=!n&&"\\"==r}return m("quasi","string-2",e.current())}var v="([{}])";function E(e,t){t.fatArrowAt&&(t.fatArrowAt=null);var r=e.string.indexOf("=>",e.start);if(!(r<0)){if(u){var n=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(e.string.slice(e.start,r));n&&(r=n.index)}for(var i=0,s=!1,o=r-1;o>=0;--o){var a=e.string.charAt(o),l=v.indexOf(a);if(l>=0&&l<3){if(!i){++o;break}if(0==--i){"("==a&&(s=!0);break}}else if(l>=3&&l<6)++i;else if(c.test(a))s=!0;else{if(/["'\/]/.test(a))return;if(s&&!i){++o;break}}}s&&!i&&(t.fatArrowAt=o)}}var C={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,this:!0,"jsonld-keyword":!0};function _(e,t,r,n,i,s){this.indented=e,this.column=t,this.type=r,this.prev=i,this.info=s,null!=n&&(this.align=n)}function y(e,t){for(var r=e.localVars;r;r=r.next)if(r.name==t)return!0;for(var n=e.context;n;n=n.prev)for(r=n.vars;r;r=r.next)if(r.name==t)return!0}var w={state:null,column:null,marked:null,cc:null};function A(){for(var e=arguments.length-1;e>=0;e--)w.cc.push(arguments[e])}function x(){return A.apply(null,arguments),!0}function k(e){function t(t){for(var r=t;r;r=r.next)if(r.name==e)return!0;return!1}var n=w.state;if(w.marked="def",n.context){if(t(n.localVars))return;n.localVars={name:e,next:n.localVars}}else{if(t(n.globalVars))return;r.globalVars&&(n.globalVars={name:e,next:n.globalVars})}}function D(e){return"public"==e||"private"==e||"protected"==e||"abstract"==e||"readonly"==e}var L={name:"this",next:{name:"arguments"}};function S(){w.state.context={prev:w.state.context,vars:w.state.localVars},w.state.localVars=L}function F(){w.state.localVars=w.state.context.vars,w.state.context=w.state.context.prev}function N(e,t){var r=function(){var r=w.state,n=r.indented;if("stat"==r.lexical.type)n=r.lexical.indented;else for(var i=r.lexical;i&&")"==i.type&&i.align;i=i.prev)n=i.indented;r.lexical=new _(n,w.stream.column(),e,null,r.lexical,t)};return r.lex=!0,r}function T(){var e=w.state;e.lexical.prev&&(")"==e.lexical.type&&(e.indented=e.lexical.indented),e.lexical=e.lexical.prev)}function q(e){return function t(r){return r==e?x():";"==e?A():x(t)}}function B(e,t){return"var"==e?x(N("vardef",t.length),me,q(";"),T):"keyword a"==e?x(N("form"),V,B,T):"keyword b"==e?x(N("form"),B,T):"keyword d"==e?w.stream.match(/^\s*$/,!1)?x():x(N("stat"),R,q(";"),T):"debugger"==e?x(q(";")):"{"==e?x(N("}"),re,T):";"==e?x():"if"==e?("else"==w.state.lexical.info&&w.state.cc[w.state.cc.length-1]==T&&w.state.cc.pop()(),x(N("form"),V,B,T,Ee)):"function"==e?x(xe):"for"==e?x(N("form"),Ce,B,T):"class"==e||u&&"interface"==t?(w.marked="keyword",x(N("form"),Le,T)):"variable"==e?u&&"declare"==t?(w.marked="keyword",x(B)):u&&("module"==t||"enum"==t||"type"==t)&&w.stream.match(/^\s*\w/,!1)?(w.marked="keyword","enum"==t?x(Re):"type"==t?x(oe,q("operator"),oe,q(";")):x(N("form"),fe,q("{"),N("}"),re,T,T)):u&&"namespace"==t?(w.marked="keyword",x(N("form"),O,re,T)):u&&"abstract"==t?(w.marked="keyword",x(B)):x(N("stat"),X):"switch"==e?x(N("form"),V,q("{"),N("}","switch"),re,T,T):"case"==e?x(O,q(":")):"default"==e?x(q(":")):"catch"==e?x(N("form"),S,I,B,T,F):"export"==e?x(N("stat"),Te,T):"import"==e?x(N("stat"),Be,T):"async"==e?x(B):"@"==t?x(O,B):A(N("stat"),O,q(";"),T)}function I(e){if("("==e)return x(ke,q(")"))}function O(e,t){return $(e,t,!1)}function M(e,t){return $(e,t,!0)}function V(e){return"("!=e?A():x(N(")"),O,q(")"),T)}function $(e,t,r){if(w.state.fatArrowAt==w.stream.start){var n=r?G:H;if("("==e)return x(S,N(")"),ee(ke,")"),T,q("=>"),n,F);if("variable"==e)return A(S,fe,q("=>"),n,F)}var i=r?j:U;return C.hasOwnProperty(e)?x(i):"function"==e?x(xe,i):"class"==e||u&&"interface"==t?(w.marked="keyword",x(N("form"),De,T)):"keyword c"==e||"async"==e?x(r?M:O):"("==e?x(N(")"),R,q(")"),T,i):"operator"==e||"spread"==e?x(r?M:O):"["==e?x(N("]"),$e,T,i):"{"==e?te(Y,"}",null,i):"quasi"==e?A(P,i):"new"==e?x(function(e){return function(t){return"."==t?x(e?Z:W):"variable"==t&&u?x(pe,e?j:U):A(e?M:O)}}(r)):"import"==e?x(O):x()}function R(e){return e.match(/[;\}\)\],]/)?A():A(O)}function U(e,t){return","==e?x(O):j(e,t,!1)}function j(e,t,r){var n=0==r?U:j,i=0==r?O:M;return"=>"==e?x(S,r?G:H,F):"operator"==e?/\+\+|--/.test(t)||u&&"!"==t?x(n):u&&"<"==t&&w.stream.match(/^([^>]|<.*?>)*>\s*\(/,!1)?x(N(">"),ee(oe,">"),T,n):"?"==t?x(O,q(":"),i):x(i):"quasi"==e?A(P,n):";"!=e?"("==e?te(M,")","call",n):"."==e?x(J,n):"["==e?x(N("]"),R,q("]"),T,n):u&&"as"==t?(w.marked="keyword",x(oe,n)):"regexp"==e?(w.state.lastType=w.marked="operator",w.stream.backUp(w.stream.pos-w.stream.start-1),x(i)):void 0:void 0}function P(e,t){return"quasi"!=e?A():"${"!=t.slice(t.length-2)?x(P):x(O,z)}function z(e){if("}"==e)return w.marked="string-2",w.state.tokenize=b,x(P)}function H(e){return E(w.stream,w.state),A("{"==e?B:O)}function G(e){return E(w.stream,w.state),A("{"==e?B:M)}function W(e,t){if("target"==t)return w.marked="keyword",x(U)}function Z(e,t){if("target"==t)return w.marked="keyword",x(j)}function X(e){return":"==e?x(T,B):A(U,q(";"),T)}function J(e){if("variable"==e)return w.marked="property",x()}function Y(e,t){if("async"==e)return w.marked="property",x(Y);if("variable"==e||"keyword"==w.style){return w.marked="property","get"==t||"set"==t?x(K):(u&&w.state.fatArrowAt==w.stream.start&&(r=w.stream.match(/^\s*:\s*/,!1))&&(w.state.fatArrowAt=w.stream.pos+r[0].length),x(Q));var r}else{if("number"==e||"string"==e)return w.marked=a?"property":w.style+" property",x(Q);if("jsonld-keyword"==e)return x(Q);if(u&&D(t))return w.marked="keyword",x(Y);if("["==e)return x(O,ne,q("]"),Q);if("spread"==e)return x(M,Q);if("*"==t)return w.marked="keyword",x(Y);if(":"==e)return A(Q)}}function K(e){return"variable"!=e?A(Q):(w.marked="property",x(xe))}function Q(e){return":"==e?x(M):"("==e?A(xe):void 0}function ee(e,t,r){function n(i,s){if(r?r.indexOf(i)>-1:","==i){var o=w.state.lexical;return"call"==o.info&&(o.pos=(o.pos||0)+1),x(function(r,n){return r==t||n==t?A():A(e)},n)}return i==t||s==t?x():x(q(t))}return function(r,i){return r==t||i==t?x():A(e,n)}}function te(e,t,r){for(var n=3;n"),oe):void 0}function ae(e){if("=>"==e)return x(oe)}function le(e,t){return"variable"==e||"keyword"==w.style?(w.marked="property",x(le)):"?"==t?x(le):":"==e?x(oe):"["==e?x(O,ne,q("]"),le):void 0}function ue(e,t){return"variable"==e&&w.stream.match(/^\s*[?:]/,!1)||"?"==t?x(ue):":"==e?x(oe):A(oe)}function ce(e,t){return"<"==t?x(N(">"),ee(oe,">"),T,ce):"|"==t||"."==e||"&"==t?x(oe):"["==e?x(q("]"),ce):"extends"==t||"implements"==t?(w.marked="keyword",x(oe)):void 0}function pe(e,t){if("<"==t)return x(N(">"),ee(oe,">"),T,ce)}function he(){return A(oe,de)}function de(e,t){if("="==t)return x(oe)}function me(e,t){return"enum"==t?(w.marked="keyword",x(Re)):A(fe,ne,be,ve)}function fe(e,t){return u&&D(t)?(w.marked="keyword",x(fe)):"variable"==e?(k(t),x()):"spread"==e?x(fe):"["==e?te(fe,"]"):"{"==e?te(ge,"}"):void 0}function ge(e,t){return"variable"!=e||w.stream.match(/^\s*:/,!1)?("variable"==e&&(w.marked="property"),"spread"==e?x(fe):"}"==e?A():x(q(":"),fe,be)):(k(t),x(be))}function be(e,t){if("="==t)return x(M)}function ve(e){if(","==e)return x(me)}function Ee(e,t){if("keyword b"==e&&"else"==t)return x(N("form","else"),B,T)}function Ce(e,t){return"await"==t?x(Ce):"("==e?x(N(")"),_e,q(")"),T):void 0}function _e(e){return"var"==e?x(me,q(";"),we):";"==e?x(we):"variable"==e?x(ye):A(O,q(";"),we)}function ye(e,t){return"in"==t||"of"==t?(w.marked="keyword",x(O)):x(U,we)}function we(e,t){return";"==e?x(Ae):"in"==t||"of"==t?(w.marked="keyword",x(O)):A(O,q(";"),Ae)}function Ae(e){")"!=e&&x(O)}function xe(e,t){return"*"==t?(w.marked="keyword",x(xe)):"variable"==e?(k(t),x(xe)):"("==e?x(S,N(")"),ee(ke,")"),T,ie,B,F):u&&"<"==t?x(N(">"),ee(he,">"),T,xe):void 0}function ke(e,t){return"@"==t&&x(O,ke),"spread"==e?x(ke):u&&D(t)?(w.marked="keyword",x(ke)):A(fe,ne,be)}function De(e,t){return"variable"==e?Le(e,t):Se(e,t)}function Le(e,t){if("variable"==e)return k(t),x(Se)}function Se(e,t){return"<"==t?x(N(">"),ee(he,">"),T,Se):"extends"==t||"implements"==t||u&&","==e?("implements"==t&&(w.marked="keyword"),x(u?oe:O,Se)):"{"==e?x(N("}"),Fe,T):void 0}function Fe(e,t){return"async"==e||"variable"==e&&("static"==t||"get"==t||"set"==t||u&&D(t))&&w.stream.match(/^\s+[\w$\xa1-\uffff]/,!1)?(w.marked="keyword",x(Fe)):"variable"==e||"keyword"==w.style?(w.marked="property",x(u?Ne:xe,Fe)):"["==e?x(O,ne,q("]"),u?Ne:xe,Fe):"*"==t?(w.marked="keyword",x(Fe)):";"==e?x(Fe):"}"==e?x():"@"==t?x(O,Fe):void 0}function Ne(e,t){return"?"==t?x(Ne):":"==e?x(oe,be):"="==t?x(M):A(xe)}function Te(e,t){return"*"==t?(w.marked="keyword",x(Ve,q(";"))):"default"==t?(w.marked="keyword",x(O,q(";"))):"{"==e?x(ee(qe,"}"),Ve,q(";")):A(B)}function qe(e,t){return"as"==t?(w.marked="keyword",x(q("variable"))):"variable"==e?A(M,qe):void 0}function Be(e){return"string"==e?x():"("==e?A(O):A(Ie,Oe,Ve)}function Ie(e,t){return"{"==e?te(Ie,"}"):("variable"==e&&k(t),"*"==t&&(w.marked="keyword"),x(Me))}function Oe(e){if(","==e)return x(Ie,Oe)}function Me(e,t){if("as"==t)return w.marked="keyword",x(Ie)}function Ve(e,t){if("from"==t)return w.marked="keyword",x(O)}function $e(e){return"]"==e?x():A(ee(M,"]"))}function Re(){return A(N("form"),fe,q("{"),N("}"),ee(Ue,"}"),T,T)}function Ue(){return A(fe,be)}function je(e,t,r){return t.tokenize==f&&/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(t.lastType)||"quasi"==t.lastType&&/\{\s*$/.test(e.string.slice(0,e.pos-(r||0)))}return T.lex=!0,{startState:function(e){var t={tokenize:f,lastType:"sof",cc:[],lexical:new _((e||0)-s,0,"block",!1),localVars:r.localVars,context:r.localVars&&{vars:r.localVars},indented:e||0};return r.globalVars&&"object"==typeof r.globalVars&&(t.globalVars=r.globalVars),t},token:function(e,t){if(e.sol()&&(t.lexical.hasOwnProperty("align")||(t.lexical.align=!1),t.indented=e.indentation(),E(e,t)),t.tokenize!=g&&e.eatSpace())return null;var r=t.tokenize(e,t);return"comment"==n?r:(t.lastType="operator"!=n||"++"!=i&&"--"!=i?n:"incdec",function(e,t,r,n,i){var s=e.cc;for(w.state=e,w.stream=i,w.marked=null,w.cc=s,w.style=t,e.lexical.hasOwnProperty("align")||(e.lexical.align=!0);;)if((s.length?s.pop():l?O:B)(r,n)){for(;s.length&&s[s.length-1].lex;)s.pop()();return w.marked?w.marked:"variable"==r&&y(e,n)?"variable-2":t}}(t,r,n,i,e))},indent:function(t,n){if(t.tokenize==g)return e.Pass;if(t.tokenize!=f)return 0;var i,a=n&&n.charAt(0),l=t.lexical;if(!/^\s*else\b/.test(n))for(var u=t.cc.length-1;u>=0;--u){var c=t.cc[u];if(c==T)l=l.prev;else if(c!=Ee)break}for(;("stat"==l.type||"form"==l.type)&&("}"==a||(i=t.cc[t.cc.length-1])&&(i==U||i==j)&&!/^[,\.=+\-*:?[\(]/.test(n));)l=l.prev;o&&")"==l.type&&"stat"==l.prev.type&&(l=l.prev);var p=l.type,d=a==p;return"vardef"==p?l.indented+("operator"==t.lastType||","==t.lastType?l.info+1:0):"form"==p&&"{"==a?l.indented:"form"==p?l.indented+s:"stat"==p?l.indented+(function(e,t){return"operator"==e.lastType||","==e.lastType||h.test(t.charAt(0))||/[,.]/.test(t.charAt(0))}(t,n)?o||s:0):"switch"!=l.info||d||0==r.doubleIndentSwitch?l.align?l.column+(d?0:1):l.indented+(d?0:s):l.indented+(/^(?:case|default)\b/.test(n)?s:2*s)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:l?null:"/*",blockCommentEnd:l?null:"*/",blockCommentContinue:l?null:" * ",lineComment:l?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:l?"json":"javascript",jsonldMode:a,jsonMode:l,expressionAllowed:je,skipExpression:function(e){var t=e.cc[e.cc.length-1];t!=O&&t!=M||e.cc.pop()}}}),e.registerHelper("wordChars","javascript",/[\w$]/),e.defineMIME("text/javascript","javascript"),e.defineMIME("text/ecmascript","javascript"),e.defineMIME("application/javascript","javascript"),e.defineMIME("application/x-javascript","javascript"),e.defineMIME("application/ecmascript","javascript"),e.defineMIME("application/json",{name:"javascript",json:!0}),e.defineMIME("application/x-json",{name:"javascript",json:!0}),e.defineMIME("application/ld+json",{name:"javascript",jsonld:!0}),e.defineMIME("text/typescript",{name:"javascript",typescript:!0}),e.defineMIME("application/typescript",{name:"javascript",typescript:!0})},"object"==typeof exports&&"object"==typeof module?E(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],E):E(CodeMirror);class C{static markdownToDOM(e,t=!1,n){const i=(new commonmark.Parser).parse(e),s=new commonmark.HtmlRenderer({safe:t,softbreak:n}).render(i),o=new DOMParser,a=document.importNode(o.parseFromString(s,"text/html").body,!0),l=/#(\d+)\b/gm;let u=document.createTreeWalker(a,NodeFilter.SHOW_TEXT,{acceptNode:e=>"A"!==e.parentElement.tagName&&(l.lastIndex=0,l.test(e.textContent))});const c=[];for(;u.nextNode();)c.push(u.currentNode);for(const e of c){const t=document.createDocumentFragment(),n=e.textContent;l.lastIndex=0;let i=0,s=null;for(;s=l.exec(n);)t.appendChild(document.createTextNode(n.substring(i,s.index))),t.appendChild(r` - #${s[1]} - `),i=l.lastIndex;i"A"!==e.parentElement.tagName&&(p.lastIndex=0,p.test(e.textContent))});const h=[];for(;u.nextNode();)h.push(u.currentNode);for(const e of h){const t=document.createDocumentFragment(),n=e.textContent;p.lastIndex=0;let i=0,s=null;for(;s=p.exec(n);)t.appendChild(document.createTextNode(n.substring(i,s.index))),t.appendChild(r` - ${s[1]} - `),i=p.lastIndex;i`;1===e.children.length&&"CODE"===e.children[0].tagName?e.children[0].appendChild(t):e.appendChild(t)}}for(const e of a.querySelectorAll("code.language-javascript"))e.classList.remove("language-javascript"),e.classList.add("language-js");for(const e of a.querySelectorAll("code.language-js"))CodeMirror.runMode(e.textContent,"text/javascript",e);return a}static create(e,t,r,n){console.time("Generate API HTML");const i=C.markdownToDOM(r);for(const t of i.querySelectorAll("a")){const r=t.getAttribute("href")||"";if(r.startsWith("#")){const n=r.substring(1),i=C._idFromGHAnchor(n);t.setAttribute("href",app.linkURL("Puppeteer",e,i))}else if(r.startsWith("/")||r.startsWith("../")||r.startsWith("./")){const n=/^\d+\.\d+\.\d+$/.test(e)?"v"+e:"main";t.setAttribute("href",`https://github.com/GoogleChrome/puppeteer/blob/${n}/docs/${r}`)}}const s=new C(e);t&&s.sections.push(y.createReleaseNotes(s,C.markdownToDOM(t,!0,"
")));const o=i.querySelectorAll("h3");for(let e=0;e -

Puppeteer ${this.version}

-
    ${this.sections.map(e=>r` -
  • ${e.createTableOfContentsElement()}
  • - `)}${this.classes.map(e=>r` -
  • - ${e.createTableOfContentsElement()} -
      ${[...e.events,...e.namespaces,...e.methods].map(e=>r` -
    • ${e.createTableOfContentsElement()}
    • `)} -
    -
  • - `)} -
- - `}_initializeContentIds(){const e=new Set,t=(t,r)=>{const n=C._idFromGHAnchor((t=>{const r=t.trim().toLowerCase().replace(/\s/g,"-").replace(/[^-0-9a-zа-яё]/gi,"");let n=r,i=0;for(;e.has(n);)n=r+"-"+ ++i;return e.add(n),n})(r));t.contentId=n,this._idToEntry.set(n,t)};for(const e of this.sections)t(e,e.name);for(const e of this.classes){t(e,`class: '${e.name}'`);for(const r of e.events)t(r,`event: '${r.name}'`);for(const r of e.methods)t(r,`${e.loweredName}.${r.name}(${r.args})`);for(const r of e.namespaces)t(r,`${e.loweredName}.${r.name}`)}}idToEntry(e){return this._idToEntry.get(e)||null}}class _{constructor(e,t,r,n){this.api=e,this.name=t,this.tableOfContentsText=t,this.element=r,this.contentId=null,this.sinceVersion="",this.untilVersion=""}linkURL(){return app.linkURL("Puppeteer",this.api.version,this.contentId)}createTableOfContentsElement(){const e=document.createDocumentFragment();return e.appendChild(r` - ${this.tableOfContentsText} - `),this.sinceVersion&&e.appendChild(r` - - ${this.sinceVersion} - - `),this.untilVersion&&e.appendChild(r`${this.untilVersion}`),e}}class y extends _{static create(e,t,n){return new y(e,t,r` - -

${t}

- ${n} -
- `)}static createReleaseNotes(e,t){return new y(e,"Release Notes",r` - -

Puppeteer ${e.version} Release Notes

- ${Array.from(t.childNodes)} -
- `)}constructor(e,t,r){super(e,t,r)}}class w extends _{static create(e,t,n){const i=t.replace(/^class:/i,"").trim(),s=n.querySelectorAll("h4"),o=r` - -

- - class: ${i} - - -

- ${D(n.firstChild,s[0])} -
- `,a=new w(e,i,o);for(let e=0;e{this.element.appendChild(r` -

${e}

-
    ${t.map(e=>r` -
  • ${e.createTableOfContentsElement()}
  • - `)} -
- `)};this.events.length&&e("Events",this.events),this.namespaces.length&&e("Namespaces",this.namespaces),this.methods.length&&e("Methods",this.methods)}}class A extends _{static create(e,t,n){const i=t.split(".").pop();return new A(e,i,r` - -

- - ${e.loweredName} - .${i} - - -

- ${n} -
- `)}constructor(e,t,r){super(e.api,t,r),this.tableOfContentsText=`${e.loweredName}.${t}`,this.apiClass=e}}class x extends _{static create(e,t,n){const i=t.match(/\.([^(]*)/)[1],s=t.match(/\((.*)\)/)[1];return new x(e,i,s,r` - -

- - ${e.loweredName} - .${i} - (${s}) - - -

- ${n} -
- `)}constructor(e,t,r,n){super(e.api,t,n),this.tableOfContentsText=`${e.loweredName}.${t}(${r})`,this.apiClass=e,this.args=r}}class k extends _{static create(e,t,n){const i=t.match(/'(.*)'/)[1];return new k(e,i,r` - -

- ${e.loweredName}.on('${i}') - - -

- ${n} -
- `)}constructor(e,t,r){super(e.api,t,r),this.tableOfContentsText=`${e.loweredName}.on('${t}')`,this.apiClass=e}}function D(e,t){const r=document.createDocumentFragment();let n=e;for(;n&&n!==t;){const e=n.nextSibling;r.appendChild(n),n=e}return r}const L="pptr-api-data",S="Puppeteer";class F extends f.Product{static async fetchReleaseAndReadme(e){const t=Date.now(),[r,n]=await Promise.all([fetch("https://api.github.com/repos/GoogleChrome/puppeteer/releases").then(e=>e.text()),fetch("https://raw.githubusercontent.com/GoogleChrome/puppeteer/main/README.md").then(e=>e.text())]),i=JSON.parse(r).map(e=>({name:e.tag_name,releaseNotes:e.body,timestamp:new Date(e.published_at).getTime(),apiText:""}));i.push({name:"v0.9.0",timestamp:new Date("August 16, 2017").getTime(),releaseNotes:"",apiText:""});for(const e of i){const[t,r,n]=e.name.substring(1).split(".").map(e=>parseInt(e,10));e.priority=100*t*100+100*r+n}if(i.sort((e,t)=>t.priority-e.priority),e){const t=new Map(e.releases.map(e=>[e.name,e.apiText]));for(const e of i)e.apiText=t.get(e.name)}for(const e of i)"v0.9.0"===e.name?e.chromiumVersion="Chromium 62.0.3188.0 (r494755)":"v0.10.0"===e.name||"v0.10.1"===e.name?e.chromiumVersion="Chromium 62.0.3193.0 (r496140)":"v0.13.0"===e.name?e.chromiumVersion="Chromium 64.0.3265.0 (r515411)":"v1.1.0"===e.name||"v1.1.1"===e.name?e.chromiumVersion="Chromium 66.0.3347.0 (r536395)":"v1.3.0"===e.name?e.chromiumVersion="Chromium 67.0.3392.0 (r536395)":"v5.1.0"===e.name?e.chromiumVersion="Chromium 84.0.4147.0 (r768783)":"v5.5.0"===e.name?e.chromiumVersion="Chromium 88.0.4298.0 (r818858)":"v6.0.0"===e.name?e.chromiumVersion="Chromium 89.0.4389.0 (r843427)":"v7.0.0"===e.name?e.chromiumVersion="Chromium 90.0.4403.0 (r848005)":"v8.0.0"===e.name&&(e.chromiumVersion="Chromium 90.0.4427.0 (r856583)");i.unshift({name:"main",chromiumVersion:"N/A",releaseNotes:"",apiText:""});for(const e of i){if(!e.releaseNotes||e.chromiumVersion)continue;const t=e.releaseNotes.match(/Chromium\s+(\d+\.\d+.\d+.\d+)\s*\((r\d{6})\)/i);e.chromiumVersion=t?`Chromium ${t[1]} (${t[2]})`:"N/A"}return await Promise.all(i.map(async e=>{"main"!==e.name&&e.apiText||(e.apiText=await fetch(`https://raw.githubusercontent.com/GoogleChrome/puppeteer/${e.name}/docs/api.md`).then(e=>e.text()))})),{fetchTimestamp:t,readmeText:n,releases:i}}static async create(e){const t=new g("pptr-db","pptr-store"),r=await function(){if(!("MozAppearance"in document.documentElement.style))return Promise.resolve(!1);const e=indexedDB.open("test");return new Promise(t=>{e.onerror=t.bind(null,!0),e.onsuccess=t.bind(null,!1)})}();let n=await async function(e,n){return r?JSON.parse(localStorage.getItem(e)):function(e,t=v()){let r;return t._withIDBStore("readonly",t=>{r=t.get(e)}).then(()=>r.result)}(L,t)}(L);const i=!e||n&&!!n.releases.find(t=>t.name===e);if(n&&i)Date.now()-n.fetchTimestamp>3e5&&F.fetchReleaseAndReadme(n).then(e=>s(L,e));else{const t=n?"Downloading Puppeteer release "+e:"Please give us a few seconds to download Puppeteer releases for the first time.\n Next time we'll do it in background.";app.setLoadingScreen(!0,t),n=await F.fetchReleaseAndReadme(n),await s(L,n),app.setLoadingScreen(!1)}return new F(n.readmeText,n.releases,n.fetchTimestamp);async function s(e,n){return r?localStorage.setItem(e,JSON.stringify(n)):function(e,t,r=v()){return r._withIDBStore("readwrite",r=>{r.put(t,e)})}(L,n,t)}}constructor(e,t,r){super(),this._readmeText=e,this._releases=t,this._fetchTimestamp=r,this._initializeAPILifespan()}toolbarElements(){function e(e,t,n){return r``}return[e("https://stackoverflow.com/questions/tagged/puppeteer","./images/stackoverflow.svg","pptr-stackoverflow"),e("https://github.com/GoogleChrome/puppeteer/blob/main/docs/troubleshooting.md","./images/wrench.svg","pptr-troubleshooting"),e("https://github.com/GoogleChrome/puppeteer","./images/github.png","pptr-github")]}_initializeAPILifespan(){const e=/### class:\s+(\w+)\s*$/,t=/#### event:\s+'(\w+)'\s*$/,r=/#### \w+\.([\w$]+)\(/,n=/#### \w+\.(\w+)\s*$/;for(const i of this._releases){i.classesLifespan=new Map;let s=null;const o=i.apiText.split("\n").filter(e=>e.startsWith("###"));for(const a of o)if(e.test(a)){s&&i.classesLifespan.set(s.name,s),s={name:a.match(e)[1],since:i.name,until:"",eventsSince:new Map,methodsSince:new Map,namespacesSince:new Map,eventsUntil:new Map,methodsUntil:new Map,namespacesUntil:new Map}}else if(t.test(a)){console.assert(s);const e=a.match(t)[1];s.eventsSince.set(e,i.name)}else if(r.test(a)){console.assert(s);const e=a.match(r)[1];s.methodsSince.set(e,i.name)}else if(n.test(a)){console.assert(s);const e=a.match(n)[1];s.namespacesSince.set(e,i.name)}s&&i.classesLifespan.set(s.name,s)}for(let e=this._releases.length-2;e>=0;--e){const t=this._releases[e+1],r=this._releases[e];for(const[e,n]of r.classesLifespan){const r=t.classesLifespan.get(e);if(r){n.since=r.since;for(const[e,t]of r.eventsSince)n.eventsSince.has(e)&&n.eventsSince.set(e,t);for(const[e,t]of r.methodsSince)n.methodsSince.has(e)&&n.methodsSince.set(e,t);for(const[e,t]of r.namespacesSince)n.namespacesSince.has(e)&&n.namespacesSince.set(e,t)}}}for(let e=0;ee.name)}versionDescriptions(){return this._releases.map(e=>({name:e.name,description:e.chromiumVersion,date:e.timestamp?new Date(e.timestamp):null}))}settingsFooterElement(){const e=Date.now()-this._fetchTimestamp;let t="";return e<1e3?t="Just Now":1e3<=e&&e<=6e4?t=`${Math.round(e/1e3)} seconds ago`:6e4<=e&&e<=36e5?t=`${Math.round(e/60/1e3)} minutes ago`:36e5<=e&&e<=864e5?t=`${Math.round(e/60/60/1e3)} hours ago`:864e5<=e&&(t=`${Math.round(e/24/60/60/1e3)} days ago`),r`Data fetched ${t}`}create404(e=""){return{element:r` - - -

Not Found

-

${e}

-

Home

-
-
- `,title:"Not Found"}}getVersion(e){const t=this._releases.find(t=>t.name===e);return t?new N(this._readmeText,t):null}}class N extends f.ProductVersion{constructor(e,{name:t,releaseNotes:r,apiText:n,classesLifespan:i}){super(),this._name=t,this._readmeText=e,this.api=C.create(t,r,n,i),this._sidebarElements=[],this._entryToSidebarElement=new Map,this._initializeSidebarElements(),this._searchItems=[];for(const e of this.api.classes){this._searchItems.push(T.createForClass(e));for(const t of e.events)this._searchItems.push(T.createForEvent(t));for(const t of e.namespaces)this._searchItems.push(T.createForNamespace(t));for(const t of e.methods)this._searchItems.push(T.createForMethod(t))}}name(){return this._name}searchItems(){return this._searchItems}sidebarElements(){return this._sidebarElements}toolbarElements(){return this._toolbarElements}content(e){if(!e){const e=r` - - - ${Array.from(C.markdownToDOM(this._readmeText).childNodes)} - - - `,t=e.querySelector("img[align=right]");if(t){t.remove();const r=e.querySelector("content-box");r.insertBefore(t,r.firstChild)}return{element:e,title:""}}if("outline"===e){return{element:r`${this.api.createOutline()}`,title:"",selectedSidebarElement:this._outlineItem}}const t=this.api.idToEntry(e);if(!t)return null;if(t instanceof w){return{element:this._showAPIClass(t),title:t.name,selectedSidebarElement:this._entryToSidebarElement.get(t)}}if(t instanceof y){return{element:r` - - ${t.element} - - `,title:"",selectedSidebarElement:this._entryToSidebarElement.get(t)}}const n=this._showAPIClass(t.apiClass),i=this._scrollAnchor(t.element);return{element:n,title:t.apiClass.loweredName+"."+t.name,selectedSidebarElement:this._entryToSidebarElement.get(t.apiClass),scrollAnchor:i}}_initializeSidebarElements(){this._outlineItem=r`Outline`,this._sidebarElements=[r`API`,this._outlineItem,...this.api.sections.map(e=>{const t=r`${e.name}`;return this._entryToSidebarElement.set(e,t),t}),...this.api.classes.map(e=>{const t=r`${e.name}`;return this._entryToSidebarElement.set(e,t),t})]}_showAPIClass(e){function t(e,t){return t.length?r` -

${e}

- ${t.map(e=>r` - ${e.element} - - `)} - - `:""}return r` - - ${e.element} - ${t("NameSpaces",e.namespaces)} - ${t("Events",e.events)} - ${t("Methods",e.methods)} - - - - - `}_scrollAnchor(e){if(e.previousSibling&&"CONTENT-DELIMETER"===e.previousSibling.tagName)return e.previousSibling;let t=e;for(;t&&"CONTENT-BOX"!==t.tagName;)t=t.parentElement;return t}}class T extends h.Item{static createForMethod(e){const t=e.apiClass.loweredName,r=e.name,n=e.args,i=e.element.querySelector("p");return new T(e,`${t}.${r}(${n})`,"pptr-method-icon",e=>q(e,[{text:t+".",tagName:"search-item-api-method-class"},{text:`${r}(${n})`,tagName:"search-item-api-method-name"}]),i?i.textContent:"")}static createForEvent(e){const t=e.apiClass.loweredName,r=e.name,n=e.element.querySelector("p");return new T(e,`${t}.on('${r}')`,"pptr-event-icon",e=>q(e,[{text:t+".on(",tagName:"search-item-api-method-class"},{text:`'${r}'`,tagName:"search-item-api-method-name"},{text:")",tagName:"search-item-api-method-class"}]),n?n.textContent:"")}static createForNamespace(e){const t=e.apiClass.loweredName,r=e.name,n=e.element.querySelector("p");return new T(e,`${t}.${r}`,"pptr-ns-icon",e=>q(e,[{text:t+".",tagName:"search-item-api-method-class"},{text:r,tagName:"search-item-api-method-name"}]),n?n.textContent:"")}static createForClass(e){const t=e.name,r=e.element.querySelector("p");return new T(e,t,"pptr-class-icon",e=>q(e,[{text:t,tagName:"search-item-api-method-name"}]),r?r.textContent:"")}constructor(e,t,r,n,i){super(),this._url=e.linkURL(S),this._text=t,this._iconTagName=r,this._titleRenderer=n,this._description=i,this._subtitleElement=null,this._iconElement=null}url(){return this._url}text(){return this._text}titleElement(e){return this._titleRenderer.call(null,e)}iconElement(){return!this._iconElement&&this._iconTagName&&(this._iconElement=document.createElement(this._iconTagName)),this._iconElement}subtitleElement(){return this._description?(this._subtitleElement||(this._subtitleElement=document.createTextNode(this._description)),this._subtitleElement):null}}function q(e,t){if(!e.length){const e=document.createDocumentFragment();for(let r of t)if(r.tagName){const t=document.createElement(r.tagName);t.textContent=r.text,e.appendChild(t)}else e.appendChild(document.createTextNode(r.text));return e}const r=document.createDocumentFragment();let n=0,i=new Set(e);for(let e of t){const t=e.tagName?document.createElement(e.tagName):document.createDocumentFragment();let s=0,o=!1;for(let r=0;r<=e.text.length;++r){const a=i.has(r+n);if(!(a===o&&r{window.app=new f(document.body);const e=new Promise(e=>window.addEventListener("load",e)),t=await F.create(f.urlVersionName());await e,app.initialize(t)}),window.__WEBSITE_VERSION__&&window.addEventListener("load",()=>{"serviceWorker"in navigator&&navigator.serviceWorker.register("./sw.js")})}(); \ No newline at end of file diff --git a/docs/issue_template.md b/docs/issue_template.md new file mode 100644 index 0000000..8284b24 --- /dev/null +++ b/docs/issue_template.md @@ -0,0 +1,40 @@ + + +### Steps to reproduce + +**Tell us about your environment:** + +* Puppeteer version: +* Platform / OS version: +* URLs (if applicable): +* Node.js version: + +**What steps will reproduce the problem?** + +_Please include code that reproduces the issue._ + +1. +2. +3. + +**What is the expected result?** + + +**What happens instead?** + diff --git a/docs/style.css b/docs/style.css deleted file mode 100644 index 51c030d..0000000 --- a/docs/style.css +++ /dev/null @@ -1,3 +0,0 @@ -/* THIS FILE IS GENERATED BY build.js */ - -content-component{background-color:#fafafa;position:absolute;left:var(--sidebar-width);top:var(--search-height);bottom:0;right:0;overflow-y:scroll;-webkit-overflow-scrolling:touch}@media only screen and (max-width:800px){content-component{left:0}}:root{--divider-color: rgba(0, 0, 0, 0.14);--sidebar-width: 200px;--search-height: 50px;--hover-color: rgba(225, 245, 254, 0.49);--selected-color: #e3f2fd;--monospace: Consolas, Menlo, monospace;--non-monospace: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--black: #24292e;font-family:var(--non-monospace);line-height:1.5;color:var(--black)}body{background-color:#fafafa}settings-button{display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:.5;flex-shrink:0;display:none;transform:scale(.8)}settings-button img{width:31px}settings-button:hover{opacity:1}home-button{display:flex;align-items:center;justify-content:center;transform:scale(.75);cursor:pointer;opacity:.5;flex-shrink:0}home-button:hover{opacity:1}menu-button{display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:.5;flex-shrink:0;display:none}menu-button:hover{opacity:1}search-button{padding-right:4px;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:.5;flex-shrink:0;display:none}search-button:hover{opacity:1}app-title{font-size:20px;color:#fff;white-space:nowrap;flex-shrink:0;margin-left:1ex}.show-mobile-search app-title{display:none}app-title-version-name{border-bottom:1px dashed #fff;cursor:pointer;margin:0 1ex}app-title-version-name:hover{border-bottom:2px dashed #fff}a{font-weight:400!important;color:#0366d6;text-decoration:none}a:hover{text-decoration:underline}external-link-icon{background-position:center right;background-repeat:no-repeat;background-image:linear-gradient(transparent,transparent),url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22%3E %3Cpath fill=%22%23fff%22 stroke=%22%2336c%22 d=%22M1.5 4.518h5.982V10.5H1.5z%22/%3E %3Cpath fill=%22%2336c%22 d=%22M5.765 1H11v5.39L9.427 7.937l-1.31-1.31L5.393 9.35l-2.69-2.688 2.81-2.808L4.2 2.544z%22/%3E %3Cpath fill=%22%23fff%22 d=%22M9.995 2.004l.022 4.885L8.2 5.07 5.32 7.95 4.09 6.723l2.882-2.88-1.85-1.852z%22/%3E %3C/svg%3E");width:13px;height:13px;display:inline-block}blockquote{background:#fffde7;padding:1px 1em 1px 2em;margin:2em 0;border-left:.25em solid #ffeb3b}loading-screen{position:absolute;display:flex;align-items:center;justify-content:center;left:0;right:0;top:0;bottom:0}loading-screen loading-content{display:flex;flex-direction:column;align-items:center}loading-screen img{opacity:.1}loading-screen .text{white-space:pre-wrap;text-align:center;margin:2em}loading-screen .spinner{width:50px;height:40px;text-align:center;font-size:10px;margin-top:30px}loading-screen .spinner>div{background-color:#40b5a491;height:100%;width:6px;display:inline-block;-webkit-animation:sk-stretchdelay 1.2s infinite ease-in-out;animation:sk-stretchdelay 1.2s infinite ease-in-out;margin-left:3px}loading-screen .spinner .rect2{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}loading-screen .spinner .rect3{-webkit-animation-delay:-1s;animation-delay:-1s}loading-screen .spinner .rect4{-webkit-animation-delay:-.9s;animation-delay:-.9s}loading-screen .spinner .rect5{-webkit-animation-delay:-.8s;animation-delay:-.8s}@-webkit-keyframes sk-stretchdelay{0%,40%,to{-webkit-transform:scaleY(.4)}20%{-webkit-transform:scaleY(1)}}@keyframes sk-stretchdelay{0%,40%,to{transform:scaleY(.4);-webkit-transform:scaleY(.4)}20%{transform:scaleY(1);-webkit-transform:scaleY(1)}}@media only screen and (max-width:800px){search-button,settings-button,menu-button{display:flex}app-title{display:none}}search-component{position:absolute;left:0;right:0;top:var(--search-height);bottom:0;background:rgba(0,0,0,.5);justify-content:center;overflow:hidden;align-items:start;--search-item-icon-width: 20px;--search-item-gap: 13px;--search-item-padding: 18px;contain:strict}search-component search-results{max-width:calc(100% - var(--results-left));--results-left: calc(var(--search-input-x) - var(--search-item-gap) - var(--search-item-icon-width) - var(--search-item-padding) + 1ex);left:var(--results-left);width:700px;max-height:700px;background:#fff;overflow:auto;position:relative;display:block}@media only screen and (max-width:800px){search-component search-results{width:100%;max-width:100%;max-height:100%;left:0;right:0;top:0;bottom:0;position:absolute}}search-highlight{background:#ff0}search-item{display:grid;align-items:center;grid-template-columns:var(--search-item-icon-width) auto;grid-template-rows:auto auto;grid-column-gap:var(--search-item-gap);grid-template-areas:"icon title" "icon subtitle";padding:4px var(--search-item-padding);cursor:pointer;min-height:50px;border-bottom:1px solid rgba(51,51,51,.12)}search-item.no-subtitle{grid-template-areas:"icon title";grid-template-rows:auto}search-item-custom{display:flex;align-items:center;justify-content:center;font-family:var(--monospace);height:50px;border-bottom:1px solid rgba(51,51,51,.12)}search-item-custom.selected,search-item.selected{background-color:#e3f2fd}search-item-custom:hover,search-item:hover{background-color:var(--hover-color)}search-item-icon{grid-area:icon;display:flex;align-items:center;justify-content:center}search-item-title{grid-area:title;font-family:var(--monospace);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}search-item-subtitle{grid-area:subtitle;font-size:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}settings-component{position:absolute;left:0;right:0;top:0;bottom:0;background:rgba(0,0,0,.5);display:flex;justify-content:center;overflow:hidden;align-items:center;z-index:1000}settings-component settings-content{max-width:700px;width:70%;max-height:70%;background:#fff;overflow:hidden;flex-grow:0;display:flex;flex-direction:column;padding:1em;position:relative}@media only screen and (max-width:800px){settings-component settings-content{max-width:100%;max-height:100%;position:absolute;left:0;right:0;top:0;bottom:0;width:unset;height:unset}}settings-component product-versions{display:block;border:.25em solid var(--divider-color);overflow:auto;-webkit-overflow-scrolling:touch}settings-component settings-header{border-bottom:1px solid var(--divider-color);display:flex;justify-content:space-between;padding:0 1ex;flex-shrink:0}settings-component settings-header h3{margin:0;font-weight:400;font-size:24px;margin-bottom:1ex}settings-component .settings-close-icon{opacity:.5;cursor:pointer}settings-component .settings-close-icon:hover{opacity:1}settings-component product-version{display:grid;grid-template-columns:100px 100px auto;grid-template-rows:auto;grid-template-areas:"name date description";grid-column-gap:10px;padding:1em;align-items:center;border-bottom:1px solid rgba(51,51,51,.12);cursor:pointer}settings-component product-version:hover{background-color:var(--hover-color)}settings-component product-version.selected{background-color:var(--selected-color)}settings-component product-version version-name{grid-area:name;font-family:var(--monospace)}settings-component product-version version-description{grid-area:description;font-size:90%;justify-self:center}settings-component product-version version-date{grid-area:date;font-size:90%}settings-component website-version{display:flex;flex-direction:row;justify-content:center;font-size:90%;align-items:baseline}settings-component website-version code{margin:0 1ex}sidebar-component{background:#fff;position:absolute;width:var(--sidebar-width);top:var(--search-height);bottom:0;left:0;overflow-y:scroll;-webkit-overflow-scrolling:touch;border-right:1px solid var(--divider-color);overflow-x:hidden}sidebar-component sidebar-item{display:flex}sidebar-component sidebar-item:hover{background-color:var(--hover-color)}sidebar-component sidebar-item.selected{background-color:var(--selected-color)}sidebar-component sidebar-glasspane{display:none}@media only screen and (max-width:800px){sidebar-component{display:none}sidebar-component.show-on-mobile{display:block;left:0;right:0;top:var(--search-height);bottom:0}sidebar-glasspane.show-on-mobile{display:block;position:absolute;left:0;right:0;top:var(--search-height);bottom:0;background-color:rgba(0,0,0,.5)}}toolbar-component{background:#40b5a4;position:absolute;left:0;right:0;top:0;height:var(--search-height);border-bottom:1px solid rgba(0,0,0,.14);display:flex;align-items:center;justify-content:space-between;overflow:hidden;z-index:100;padding:0 15px}toolbar-component input[type=search]{-webkit-appearance:none;background:0 0;text-align:left;border:0;border-bottom:2px solid #fff;padding:0 1ex;font-size:20px;color:#fff;margin:0 1ex;text-overflow:ellipsis;max-width:250px;flex-grow:1}toolbar-component input[type=search]::placeholder{color:#eeeeeeb0;text-align:center}toolbar-component input[type=search]:focus{outline:0}toolbar-component toolbar-section{display:flex;align-items:center;flex-grow:0}toolbar-component toolbar-section.left{flex-grow:1}@media only screen and (max-width:800px){toolbar-section.right{display:none}toolbar-component input[type=search]{width:initial}}pptr-api>h3{text-align:left;margin-left:1em}pptr-api content-box{display:block;max-width:890px;border-radius:2px;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);margin:16px 45px 32px 45px;background-color:#fff;padding:0 24px 0 24px;box-sizing:border-box;overflow-x:hidden}@media only screen and (max-width:800px){pptr-api content-box{margin:7px 5px}}pptr-api.pptr-readme img{background:#fff}pptr-settings-footer{display:flex;align-items:center;justify-content:center;font-size:90%;margin:1em}pptr-api-padding{height:75vh;max-width:890px;display:flex;align-items:center;justify-content:center;margin:16px 45px 32px 45px}pptr-api content-delimeter{display:block;border-bottom:double #b6b6b6a6 3px;width:200%;position:relative;left:-25%}pptr-api content-delimeter:last-child{display:none}pptr-api api-class h3,pptr-api api-method h4,pptr-api api-event h4,pptr-api api-ns h4{white-space:nowrap;display:flex;align-items:center}pptr-api .api-entry{display:block}pptr-api ul.pptr-table-of-contents li{white-space:nowrap}pptr-api li+li{margin-top:.25em}pptr-api h6{font-size:.85em;color:#6a737d}pptr-api api-method-classname,pptr-api api-ns-classname{color:#ababab}pptr-api api-event h4{color:#ababab}pptr-api api-event-name{color:var(--black)}pptr-api api-section img{max-width:100%}pptr-api-since{display:inline-block;font-size:10px;color:#ababab;border-radius:2px;padding:1px 3px;font-weight:400;vertical-align:super;font-family:var(--monospace);user-select:none;align-self:self-start}pptr-api-since::before{content:'since '}pptr-api-since.pptr-new-api{background:#81c784d4;color:#fff;margin-left:4px}pptr-api-since.pptr-new-api::before{content:'new in '}pptr-api-until{display:inline-block;font-size:10px;color:#fff;background:#ef9a9a;border-radius:2px;padding:1px 3px;font-weight:400;vertical-align:super;user-select:none;margin-left:4px;align-self:self-start}pptr-api-until::before{content:'removed '}.pptr-sidebar-item{text-decoration:none;display:flex;flex-direction:row;align-items:center;font-size:14px;font-weight:400;line-height:24px;height:48px;padding:0 16px;width:100%}.pptr-sidebar-item{text-decoration:none!important}.pptr-stackoverflow img{width:29px;opacity:.5}.pptr-stackoverflow img:hover{opacity:1}.pptr-troubleshooting{padding:0 11px 0 3px}.pptr-troubleshooting img{opacity:.5;width:22px}.pptr-troubleshooting img:hover{opacity:1}.pptr-github{padding:0 3px}.pptr-github img:hover{opacity:1}.pptr-github img{width:22px;opacity:.5}pptr-api code:not(.language-js){font-family:var(--monospace);padding:.2em .4em;background-color:rgba(27,31,35,.05);border-radius:3px;font-size:85%}pptr-api code.language-js{font-family:var(--monospace);display:block;padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f6f8fa;border-radius:3px}pptr-sidebar-divider{font-size:12px;font-weight:700;padding-left:6px;text-align:left;display:flex;padding:4px 2ex;background-color:#fafafa;border-top:1px solid var(--divider-color);border-bottom:1px solid var(--divider-color);width:100%;justify-content:space-between;align-items:center}pptr-api li.table-of-contents-entry{white-space:nowrap}pptr-event-icon,pptr-class-icon,pptr-ns-icon,pptr-method-icon{display:inline-flex;width:16px;font-size:12px;color:#fff;border-radius:3px;user-select:none;font-family:var(--monospace);height:16px;align-items:center;justify-content:center;font-weight:700;flex-shrink:0}pptr-api pptr-event-icon,pptr-api pptr-method-icon,pptr-api pptr-ns-icon,pptr-api pptr-class-icon{margin-right:1ex}sidebar-item pptr-event-icon,sidebar-item pptr-method-icon,sidebar-item pptr-ns-icon,sidebar-item pptr-class-icon{margin-right:1ex}pptr-method-icon{background-color:#ff9800}pptr-method-icon::after{content:'M'}pptr-event-icon{background-color:#4caf50}pptr-event-icon::after{content:'E'}pptr-class-icon{background-color:#3f51b5}pptr-class-icon::after{content:'C'}pptr-ns-icon{background-color:#9e9e9e}pptr-ns-icon::after{content:'N'}search-item-api-method-class{color:#ababab}pptr-api .cm-negative{color:#d44}pptr-api .cm-positive{color:#292}pptr-api .cm-header,.cm-strong{font-weight:700}pptr-api .cm-em{font-style:italic}pptr-api .cm-link{text-decoration:underline}pptr-api .cm-strikethrough{text-decoration:line-through}pptr-api .cm-invalidchar{color:red}pptr-api .cm-header{color:#00f}pptr-api .cm-quote{color:#090}pptr-api .cm-keyword{color:#708}pptr-api .cm-atom{color:#219}pptr-api .cm-number{color:#164}pptr-api .cm-def{color:#00f}pptr-api .cm-variable-2{color:#05a}pptr-api .cm-variable-3,.cm-type{color:#085}pptr-api .cm-comment{color:#a50}pptr-api .cm-string{color:#a11}pptr-api .cm-string-2{color:#f50}pptr-api .cm-meta{color:#555}pptr-api .cm-qualifier{color:#555}pptr-api .cm-builtin{color:#30a}pptr-api .cm-bracket{color:#997}pptr-api .cm-tag{color:#170}pptr-api .cm-attribute{color:#00c}pptr-api .cm-hr{color:#999}pptr-api .cm-link{color:#00c}pptr-api .cm-error{color:red} \ No newline at end of file diff --git a/docs/sw.js b/docs/sw.js deleted file mode 100644 index 28b1c4d..0000000 --- a/docs/sw.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * 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. - */ - -importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.1.0/workbox-sw.js"); - -workbox.core.skipWaiting(); -workbox.core.clientsClaim(); - -// Enable offline GoogleAnalytics. -workbox.googleAnalytics.initialize(); - -workbox.precaching.precacheAndRoute([ - { - "url": "favicons/android-chrome-192x192.png", - "revision": "05be0b0c0d82fd26e50eef9fd255f21e" - }, - { - "url": "favicons/android-chrome-384x384.png", - "revision": "0b84f07f6d1a9f09b52f47dc9a744ba8" - }, - { - "url": "favicons/apple-touch-icon.png", - "revision": "f993b772518d7d488b1f2f7e03908f22" - }, - { - "url": "favicons/browserconfig.xml", - "revision": "a89eb8c368c15b982c5808ce906e8ac2" - }, - { - "url": "favicons/favicon-16x16.png", - "revision": "9af3980ba3ffc11ccb9b01dfbe2555b6" - }, - { - "url": "favicons/favicon-32x32.png", - "revision": "e2f8da1b76492c82494e071e4c9e3d64" - }, - { - "url": "favicons/favicon.ico", - "revision": "ddd8a3f15f6a2a620d9011aaf2559178" - }, - { - "url": "favicons/mstile-150x150.png", - "revision": "203fc1bff1934f7b478c0bd2c6713854" - }, - { - "url": "favicons/safari-pinned-tab.svg", - "revision": "d4e8575115db91ea0154f94a64686b08" - }, - { - "url": "favicons/site.webmanifest", - "revision": "12f48e2d6dcf0f66bf08ff7be81175f7" - }, - { - "url": "images/checkmark.svg", - "revision": "094c946e202c6b49672d422f3c50307f" - }, - { - "url": "images/close.svg", - "revision": "e22e537e8340ffc1ab24bfe61956644f" - }, - { - "url": "images/cog.svg", - "revision": "767280f704baffa60642f681d490ca3b" - }, - { - "url": "images/github.png", - "revision": "d56df49a807a9fd06eb1667a84d3810e" - }, - { - "url": "images/home.svg", - "revision": "5a5d43fb5504cd8984e4e59f1832ee3e" - }, - { - "url": "images/menu.svg", - "revision": "6aa1bfbadbdeb0e73c5c945c2dbc0019" - }, - { - "url": "images/pptr.png", - "revision": "924f28bd0281fb8e45b19f1364cfaf8e" - }, - { - "url": "images/search.svg", - "revision": "69536ad6f996355f6c73270a0c018ac8" - }, - { - "url": "images/slack.svg", - "revision": "9619ac706025ffb434bd25df418e5591" - }, - { - "url": "images/stackoverflow.svg", - "revision": "4f62b6320d08038f64406b39f3943108" - }, - { - "url": "images/wrench.svg", - "revision": "dba7613975ea6b812b22d6a6c8f74db7" - }, - { - "url": "index.html", - "revision": "06b84c4670aeb2ff634d824d45c79202" - }, - { - "url": "index.js", - "revision": "186a08ed5d613abb31bad0dd19488180" - }, - { - "url": "style.css", - "revision": "5634f6d684b9b32efdf002f63a4e133d" - } -], {}); - -// This is needed to make SPA to work offline. -workbox.routing.registerNavigationRoute("index.html"); - -// Cache common github images (e.g. pptr logo). -workbox.routing.registerRoute(/^https:\/\/user-images\.githubusercontent\.com\/.*/, new workbox.strategies.StaleWhileRevalidate(), 'GET'); diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..979c1dd --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,453 @@ +# Troubleshooting + + + + +- [Chrome headless doesn't launch on Windows](#chrome-headless-doesnt-launch-on-windows) +- [Chrome headless doesn't launch on UNIX](#chrome-headless-doesnt-launch-on-unix) +- [Chrome is downloaded but fails to launch on Node.js 14](#chrome-is-downloaded-but-fails-to-launch-on-nodejs-14) +- [Setting Up Chrome Linux Sandbox](#setting-up-chrome-linux-sandbox) + * [[recommended] Enable user namespace cloning](#recommended-enable-user-namespace-cloning) + * [[alternative] Setup setuid sandbox](#alternative-setup-setuid-sandbox) +- [Running Puppeteer on Travis CI](#running-puppeteer-on-travis-ci) +- [Running Puppeteer on CircleCI](#running-puppeteer-on-circleci) +- [Running Puppeteer in Docker](#running-puppeteer-in-docker) + * [Running on Alpine](#running-on-alpine) + - [Tips](#tips) +- [Running Puppeteer in the cloud](#running-puppeteer-in-the-cloud) + * [Running Puppeteer on Google App Engine](#running-puppeteer-on-google-app-engine) + * [Running Puppeteer on Google Cloud Functions](#running-puppeteer-on-google-cloud-functions) + * [Running Puppeteer on Heroku](#running-puppeteer-on-heroku) + * [Running Puppeteer on AWS Lambda](#running-puppeteer-on-aws-lambda) + * [Running Puppeteer on AWS EC2 instance running Amazon-Linux](#running-puppeteer-on-aws-ec2-instance-running-amazon-linux) +- [Code Transpilation Issues](#code-transpilation-issues) + + + + +## Chrome headless doesn't launch on Windows + +Some [chrome policies](https://support.google.com/chrome/a/answer/7532015) might enforce running Chrome/Chromium +with certain extensions. + +Puppeteer passes `--disable-extensions` flag by default and will fail to launch when such policies are active. + +To work around this, try running without the flag: + +```js +const browser = await puppeteer.launch({ + ignoreDefaultArgs: ['--disable-extensions'], +}); +``` + +> Context: [issue 3681](https://github.com/puppeteer/puppeteer/issues/3681#issuecomment-447865342). + +## Chrome headless doesn't launch on UNIX + +Make sure all the necessary dependencies are installed. You can run `ldd chrome | grep not` on a Linux +machine to check which dependencies are missing. The common ones are provided below. + +
+Debian (e.g. Ubuntu) Dependencies + +``` +ca-certificates +fonts-liberation +libappindicator3-1 +libasound2 +libatk-bridge2.0-0 +libatk1.0-0 +libc6 +libcairo2 +libcups2 +libdbus-1-3 +libexpat1 +libfontconfig1 +libgbm1 +libgcc1 +libglib2.0-0 +libgtk-3-0 +libnspr4 +libnss3 +libpango-1.0-0 +libpangocairo-1.0-0 +libstdc++6 +libx11-6 +libx11-xcb1 +libxcb1 +libxcomposite1 +libxcursor1 +libxdamage1 +libxext6 +libxfixes3 +libxi6 +libxrandr2 +libxrender1 +libxss1 +libxtst6 +lsb-release +wget +xdg-utils +``` + +
+ +
+CentOS Dependencies + +``` +alsa-lib.x86_64 +atk.x86_64 +cups-libs.x86_64 +gtk3.x86_64 +ipa-gothic-fonts +libXcomposite.x86_64 +libXcursor.x86_64 +libXdamage.x86_64 +libXext.x86_64 +libXi.x86_64 +libXrandr.x86_64 +libXScrnSaver.x86_64 +libXtst.x86_64 +pango.x86_64 +xorg-x11-fonts-100dpi +xorg-x11-fonts-75dpi +xorg-x11-fonts-cyrillic +xorg-x11-fonts-misc +xorg-x11-fonts-Type1 +xorg-x11-utils +``` + +After installing dependencies you need to update nss library using this command + +``` +yum update nss -y +``` + +
+ +
+ Check out discussions + +- [#290](https://github.com/puppeteer/puppeteer/issues/290) - Debian troubleshooting
+- [#391](https://github.com/puppeteer/puppeteer/issues/391) - CentOS troubleshooting
+- [#379](https://github.com/puppeteer/puppeteer/issues/379) - Alpine troubleshooting
+
+ +## Chrome is downloaded but fails to launch on Node.js 14 + +If you get an error that looks like this when trying to launch Chromium: + +``` +(node:15505) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process! +spawn /Users/.../node_modules/puppeteer/.local-chromium/mac-756035/chrome-mac/Chromium.app/Contents/MacOS/Chromium ENOENT +``` + +This means that the browser was downloaded but failed to be extracted correctly. The most common cause is a bug in Node.js v14.0.0 which broke `extract-zip`, the module Puppeteer uses to extract browser downloads into the right place. The bug was fixed in Node.js v14.1.0, so please make sure you're running that version or higher. Alternatively, if you cannot upgrade, you could downgrade to Node.js v12, but we recommend upgrading when possible. + +## Setting Up Chrome Linux Sandbox + +In order to protect the host environment from untrusted web content, Chrome uses [multiple layers of sandboxing](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux/sandboxing.md). For this to work properly, +the host should be configured first. If there's no good sandbox for Chrome to use, it will crash +with the error `No usable sandbox!`. + +If you **absolutely trust** the content you open in Chrome, you can launch Chrome +with the `--no-sandbox` argument: + +```js +const browser = await puppeteer.launch({ + args: ['--no-sandbox', '--disable-setuid-sandbox'], +}); +``` + +> **NOTE**: Running without a sandbox is **strongly discouraged**. Consider configuring a sandbox instead. + +There are 2 ways to configure a sandbox in Chromium. + +### [recommended] Enable [user namespace cloning](http://man7.org/linux/man-pages/man7/user_namespaces.7.html) + +User namespace cloning is only supported by modern kernels. Unprivileged user namespaces are generally fine to enable, +but in some cases they open up more kernel attack surface for (unsandboxed) non-root processes to elevate to +kernel privileges. + +```bash +sudo sysctl -w kernel.unprivileged_userns_clone=1 +``` + +### [alternative] Setup [setuid sandbox](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux/suid_sandbox_development.md) + +The setuid sandbox comes as a standalone executable and is located next to the Chromium that Puppeteer downloads. It is +fine to re-use the same sandbox executable for different Chromium versions, so the following could be +done only once per host environment: + +```bash +# cd to the downloaded instance +cd /node_modules/puppeteer/.local-chromium/linux-/chrome-linux/ +sudo chown root:root chrome_sandbox +sudo chmod 4755 chrome_sandbox +# copy sandbox executable to a shared location +sudo cp -p chrome_sandbox /usr/local/sbin/chrome-devel-sandbox +# export CHROME_DEVEL_SANDBOX env variable +export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox +``` + +You might want to export the `CHROME_DEVEL_SANDBOX` env variable by default. In this case, add the following to the `~/.bashrc` +or `.zshenv`: + +```bash +export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox +``` + +## Running Puppeteer on Travis CI + +> 👋 We ran our tests for Puppeteer on Travis CI until v6.0.0 (when we've migrated to GitHub Actions) - see our historical [`.travis.yml` (v5.5.0)](https://github.com/puppeteer/puppeteer/blob/v5.5.0/.travis.yml) for reference. + +Tips-n-tricks: + +- [xvfb](https://en.wikipedia.org/wiki/Xvfb) service should be launched in order to run Chromium in non-headless mode +- Runs on Xenial Linux on Travis by default +- Runs `npm install` by default +- `node_modules` is cached by default + +`.travis.yml` might look like this: + +```yml +language: node_js +node_js: node +services: xvfb + +script: + - npm run test +``` + +## Running Puppeteer on CircleCI + +Running Puppeteer smoothly on CircleCI requires the following steps: + +1. Start with a [NodeJS + image](https://circleci.com/docs/2.0/circleci-images/#nodejs) in your config + like so: + ```yaml + docker: + - image: circleci/node:12 # Use your desired version + environment: + NODE_ENV: development # Only needed if puppeteer is in `devDependencies` + ``` +1. Dependencies like `libXtst6` probably need to be installed via `apt-get`, + so use the + [threetreeslight/puppeteer](https://circleci.com/orbs/registry/orb/threetreeslight/puppeteer) + orb + ([instructions](https://circleci.com/orbs/registry/orb/threetreeslight/puppeteer#quick-start)), + or paste parts of its + [source](https://circleci.com/orbs/registry/orb/threetreeslight/puppeteer#orb-source) + into your own config. +1. Lastly, if you’re using Puppeteer through Jest, then you may encounter an + error spawning child processes: + ``` + [00:00.0] jest args: --e2e --spec --max-workers=36 + Error: spawn ENOMEM + at ChildProcess.spawn (internal/child_process.js:394:11) + ``` + This is likely caused by Jest autodetecting the number of processes on the + entire machine (`36`) rather than the number allowed to your container + (`2`). To fix this, set `jest --maxWorkers=2` in your test command. + +## Running Puppeteer in Docker + +> 👋 We used [Cirrus Ci](https://cirrus-ci.org/) to run our tests for Puppeteer in a Docker container until v3.0.x - see our historical [`Dockerfile.linux` (v3.0.1)](https://github.com/puppeteer/puppeteer/blob/v3.0.1/.ci/node12/Dockerfile.linux) for reference. + +Getting headless Chrome up and running in Docker can be tricky. +The bundled Chromium that Puppeteer installs is missing the necessary +shared library dependencies. + +To fix, you'll need to install the missing dependencies and the +latest Chromium package in your Dockerfile: + +```Dockerfile +FROM node:12-slim + +# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others) +# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer +# installs, work. +RUN apt-get update \ + && apt-get install -y wget gnupg \ + && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ + && apt-get update \ + && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +# If running Docker >= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise +# uncomment the following lines to have `dumb-init` as PID 1 +# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_x86_64 /usr/local/bin/dumb-init +# RUN chmod +x /usr/local/bin/dumb-init +# ENTRYPOINT ["dumb-init", "--"] + +# Uncomment to skip the chromium download when installing puppeteer. If you do, +# you'll need to launch puppeteer with: +# browser.launch({executablePath: 'google-chrome-stable'}) +# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true + +# Install puppeteer so it's available in the container. +RUN npm i puppeteer \ + # Add user so we don't need --no-sandbox. + # same layer as npm install to keep re-chowned files from using up several hundred MBs more space + && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ + && mkdir -p /home/pptruser/Downloads \ + && chown -R pptruser:pptruser /home/pptruser \ + && chown -R pptruser:pptruser /node_modules + +# Run everything after as non-privileged user. +USER pptruser + +CMD ["google-chrome-stable"] +``` + +Build the container: + +```bash +docker build -t puppeteer-chrome-linux . +``` + +Run the container by passing `node -e ""` as the command: + +```bash + docker run -i --init --rm --cap-add=SYS_ADMIN \ + --name puppeteer-chrome puppeteer-chrome-linux \ + node -e "`cat yourscript.js`" +``` + +There's a full example at https://github.com/ebidel/try-puppeteer that shows +how to run this Dockerfile from a webserver running on App Engine Flex (Node). + +### Running on Alpine + +The [newest Chromium package](https://pkgs.alpinelinux.org/package/edge/community/x86_64/chromium) supported on Alpine is 89, which corresponds to [Puppeteer v6.0.0](https://github.com/puppeteer/puppeteer/releases/tag/v6.0.0). + +Example Dockerfile: + +```Dockerfile +FROM alpine:edge + +# Installs latest Chromium (89) package. +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + harfbuzz \ + ca-certificates \ + ttf-freefont \ + nodejs \ + yarn + +... + +# Tell Puppeteer to skip installing Chrome. We'll be using the installed package. +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ + PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +# Puppeteer v6.0.0 works with Chromium 89. +RUN yarn add puppeteer@6.0.0 + +# Add user so we don't need --no-sandbox. +RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \ + && mkdir -p /home/pptruser/Downloads /app \ + && chown -R pptruser:pptruser /home/pptruser \ + && chown -R pptruser:pptruser /app + +# Run everything after as non-privileged user. +USER pptruser + +... +``` + +#### Tips + +By default, Docker runs a container with a `/dev/shm` shared memory space 64MB. +This is [typically too small](https://github.com/c0b/chrome-in-docker/issues/1) for Chrome +and will cause Chrome to crash when rendering large pages. To fix, run the container with +`docker run --shm-size=1gb` to increase the size of `/dev/shm`. Since Chrome 65, this is no +longer necessary. Instead, launch the browser with the `--disable-dev-shm-usage` flag: + +```js +const browser = await puppeteer.launch({ + args: ['--disable-dev-shm-usage'], +}); +``` + +This will write shared memory files into `/tmp` instead of `/dev/shm`. See [crbug.com/736452](https://bugs.chromium.org/p/chromium/issues/detail?id=736452) for more details. + +Seeing other weird errors when launching Chrome? Try running your container +with `docker run --cap-add=SYS_ADMIN` when developing locally. Since the Dockerfile +adds a `pptr` user as a non-privileged user, it may not have all the necessary privileges. + +[dumb-init](https://github.com/Yelp/dumb-init) is worth checking out if you're +experiencing a lot of zombies Chrome processes sticking around. There's special +treatment for processes with PID=1, which makes it hard to terminate Chrome +properly in some cases (e.g. in Docker). + +## Running Puppeteer in the cloud + +### Running Puppeteer on Google App Engine + +The Node.js runtime of the [App Engine standard environment](https://cloud.google.com/appengine/docs/standard/nodejs/) comes with all system packages needed to run Headless Chrome. + +To use `puppeteer`, simply list the module as a dependency in your `package.json` and deploy to Google App Engine. Read more about using `puppeteer` on App Engine by following [the official tutorial](https://cloud.google.com/appengine/docs/standard/nodejs/using-headless-chrome-with-puppeteer). + +### Running Puppeteer on Google Cloud Functions + +The Node.js 10 runtime of [Google Cloud Functions](https://cloud.google.com/functions/docs/) comes with all system packages needed to run Headless Chrome. + +To use `puppeteer`, simply list the module as a dependency in your `package.json` and deploy your function to Google Cloud Functions using the `nodejs10` runtime. + +### Running Puppeteer on Heroku + +Running Puppeteer on Heroku requires some additional dependencies that aren't included on the Linux box that Heroku spins up for you. To add the dependencies on deploy, add the Puppeteer Heroku buildpack to the list of buildpacks for your app under Settings > Buildpacks. + +The url for the buildpack is https://github.com/jontewks/puppeteer-heroku-buildpack + +Ensure that you're using `'--no-sandbox'` mode when launching Puppeteer. This can be done by passing it as an argument to your `.launch()` call: `puppeteer.launch({ args: ['--no-sandbox'] });`. + +When you click add buildpack, simply paste that url into the input, and click save. On the next deploy, your app will also install the dependencies that Puppeteer needs to run. + +If you need to render Chinese, Japanese, or Korean characters you may need to use a buildpack with additional font files like https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack + +There's also another [simple guide](https://timleland.com/headless-chrome-on-heroku/) from @timleland that includes a sample project: https://timleland.com/headless-chrome-on-heroku/. + +### Running Puppeteer on AWS Lambda + +AWS Lambda [limits](https://docs.aws.amazon.com/lambda/latest/dg/limits.html) deployment package sizes to ~50MB. This presents challenges for running headless Chrome (and therefore Puppeteer) on Lambda. The community has put together a few resources that work around the issues: + +- https://github.com/alixaxel/chrome-aws-lambda (kept updated with the latest stable release of puppeteer) +- https://github.com/adieuadieu/serverless-chrome/blob/master/docs/chrome.md (serverless plugin - outdated) + +### Running Puppeteer on AWS EC2 instance running Amazon-Linux + +If you are using an EC2 instance running amazon-linux in your CI/CD pipeline, and if you want to run Puppeteer tests in amazon-linux, follow these steps. + +1. To install Chromium, you have to first enable `amazon-linux-extras` which comes as part of [EPEL (Extra Packages for Enterprise Linux)](https://aws.amazon.com/premiumsupport/knowledge-center/ec2-enable-epel/): + + ```sh + sudo amazon-linux-extras install epel -y + ``` + +1. Next, install Chromium: + + ```sh + sudo yum install -y chromium + ``` + +Now Puppeteer can launch Chromium to run your tests. If you do not enable EPEL and if you continue installing chromium as part of `npm install`, Puppeteer cannot launch Chromium due to unavailablity of `libatk-1.0.so.0` and many more packages. + +## Code Transpilation Issues + +If you are using a JavaScript transpiler like babel or TypeScript, calling `evaluate()` with an async function might not work. This is because while `puppeteer` uses `Function.prototype.toString()` to serialize functions while transpilers could be changing the output code in such a way it's incompatible with `puppeteer`. + +Some workarounds to this problem would be to instruct the transpiler not to mess up with the code, for example, configure TypeScript to use latest ecma version (`"target": "es2018"`). Another workaround could be using string templates instead of functions: + +```js +await page.evaluate(`(async() => { + console.log('1'); +})()`); +``` diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c23d9df --- /dev/null +++ b/examples/README.md @@ -0,0 +1,39 @@ +# 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: + +```sh +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 Sandbox](https://puppeteersandbox.com) - Puppeteer sandbox environment as a service. Runs Puppeteer scripts and allows saving and embedding them in external sites and markdown files. + +## 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. +- [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. diff --git a/examples/block-images.js b/examples/block-images.js new file mode 100644 index 0000000..298e473 --- /dev/null +++ b/examples/block-images.js @@ -0,0 +1,33 @@ +/** + * 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/examples/cross-browser.js b/examples/cross-browser.js new file mode 100644 index 0000000..f709abf --- /dev/null +++ b/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 = '.storylink'; + 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/examples/custom-event.js b/examples/custom-event.js new file mode 100644 index 0000000..bf19634 --- /dev/null +++ b/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/examples/detect-sniff.js b/examples/detect-sniff.js new file mode 100644 index 0000000..76bb75b --- /dev/null +++ b/examples/detect-sniff.js @@ -0,0 +1,44 @@ +/** + * 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(() => !!navigator.sniffed))); + + await browser.close(); +})(); diff --git a/examples/oopif.js b/examples/oopif.js new file mode 100644 index 0000000..52e2047 --- /dev/null +++ b/examples/oopif.js @@ -0,0 +1,47 @@ +/** + * 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) => (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-proccess + // iframes (OOPIF) are not supported by Puppeteer yet. + // https://github.com/puppeteer/puppeteer/issues/2548 + + await browser.close(); +})(); diff --git a/examples/pdf.js b/examples/pdf.js new file mode 100644 index 0000000..9f7ac92 --- /dev/null +++ b/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/examples/proxy.js b/examples/proxy.js new file mode 100644 index 0000000..5490822 --- /dev/null +++ b/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/examples/screenshot-fullpage.js b/examples/screenshot-fullpage.js new file mode 100644 index 0000000..8844cbc --- /dev/null +++ b/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/examples/screenshot.js b/examples/screenshot.js new file mode 100644 index 0000000..28b4dbb --- /dev/null +++ b/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/examples/search.js b/examples/search.js new file mode 100644 index 0000000..828d155 --- /dev/null +++ b/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-searchbox input', '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-results .gsc-thumbnail-inside a.gs-title'; + 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/experimental/puppeteer-firefox/.ci/node10/Dockerfile.linux b/experimental/puppeteer-firefox/.ci/node10/Dockerfile.linux new file mode 100644 index 0000000..7c3d3ce --- /dev/null +++ b/experimental/puppeteer-firefox/.ci/node10/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:10.18.1-stretch + +RUN apt-get update && \ + apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ + libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ + libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ + libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \ + rm -rf /var/lib/apt/lists/* + +# Add user so we don't need --no-sandbox. +RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ + && mkdir -p /home/pptruser/Downloads \ + && chown -R pptruser:pptruser /home/pptruser + +# Run everything after as non-privileged user. +USER pptruser diff --git a/experimental/puppeteer-firefox/.ci/node10/Dockerfile.windows b/experimental/puppeteer-firefox/.ci/node10/Dockerfile.windows new file mode 100644 index 0000000..8494e33 --- /dev/null +++ b/experimental/puppeteer-firefox/.ci/node10/Dockerfile.windows @@ -0,0 +1,11 @@ +FROM microsoft/windowsservercore:latest + +ENV NODE_VERSION 10.18.1 + +RUN setx /m PATH "%PATH%;C:\nodejs" + +RUN powershell -Command \ + netsh interface ipv4 set subinterface 18 mtu=1460 store=persistent ; \ + Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \ + Expand-Archive node.zip -DestinationPath C:\ ; \ + Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs' diff --git a/experimental/puppeteer-firefox/.cirrus.yml b/experimental/puppeteer-firefox/.cirrus.yml new file mode 100644 index 0000000..ce31e5c --- /dev/null +++ b/experimental/puppeteer-firefox/.cirrus.yml @@ -0,0 +1,31 @@ +env: + DISPLAY: :99.0 + +task: + name: node10 (linux) + container: + dockerfile: .ci/node10/Dockerfile.linux + xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24 + install_script: npm install + test_script: npm run fjunit + +task: + name: node10 (macOS) + osx_instance: + image: high-sierra-base + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + node_install_script: + - brew install node@10 + - brew link --force node@10 + install_script: npm install + test_script: npm run fjunit + +# task: +# allow_failures: true +# windows_container: +# dockerfile: .ci/node10/Dockerfile.windows +# os_version: 2016 +# name: node10 (windows) +# install_script: npm install --unsafe-perm +# test_script: npm run fjunit diff --git a/experimental/puppeteer-firefox/.gitignore b/experimental/puppeteer-firefox/.gitignore new file mode 100644 index 0000000..e8dbe44 --- /dev/null +++ b/experimental/puppeteer-firefox/.gitignore @@ -0,0 +1,10 @@ +/node_modules/ +.DS_Store +*.swp +*.pyc +.vscode +package-lock.json +yarn.lock +.local-browser +/test/output-chromium +/test/output-firefox diff --git a/experimental/puppeteer-firefox/.npmignore b/experimental/puppeteer-firefox/.npmignore new file mode 100644 index 0000000..4578220 --- /dev/null +++ b/experimental/puppeteer-firefox/.npmignore @@ -0,0 +1,36 @@ +# exclude all tests +test +utils/node6-transform + +# exclude internal type definition files +/lib/*.d.ts +/node6/lib/*.d.ts + +# repeats from .gitignore +node_modules +.local-chromium +.local-browser +.dev_profile* +.DS_Store +*.swp +*.pyc +.vscode +package-lock.json +/node6/test +/node6/utils +/test +/utils +/docs +yarn.lock + +# other +/.ci +/examples +.appveyour.yml +.cirrus.yml +.editorconfig +.eslintignore +.eslintrc.js +README.md +tsconfig.json + diff --git a/experimental/puppeteer-firefox/DeviceDescriptors.js b/experimental/puppeteer-firefox/DeviceDescriptors.js new file mode 100644 index 0000000..05135a1 --- /dev/null +++ b/experimental/puppeteer-firefox/DeviceDescriptors.js @@ -0,0 +1,17 @@ +/** + * 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. + */ + +module.exports = require('./lib/DeviceDescriptors'); diff --git a/experimental/puppeteer-firefox/Errors.js b/experimental/puppeteer-firefox/Errors.js new file mode 100644 index 0000000..94d6d41 --- /dev/null +++ b/experimental/puppeteer-firefox/Errors.js @@ -0,0 +1 @@ +module.exports = require('./lib/Errors'); diff --git a/experimental/puppeteer-firefox/LICENSE b/experimental/puppeteer-firefox/LICENSE new file mode 100644 index 0000000..afdfe50 --- /dev/null +++ b/experimental/puppeteer-firefox/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/experimental/puppeteer-firefox/README.md b/experimental/puppeteer-firefox/README.md new file mode 100644 index 0000000..dc4c2a7 --- /dev/null +++ b/experimental/puppeteer-firefox/README.md @@ -0,0 +1,48 @@ + + +# Prototype: Puppeteer for Firefox + +**⚠️ The puppeteer-firefox package has been deprecated**: Firefox support is gradually transitioning to the puppeteer package. As of puppeteer v2.1.0 you can interact with Firefox Nightly. The puppeteer-firefox package will remain available until the transition is complete, but it is no longer actively maintained. For more information visit https://wiki.mozilla.org/Remote + +This project is an experimental feasibility prototype to guide the work of implementing Puppeteer endpoints into Firefox's code base. Mozilla's [bug 1545057](https://bugzilla.mozilla.org/show_bug.cgi?id=1545057) tracks the initial milestone, which will be based on a CDP-based [remote protocol](https://wiki.mozilla.org/Remote). + +## Getting Started + +### Installation + +To try out Puppeteer with Firefox in your project, run: + +```bash +npm i puppeteer-firefox +# or "yarn add puppeteer-firefox" +``` + +Note: When you install puppeteer-firefox, it downloads a [custom-built Firefox](https://github.com/puppeteer/juggler) (Firefox/63.0.4) that is guaranteed to work with the API. + +### Usage + +**Example** - navigating to https://example.com and saving a screenshot as `example.png`: + +Save file as **example.js** + +```js +const pptrFirefox = require('puppeteer-firefox'); + +(async () => { + const browser = await pptrFirefox.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({ path: 'example.png' }); + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node example.js +``` + +### Credits + +Special thanks to [Amine Bouhlali](https://bitbucket.org/aminerop/) who volunteered the [`puppeteer-firefox`](https://www.npmjs.com/package/puppeteer-firefox) NPM package. diff --git a/experimental/puppeteer-firefox/examples/screenshot.js b/experimental/puppeteer-firefox/examples/screenshot.js new file mode 100644 index 0000000..09d3ccf --- /dev/null +++ b/experimental/puppeteer-firefox/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-firefox'); + +(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/experimental/puppeteer-firefox/examples/search.js b/experimental/puppeteer-firefox/examples/search.js new file mode 100644 index 0000000..6f55e98 --- /dev/null +++ b/experimental/puppeteer-firefox/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-firefox'); + +(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-searchbox input', '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-results .gsc-thumbnail-inside a.gs-title'; + 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/experimental/puppeteer-firefox/index.js b/experimental/puppeteer-firefox/index.js new file mode 100644 index 0000000..08b9425 --- /dev/null +++ b/experimental/puppeteer-firefox/index.js @@ -0,0 +1,25 @@ +/** + * 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. + */ + +const {helper} = require('./lib/helper'); +const api = require('./lib/api'); +for (const className in api) + helper.installAsyncStackHooks(api[className]); + +const {Puppeteer} = require('./lib/Puppeteer'); +const packageJson = require('./package.json'); +const preferredRevision = packageJson.puppeteer.firefox_revision; +module.exports = new Puppeteer(__dirname, preferredRevision); diff --git a/experimental/puppeteer-firefox/install.js b/experimental/puppeteer-firefox/install.js new file mode 100644 index 0000000..a7ead10 --- /dev/null +++ b/experimental/puppeteer-firefox/install.js @@ -0,0 +1,97 @@ +/** + * 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. + */ + + +// puppeteer-core should not install anything. +if (require('./package.json').name === 'puppeteer-core') + return; + +const downloadHost = process.env.PUPPETEER_DOWNLOAD_HOST || process.env.npm_config_puppeteer_download_host || process.env.npm_package_config_puppeteer_download_host; +const downloadPath = process.env.PUPPETEER_DOWNLOAD_PATH || process.env.npm_config_puppeteer_download_path || process.env.npm_package_config_puppeteer_download_path; + +const puppeteer = require('./index'); +const browserFetcher = puppeteer.createBrowserFetcher({ host: downloadHost, product: 'firefox', path: downloadPath }); + +const revision = require('./package.json').puppeteer.firefox_revision; + +const revisionInfo = browserFetcher.revisionInfo(revision); + +// Do nothing if the revision is already downloaded. +if (revisionInfo.local) + return; + +// Override current environment proxy settings with npm configuration, if any. +const NPM_HTTPS_PROXY = process.env.npm_config_https_proxy || process.env.npm_config_proxy; +const NPM_HTTP_PROXY = process.env.npm_config_http_proxy || process.env.npm_config_proxy; +const NPM_NO_PROXY = process.env.npm_config_no_proxy; + +if (NPM_HTTPS_PROXY) + process.env.HTTPS_PROXY = NPM_HTTPS_PROXY; +if (NPM_HTTP_PROXY) + process.env.HTTP_PROXY = NPM_HTTP_PROXY; +if (NPM_NO_PROXY) + process.env.NO_PROXY = NPM_NO_PROXY; + +browserFetcher.download(revisionInfo.revision, onProgress) + .then(() => browserFetcher.localRevisions()) + .then(onSuccess) + .catch(onError); + +/** + * @param {!Array} + * @return {!Promise} + */ +function onSuccess(localRevisions) { + console.log('Firefox downloaded to ' + revisionInfo.folderPath); + localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision); + // Remove previous firefox revisions. + const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision)); + const installFirefoxPreferences = require('./misc/install-preferences'); + return Promise.all([...cleanupOldVersions, installFirefoxPreferences(revisionInfo.executablePath)]).then(() => { + console.log('Firefox preferences installed!'); + }); +} + +/** + * @param {!Error} error + */ +function onError(error) { + console.error(`ERROR: Failed to download Firefox r${revision}!`); + console.error(error); + process.exit(1); +} + +let progressBar = null; +let lastDownloadedBytes = 0; +function onProgress(downloadedBytes, totalBytes) { + if (!progressBar) { + const ProgressBar = require('progress'); + progressBar = new ProgressBar(`Downloading Firefox+Puppeteer ${revision.substring(0, 8)} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, { + complete: '|', + incomplete: ' ', + width: 20, + total: totalBytes, + }); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); +} + +function toMegabytes(bytes) { + const mb = bytes / 1024 / 1024; + return `${Math.round(mb * 10) / 10} Mb`; +} diff --git a/experimental/puppeteer-firefox/lib/Accessibility.js b/experimental/puppeteer-firefox/lib/Accessibility.js new file mode 100644 index 0000000..163576d --- /dev/null +++ b/experimental/puppeteer-firefox/lib/Accessibility.js @@ -0,0 +1,322 @@ +/** + * @typedef {Object} SerializedAXNode + * @property {string} role + * + * @property {string=} name + * @property {string|number=} value + * @property {string=} description + * + * @property {string=} keyshortcuts + * @property {string=} roledescription + * @property {string=} valuetext + * + * @property {boolean=} disabled + * @property {boolean=} expanded + * @property {boolean=} focused + * @property {boolean=} modal + * @property {boolean=} multiline + * @property {boolean=} multiselectable + * @property {boolean=} readonly + * @property {boolean=} required + * @property {boolean=} selected + * + * @property {boolean|"mixed"=} checked + * @property {boolean|"mixed"=} pressed + * + * @property {number=} level + * + * @property {string=} autocomplete + * @property {string=} haspopup + * @property {string=} invalid + * @property {string=} orientation + * + * @property {Array=} children + */ + +class Accessibility { + constructor(session) { + this._session = session; + } + + /** + * @param {{interestingOnly?: boolean}=} options + * @return {!Promise} + */ + async snapshot(options = {}) { + const {interestingOnly = true} = options; + const {tree} = await this._session.send('Accessibility.getFullAXTree'); + const root = new AXNode(tree); + if (!interestingOnly) + return serializeTree(root)[0]; + + /** @type {!Set} */ + const interestingNodes = new Set(); + collectInterestingNodes(interestingNodes, root, false); + return serializeTree(root, interestingNodes)[0]; + } +} + +/** + * @param {!Set} collection + * @param {!AXNode} node + * @param {boolean} insideControl + */ +function collectInterestingNodes(collection, node, insideControl) { + if (node.isInteresting(insideControl)) + collection.add(node); + if (node.isLeafNode()) + return; + insideControl = insideControl || node.isControl(); + for (const child of node._children) + collectInterestingNodes(collection, child, insideControl); +} + +/** + * @param {!AXNode} node + * @param {!Set=} whitelistedNodes + * @return {!Array} + */ +function serializeTree(node, whitelistedNodes) { + /** @type {!Array} */ + const children = []; + for (const child of node._children) + children.push(...serializeTree(child, whitelistedNodes)); + + if (whitelistedNodes && !whitelistedNodes.has(node)) + return children; + + const serializedNode = node.serialize(); + if (children.length) + serializedNode.children = children; + return [serializedNode]; +} + + +class AXNode { + constructor(payload) { + this._payload = payload; + + /** @type {!Array} */ + this._children = (payload.children || []).map(x => new AXNode(x)); + + this._editable = payload.editable; + this._richlyEditable = this._editable && (payload.tag !== 'textarea' && payload.tag !== 'input'); + this._focusable = payload.focusable; + this._expanded = payload.expanded; + this._name = this._payload.name; + this._role = this._payload.role; + this._cachedHasFocusableChild; + } + + /** + * @return {boolean} + */ + _isPlainTextField() { + if (this._richlyEditable) + return false; + if (this._editable) + return true; + return this._role === 'entry'; + } + + /** + * @return {boolean} + */ + _isTextOnlyObject() { + const role = this._role; + return (role === 'text leaf' || role === 'text' || role === 'statictext'); + } + + /** + * @return {boolean} + */ + _hasFocusableChild() { + if (this._cachedHasFocusableChild === undefined) { + this._cachedHasFocusableChild = false; + for (const child of this._children) { + if (child._focusable || child._hasFocusableChild()) { + this._cachedHasFocusableChild = true; + break; + } + } + } + return this._cachedHasFocusableChild; + } + + /** + * @return {boolean} + */ + isLeafNode() { + if (!this._children.length) + return true; + + // These types of objects may have children that we use as internal + // implementation details, but we want to expose them as leaves to platform + // accessibility APIs because screen readers might be confused if they find + // any children. + if (this._isPlainTextField() || this._isTextOnlyObject()) + return true; + + // Roles whose children are only presentational according to the ARIA and + // HTML5 Specs should be hidden from screen readers. + // (Note that whilst ARIA buttons can have only presentational children, HTML5 + // buttons are allowed to have content.) + switch (this._role) { + case 'graphic': + case 'scrollbar': + case 'slider': + case 'separator': + case 'progressbar': + return true; + default: + break; + } + + // Here and below: Android heuristics + if (this._hasFocusableChild()) + return false; + if (this._focusable && this._name) + return true; + if (this._role === 'heading' && this._name) + return true; + return false; + } + + /** + * @return {boolean} + */ + isControl() { + switch (this._role) { + case 'checkbutton': + case 'check menu item': + case 'check rich option': + case 'combobox': + case 'combobox option': + case 'color chooser': + case 'listbox': + case 'listbox option': + case 'listbox rich option': + case 'popup menu': + case 'menupopup': + case 'menuitem': + case 'menubar': + case 'button': + case 'pushbutton': + case 'radiobutton': + case 'radio menuitem': + case 'scrollbar': + case 'slider': + case 'spinbutton': + case 'switch': + case 'pagetab': + case 'entry': + case 'tree table': + return true; + default: + return false; + } + } + + /** + * @param {boolean} insideControl + * @return {boolean} + */ + isInteresting(insideControl) { + if (this._focusable || this._richlyEditable) + return true; + + // If it's not focusable but has a control role, then it's interesting. + if (this.isControl()) + return true; + + // A non focusable child of a control is not interesting + if (insideControl) + return false; + + return this.isLeafNode() && !!this._name.trim(); + } + + /** + * @return {!SerializedAXNode} + */ + serialize() { + /** @type {SerializedAXNode} */ + const node = { + role: this._role + }; + + /** @type {!Array} */ + const userStringProperties = [ + 'name', + 'value', + 'description', + 'roledescription', + 'valuetext', + 'keyshortcuts', + ]; + for (const userStringProperty of userStringProperties) { + if (!(userStringProperty in this._payload)) + continue; + node[userStringProperty] = this._payload[userStringProperty]; + } + /** @type {!Array} */ + const booleanProperties = [ + 'disabled', + 'expanded', + 'focused', + 'modal', + 'multiline', + 'multiselectable', + 'readonly', + 'required', + 'selected', + ]; + for (const booleanProperty of booleanProperties) { + if (this._role === 'document' && booleanProperty === 'focused') + continue; // document focusing is strange + const value = this._payload[booleanProperty]; + if (!value) + continue; + node[booleanProperty] = value; + } + + /** @type {!Array} */ + const tristateProperties = [ + 'checked', + 'pressed', + ]; + for (const tristateProperty of tristateProperties) { + if (!(tristateProperty in this._payload)) + continue; + const value = this._payload[tristateProperty]; + node[tristateProperty] = value; + } + /** @type {!Array} */ + const numericalProperties = [ + 'level', + 'valuemax', + 'valuemin', + ]; + for (const numericalProperty of numericalProperties) { + if (!(numericalProperty in this._payload)) + continue; + node[numericalProperty] = this._payload[numericalProperty]; + } + /** @type {!Array} */ + const tokenProperties = [ + 'autocomplete', + 'haspopup', + 'invalid', + 'orientation', + ]; + for (const tokenProperty of tokenProperties) { + const value = this._payload[tokenProperty]; + if (!value || value === 'false') + continue; + node[tokenProperty] = value; + } + return node; + } +} + +module.exports = {Accessibility}; diff --git a/experimental/puppeteer-firefox/lib/Browser.js b/experimental/puppeteer-firefox/lib/Browser.js new file mode 100644 index 0000000..5d2169f --- /dev/null +++ b/experimental/puppeteer-firefox/lib/Browser.js @@ -0,0 +1,369 @@ +const {helper, assert} = require('./helper'); +const {Page} = require('./Page'); +const {Events} = require('./Events'); +const EventEmitter = require('events'); + +class Browser extends EventEmitter { + /** + * @param {!Puppeteer.Connection} connection + * @param {?Puppeteer.Viewport} defaultViewport + * @param {?Puppeteer.ChildProcess} process + * @param {function():void} closeCallback + */ + static async create(connection, defaultViewport, process, closeCallback) { + const {browserContextIds} = await connection.send('Target.getBrowserContexts'); + const browser = new Browser(connection, browserContextIds, defaultViewport, process, closeCallback); + await connection.send('Target.enable'); + return browser; + } + + /** + * @param {!Puppeteer.Connection} connection + * @param {!Array} browserContextIds + * @param {?Puppeteer.Viewport} defaultViewport + * @param {?Puppeteer.ChildProcess} process + * @param {function():void} closeCallback + */ + constructor(connection, browserContextIds, defaultViewport, process, closeCallback) { + super(); + this._connection = connection; + this._defaultViewport = defaultViewport; + this._process = process; + this._closeCallback = closeCallback; + + /** @type {!Map} */ + this._targets = new Map(); + + this._defaultContext = new BrowserContext(this._connection, this, null); + /** @type {!Map} */ + this._contexts = new Map(); + for (const browserContextId of browserContextIds) + this._contexts.set(browserContextId, new BrowserContext(this._connection, this, browserContextId)); + + this._connection.on(Events.Connection.Disconnected, () => this.emit(Events.Browser.Disconnected)); + + this._eventListeners = [ + helper.addEventListener(this._connection, 'Target.targetCreated', this._onTargetCreated.bind(this)), + helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), + helper.addEventListener(this._connection, 'Target.targetInfoChanged', this._onTargetInfoChanged.bind(this)), + ]; + } + + wsEndpoint() { + return this._connection.url(); + } + + disconnect() { + this._connection.dispose(); + } + + /** + * @return {boolean} + */ + isConnected() { + return !this._connection._closed; + } + + /** + * @return {!BrowserContext} + */ + async createIncognitoBrowserContext() { + const {browserContextId} = await this._connection.send('Target.createBrowserContext'); + const context = new BrowserContext(this._connection, this, browserContextId); + this._contexts.set(browserContextId, context); + return context; + } + + /** + * @return {!Array} + */ + browserContexts() { + return [this._defaultContext, ...Array.from(this._contexts.values())]; + } + + defaultBrowserContext() { + return this._defaultContext; + } + + async _disposeContext(browserContextId) { + await this._connection.send('Target.removeBrowserContext', {browserContextId}); + this._contexts.delete(browserContextId); + } + + /** + * @return {!Promise} + */ + async userAgent() { + const info = await this._connection.send('Browser.getInfo'); + return info.userAgent; + } + + /** + * @return {!Promise} + */ + async version() { + const info = await this._connection.send('Browser.getInfo'); + return info.version; + } + + /** + * @return {?Puppeteer.ChildProcess} + */ + process() { + return this._process; + } + + /** + * @param {function(!Target):boolean} predicate + * @param {{timeout?: number}=} options + * @return {!Promise} + */ + async waitForTarget(predicate, options = {}) { + const { + timeout = 30000 + } = options; + const existingTarget = this.targets().find(predicate); + if (existingTarget) + return existingTarget; + let resolve; + const targetPromise = new Promise(x => resolve = x); + this.on(Events.Browser.TargetCreated, check); + this.on('targetchanged', check); + try { + if (!timeout) + return await targetPromise; + return await helper.waitWithTimeout(targetPromise, 'target', timeout); + } finally { + this.removeListener(Events.Browser.TargetCreated, check); + this.removeListener('targetchanged', check); + } + + /** + * @param {!Target} target + */ + function check(target) { + if (predicate(target)) + resolve(target); + } + } + + /** + * @return {Promise} + */ + newPage() { + return this._createPageInContext(this._defaultContext._browserContextId); + } + + /** + * @param {?string} browserContextId + * @return {Promise} + */ + async _createPageInContext(browserContextId) { + const {targetId} = await this._connection.send('Target.newPage', { + browserContextId: browserContextId || undefined + }); + const target = this._targets.get(targetId); + return await target.page(); + } + + async pages() { + const pageTargets = Array.from(this._targets.values()).filter(target => target.type() === 'page'); + return await Promise.all(pageTargets.map(target => target.page())); + } + + targets() { + return Array.from(this._targets.values()); + } + + target() { + return this.targets().find(target => target.type() === 'browser'); + } + + async _onTargetCreated({targetId, url, browserContextId, openerId, type}) { + const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext; + const target = new Target(this._connection, this, context, targetId, type, url, openerId); + this._targets.set(targetId, target); + if (target.opener() && target.opener()._pagePromise) { + const openerPage = await target.opener()._pagePromise; + if (openerPage.listenerCount(Events.Page.Popup)) { + const popupPage = await target.page(); + openerPage.emit(Events.Page.Popup, popupPage); + } + } + this.emit(Events.Browser.TargetCreated, target); + context.emit(Events.BrowserContext.TargetCreated, target); + } + + _onTargetDestroyed({targetId}) { + const target = this._targets.get(targetId); + this._targets.delete(targetId); + target._closedCallback(); + this.emit(Events.Browser.TargetDestroyed, target); + target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target); + } + + _onTargetInfoChanged({targetId, url}) { + const target = this._targets.get(targetId); + target._url = url; + this.emit(Events.Browser.TargetChanged, target); + target.browserContext().emit(Events.BrowserContext.TargetChanged, target); + } + + async close() { + helper.removeEventListeners(this._eventListeners); + await this._closeCallback(); + } +} + +class Target { + /** + * + * @param {*} connection + * @param {!Browser} browser + * @param {!BrowserContext} context + * @param {string} targetId + * @param {string} type + * @param {string} url + * @param {string=} openerId + */ + constructor(connection, browser, context, targetId, type, url, openerId) { + this._browser = browser; + this._context = context; + this._connection = connection; + this._targetId = targetId; + this._type = type; + /** @type {?Promise} */ + this._pagePromise = null; + this._url = url; + this._openerId = openerId; + this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill); + } + + /** + * @return {?Target} + */ + opener() { + return this._openerId ? this._browser._targets.get(this._openerId) : null; + } + + /** + * @return {"page"|"browser"} + */ + type() { + return this._type; + } + + url() { + return this._url; + } + + /** + * @return {!BrowserContext} + */ + browserContext() { + return this._context; + } + + async page() { + if (this._type === 'page' && !this._pagePromise) { + const session = await this._connection.createSession(this._targetId); + this._pagePromise = Page.create(session, this, this._browser._defaultViewport); + } + return this._pagePromise; + } + + browser() { + return this._browser; + } +} + +class BrowserContext extends EventEmitter { + /** + * @param {!Puppeteer.Connection} connection + * @param {!Browser} browser + * @param {?string} browserContextId + */ + constructor(connection, browser, browserContextId) { + super(); + this._connection = connection; + this._browser = browser; + this._browserContextId = browserContextId; + } + + /** + * @param {string} origin + * @param {!Array} permissions + */ + async overridePermissions(origin, permissions) { + const webPermissionToProtocol = new Map([ + ['geolocation', 'geo'], + ['microphone', 'microphone'], + ['camera', 'camera'], + ['notifications', 'desktop-notifications'], + ]); + permissions = permissions.map(permission => { + const protocolPermission = webPermissionToProtocol.get(permission); + if (!protocolPermission) + throw new Error('Unknown permission: ' + permission); + return protocolPermission; + }); + await this._connection.send('Browser.grantPermissions', {origin, browserContextId: this._browserContextId || undefined, permissions}); + } + + async clearPermissionOverrides() { + await this._connection.send('Browser.resetPermissions', {browserContextId: this._browserContextId || undefined}); + } + + /** + * @return {Array} + */ + targets() { + return this._browser.targets().filter(target => target.browserContext() === this); + } + + /** + * @return {Promise>} + */ + async pages() { + const pages = await Promise.all( + this.targets() + .filter(target => target.type() === 'page') + .map(target => target.page()) + ); + return pages.filter(page => !!page); + } + + /** + * @param {function(Target):boolean} predicate + * @param {{timeout?: number}=} options + * @return {!Promise} + */ + waitForTarget(predicate, options) { + return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options); + } + + /** + * @return {boolean} + */ + isIncognito() { + return !!this._browserContextId; + } + + newPage() { + return this._browser._createPageInContext(this._browserContextId); + } + + /** + * @return {!Browser} + */ + browser() { + return this._browser; + } + + async close() { + assert(this._browserContextId, 'Non-incognito contexts cannot be closed!'); + await this._browser._disposeContext(this._browserContextId); + } +} + +module.exports = {Browser, BrowserContext, Target}; diff --git a/experimental/puppeteer-firefox/lib/BrowserFetcher.js b/experimental/puppeteer-firefox/lib/BrowserFetcher.js new file mode 100644 index 0000000..3371f7e --- /dev/null +++ b/experimental/puppeteer-firefox/lib/BrowserFetcher.js @@ -0,0 +1,342 @@ +/** + * 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. + */ + +const os = require('os'); +const fs = require('fs'); +const path = require('path'); +const extract = require('extract-zip'); +const util = require('util'); +const URL = require('url'); +const {helper, assert} = require('./helper'); +const removeRecursive = require('rimraf'); +// @ts-ignore +const ProxyAgent = require('https-proxy-agent'); +// @ts-ignore +const getProxyForUrl = require('proxy-from-env').getProxyForUrl; + +const downloadURLs = { + chromium: { + host: 'https://storage.googleapis.com', + linux: '%s/chromium-browser-snapshots/Linux_x64/%s/%s.zip', + mac: '%s/chromium-browser-snapshots/Mac/%s/%s.zip', + win32: '%s/chromium-browser-snapshots/Win/%s/%s.zip', + win64: '%s/chromium-browser-snapshots/Win_x64/%s/%s.zip', + }, + firefox: { + host: 'https://github.com/puppeteer/juggler/releases', + linux: '%s/download/%s/%s.zip', + mac: '%s/download/%s/%s.zip', + win32: '%s/download/%s/%s.zip', + win64: '%s/download/%s/%s.zip', + }, +}; + +/** + * @param {string} product + * @param {string} platform + * @param {string} revision + * @return {string} + */ +function archiveName(product, platform, revision) { + if (product === 'chromium') { + if (platform === 'linux') + return 'chrome-linux'; + if (platform === 'mac') + return 'chrome-mac'; + if (platform === 'win32' || platform === 'win64') { + // Windows archive name changed at r591479. + return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; + } + } else if (product === 'firefox') { + if (platform === 'linux') + return 'firefox-linux'; + if (platform === 'mac') + return 'firefox-mac'; + if (platform === 'win32' || platform === 'win64') + return 'firefox-' + platform; + } + return null; +} + +/** + * @param {string} product + * @param {string} platform + * @param {string} host + * @param {string} revision + * @return {string} + */ +function downloadURL(product, platform, host, revision) { + const url = util.format(downloadURLs[product][platform], host, revision, archiveName(product, platform, revision)); + return url; +} + +const readdirAsync = helper.promisify(fs.readdir.bind(fs)); +const mkdirAsync = helper.promisify(fs.mkdir.bind(fs)); +const unlinkAsync = helper.promisify(fs.unlink.bind(fs)); +const chmodAsync = helper.promisify(fs.chmod.bind(fs)); + +function existsAsync(filePath) { + let fulfill = null; + const promise = new Promise(x => fulfill = x); + fs.access(filePath, err => fulfill(!err)); + return promise; +} + +class BrowserFetcher { + /** + * @param {string} projectRoot + * @param {!BrowserFetcher.Options=} options + */ + constructor(projectRoot, options = {}) { + this._product = (options.product || 'chromium').toLowerCase(); + assert(this._product === 'chromium' || this._product === 'firefox', `Unkown product: "${options.product}"`); + this._downloadsFolder = options.path || path.join(projectRoot, '.local-browser'); + this._downloadHost = options.host || downloadURLs[this._product].host; + this._platform = options.platform || ''; + if (!this._platform) { + const platform = os.platform(); + if (platform === 'darwin') + this._platform = 'mac'; + else if (platform === 'linux') + this._platform = 'linux'; + else if (platform === 'win32') + this._platform = os.arch() === 'x64' ? 'win64' : 'win32'; + assert(this._platform, 'Unsupported platform: ' + os.platform()); + } + assert(downloadURLs[this._product][this._platform], 'Unsupported platform: ' + this._platform); + } + + /** + * @return {string} + */ + platform() { + return this._platform; + } + + /** + * @param {string} revision + * @return {!Promise} + */ + canDownload(revision) { + const url = downloadURL(this._product, this._platform, this._downloadHost, revision); + let resolve; + const promise = new Promise(x => resolve = x); + const request = httpRequest(url, 'HEAD', response => { + resolve(response.statusCode === 200); + }); + request.on('error', error => { + console.error(error); + resolve(false); + }); + return promise; + } + + /** + * @param {string} revision + * @param {?function(number, number)} progressCallback + * @return {!Promise} + */ + async download(revision, progressCallback) { + const url = downloadURL(this._product, this._platform, this._downloadHost, revision); + const zipPath = path.join(this._downloadsFolder, `download-${this._product}-${this._platform}-${revision}.zip`); + const folderPath = this._getFolderPath(revision); + if (await existsAsync(folderPath)) + return this.revisionInfo(revision); + if (!(await existsAsync(this._downloadsFolder))) + await mkdirAsync(this._downloadsFolder); + try { + await downloadFile(url, zipPath, progressCallback); + await extractZip(zipPath, folderPath); + } finally { + if (await existsAsync(zipPath)) + await unlinkAsync(zipPath); + } + const revisionInfo = this.revisionInfo(revision); + if (revisionInfo) + await chmodAsync(revisionInfo.executablePath, 0o755); + return revisionInfo; + } + + /** + * @return {!Promise>} + */ + async localRevisions() { + if (!await existsAsync(this._downloadsFolder)) + return []; + const fileNames = await readdirAsync(this._downloadsFolder); + return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision); + } + + /** + * @param {string} revision + */ + async remove(revision) { + const folderPath = this._getFolderPath(revision); + assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`); + await new Promise(fulfill => removeRecursive(folderPath, fulfill)); + } + + /** + * @param {string} revision + * @return {!BrowserFetcher.RevisionInfo} + */ + revisionInfo(revision) { + const folderPath = this._getFolderPath(revision); + let executablePath = ''; + if (this._product === 'chromium') { + if (this._platform === 'mac') + executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); + else if (this._platform === 'linux') + executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome'); + else if (this._platform === 'win32' || this._platform === 'win64') + executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome.exe'); + else + throw new Error('Unsupported platform: ' + this._platform); + } else if (this._product === 'firefox') { + if (this._platform === 'mac') + executablePath = path.join(folderPath, 'firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'); + else if (this._platform === 'linux') + executablePath = path.join(folderPath, 'firefox', 'firefox'); + else if (this._platform === 'win32' || this._platform === 'win64') + executablePath = path.join(folderPath, 'firefox', 'firefox.exe'); + else + throw new Error('Unsupported platform: ' + this._platform); + } + const url = downloadURL(this._product, this._platform, this._downloadHost, revision); + const local = fs.existsSync(folderPath); + return {revision, executablePath, folderPath, local, url}; + } + + /** + * @param {string} revision + * @return {string} + */ + _getFolderPath(revision) { + return path.join(this._downloadsFolder, this._product + '-' + this._platform + '-' + revision); + } +} + +module.exports = {BrowserFetcher}; + +/** + * @param {string} folderPath + * @return {?{platform: string, revision: string}} + */ +function parseFolderPath(folderPath) { + const name = path.basename(folderPath); + const splits = name.split('-'); + if (splits.length !== 3) + return null; + const [product, platform, revision] = splits; + if (!downloadURLs[product][platform]) + return null; + return {platform, revision}; +} + +/** + * @param {string} url + * @param {string} destinationPath + * @param {?function(number, number)} progressCallback + * @return {!Promise} + */ +function downloadFile(url, destinationPath, progressCallback) { + let fulfill, reject; + let downloadedBytes = 0; + let totalBytes = 0; + + const promise = new Promise((x, y) => { fulfill = x; reject = y; }); + + 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 = fs.createWriteStream(destinationPath); + file.on('finish', () => fulfill()); + file.on('error', error => reject(error)); + response.pipe(file); + totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10); + if (progressCallback) + response.on('data', onData); + }); + request.on('error', error => reject(error)); + return promise; + + function onData(chunk) { + downloadedBytes += chunk.length; + progressCallback(downloadedBytes, totalBytes); + } +} + +/** + * @param {string} zipPath + * @param {string} folderPath + * @return {!Promise} + */ +async function extractZip(zipPath, folderPath) { + try { + await extract(zipPath, {dir: folderPath}); + } catch (error) { + return error; + } +} + +function httpRequest(url, method, response) { + /** @type {Object} */ + const options = URL.parse(url); + options.method = method; + + const proxyURL = getProxyForUrl(url); + if (proxyURL) { + /** @type {Object} */ + const parsedProxyURL = URL.parse(proxyURL); + parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:'; + + options.agent = new ProxyAgent(parsedProxyURL); + options.rejectUnauthorized = false; + } + + const requestCallback = res => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) + httpRequest(res.headers.location, method, response); + else + response(res); + }; + const request = options.protocol === 'https:' ? + require('https').request(options, requestCallback) : + require('http').request(options, requestCallback); + request.end(); + return request; +} + +/** + * @typedef {Object} BrowserFetcher.Options + * @property {string=} platform + * @property {string=} path + * @property {string=} host + */ + +/** + * @typedef {Object} BrowserFetcher.RevisionInfo + * @property {string} folderPath + * @property {string} executablePath + * @property {string} url + * @property {boolean} local + * @property {string} revision + */ diff --git a/experimental/puppeteer-firefox/lib/Connection.js b/experimental/puppeteer-firefox/lib/Connection.js new file mode 100644 index 0000000..d058575 --- /dev/null +++ b/experimental/puppeteer-firefox/lib/Connection.js @@ -0,0 +1,242 @@ +/** + * 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. + */ +const {assert} = require('./helper'); +const {Events} = require('./Events'); +const debugProtocol = require('debug')('puppeteer:protocol'); +const EventEmitter = require('events'); + +class Connection extends EventEmitter { + /** + * @param {string} url + * @param {!Puppeteer.ConnectionTransport} transport + * @param {number=} delay + */ + constructor(url, transport, delay = 0) { + super(); + this._url = url; + this._lastId = 0; + /** @type {!Map}*/ + this._callbacks = new Map(); + this._delay = delay; + + this._transport = transport; + this._transport.onmessage = this._onMessage.bind(this); + this._transport.onclose = this._onClose.bind(this); + /** @type {!Map}*/ + this._sessions = new Map(); + this._closed = false; + } + + /** + * @param {!JugglerSession} session + * @return {!Connection} + */ + static fromSession(session) { + return session._connection; + } + + /** + * @param {string} sessionId + * @return {?JugglerSession} + */ + session(sessionId) { + return this._sessions.get(sessionId) || null; + } + + /** + * @return {string} + */ + url() { + return this._url; + } + + /** + * @param {string} method + * @param {!Object=} params + * @return {!Promise} + */ + send(method, params = {}) { + const id = this._rawSend({method, params}); + return new Promise((resolve, reject) => { + this._callbacks.set(id, {resolve, reject, error: new Error(), method}); + }); + } + + /** + * @param {*} message + * @return {number} + */ + _rawSend(message) { + const id = ++this._lastId; + message = JSON.stringify(Object.assign({}, message, {id})); + debugProtocol('SEND ► ' + message); + this._transport.send(message); + return id; + } + + /** + * @param {string} message + */ + async _onMessage(message) { + if (this._delay) + await new Promise(f => setTimeout(f, this._delay)); + debugProtocol('◀ RECV ' + message); + const object = JSON.parse(message); + if (object.method === 'Target.attachedToTarget') { + const sessionId = object.params.sessionId; + const session = new JugglerSession(this, object.params.targetInfo.type, sessionId); + this._sessions.set(sessionId, session); + } else if (object.method === 'Browser.detachedFromTarget') { + const session = this._sessions.get(object.params.sessionId); + if (session) { + session._onClosed(); + this._sessions.delete(object.params.sessionId); + } + } + if (object.sessionId) { + const session = this._sessions.get(object.sessionId); + if (session) + session._onMessage(object); + } else if (object.id) { + const callback = this._callbacks.get(object.id); + // Callbacks could be all rejected if someone has called `.dispose()`. + if (callback) { + this._callbacks.delete(object.id); + if (object.error) + callback.reject(createProtocolError(callback.error, callback.method, object)); + else + callback.resolve(object.result); + } + } else { + this.emit(object.method, object.params); + } + } + + _onClose() { + if (this._closed) + return; + this._closed = true; + this._transport.onmessage = null; + this._transport.onclose = null; + for (const callback of this._callbacks.values()) + callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); + this._callbacks.clear(); + for (const session of this._sessions.values()) + session._onClosed(); + this._sessions.clear(); + this.emit(Events.Connection.Disconnected); + } + + dispose() { + this._onClose(); + this._transport.close(); + } + + /** + * @param {string} targetId + * @return {!Promise} + */ + async createSession(targetId) { + const {sessionId} = await this.send('Target.attachToTarget', {targetId}); + return this._sessions.get(sessionId); + } +} + +class JugglerSession extends EventEmitter { + /** + * @param {!Connection} connection + * @param {string} targetType + * @param {string} sessionId + */ + constructor(connection, targetType, sessionId) { + super(); + /** @type {!Map}*/ + this._callbacks = new Map(); + this._connection = connection; + this._targetType = targetType; + this._sessionId = sessionId; + } + + /** + * @param {string} method + * @param {!Object=} params + * @return {!Promise} + */ + send(method, params = {}) { + if (!this._connection) + return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`)); + const id = this._connection._rawSend({sessionId: this._sessionId, method, params}); + return new Promise((resolve, reject) => { + this._callbacks.set(id, {resolve, reject, error: new Error(), method}); + }); + } + + /** + * @param {{id?: number, method: string, params: Object, error: {message: string, data: any}, result?: *}} object + */ + _onMessage(object) { + if (object.id && this._callbacks.has(object.id)) { + const callback = this._callbacks.get(object.id); + this._callbacks.delete(object.id); + if (object.error) + callback.reject(createProtocolError(callback.error, callback.method, object)); + else + callback.resolve(object.result); + } else { + assert(!object.id); + this.emit(object.method, object.params); + } + } + + async detach() { + if (!this._connection) + throw new Error(`Session already detached. Most likely the ${this._targetType} has been closed.`); + await this._connection.send('Target.detachFromTarget', {sessionId: this._sessionId}); + } + + _onClosed() { + for (const callback of this._callbacks.values()) + callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); + this._callbacks.clear(); + this._connection = null; + this.emit(Events.JugglerSession.Disconnected); + } +} + +/** + * @param {!Error} error + * @param {string} method + * @param {{error: {message: string, data: any}}} object + * @return {!Error} + */ +function createProtocolError(error, method, object) { + let message = `Protocol error (${method}): ${object.error.message}`; + if ('data' in object.error) + message += ` ${object.error.data}`; + return rewriteError(error, message); +} + +/** + * @param {!Error} error + * @param {string} message + * @return {!Error} + */ +function rewriteError(error, message) { + error.message = message; + return error; +} + +module.exports = {Connection, JugglerSession}; diff --git a/experimental/puppeteer-firefox/lib/DOMWorld.js b/experimental/puppeteer-firefox/lib/DOMWorld.js new file mode 100644 index 0000000..096c06f --- /dev/null +++ b/experimental/puppeteer-firefox/lib/DOMWorld.js @@ -0,0 +1,625 @@ +const {helper, assert} = require('./helper'); +const {TimeoutError} = require('./Errors'); +const fs = require('fs'); +const util = require('util'); +const readFileAsync = util.promisify(fs.readFile); + +class DOMWorld { + constructor(frame, timeoutSettings) { + this._frame = frame; + this._timeoutSettings = timeoutSettings; + + this._documentPromise = null; + this._contextPromise; + this._contextResolveCallback = null; + this._setContext(null); + + /** @type {!Set} */ + this._waitTasks = new Set(); + this._detached = false; + } + + frame() { + return this._frame; + } + + _setContext(context) { + if (context) { + this._contextResolveCallback.call(null, context); + this._contextResolveCallback = null; + for (const waitTask of this._waitTasks) + waitTask.rerun(); + } else { + this._documentPromise = null; + this._contextPromise = new Promise(fulfill => { + this._contextResolveCallback = fulfill; + }); + } + } + + _detach() { + this._detached = true; + for (const waitTask of this._waitTasks) + waitTask.terminate(new Error('waitForFunction failed: frame got detached.')); + } + + async executionContext() { + if (this._detached) + throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); + return this._contextPromise; + } + + async evaluateHandle(pageFunction, ...args) { + const context = await this.executionContext(); + return context.evaluateHandle(pageFunction, ...args); + } + + async evaluate(pageFunction, ...args) { + const context = await this.executionContext(); + return context.evaluate(pageFunction, ...args); + } + + /** + * @param {string} selector + * @return {!Promise} + */ + async $(selector) { + const document = await this._document(); + return document.$(selector); + } + + _document() { + if (!this._documentPromise) + this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()); + return this._documentPromise; + } + + /** + * @param {string} expression + * @return {!Promise>} + */ + async $x(expression) { + const document = await this._document(); + return document.$x(expression); + } + + /** + * @param {string} selector + * @param {Function|String} pageFunction + * @param {!Array<*>} args + * @return {!Promise<(!Object|undefined)>} + */ + async $eval(selector, pageFunction, ...args) { + const document = await this._document(); + return document.$eval(selector, pageFunction, ...args); + } + + /** + * @param {string} selector + * @param {Function|String} pageFunction + * @param {!Array<*>} args + * @return {!Promise<(!Object|undefined)>} + */ + async $$eval(selector, pageFunction, ...args) { + const document = await this._document(); + return document.$$eval(selector, pageFunction, ...args); + } + + /** + * @param {string} selector + * @return {!Promise>} + */ + async $$(selector) { + const document = await this._document(); + return document.$$(selector); + } + + /** + * @return {!Promise} + */ + async content() { + return await this.evaluate(() => { + let retVal = ''; + if (document.doctype) + retVal = new XMLSerializer().serializeToString(document.doctype); + if (document.documentElement) + retVal += document.documentElement.outerHTML; + return retVal; + }); + } + + /** + * @param {string} html + */ + async setContent(html) { + await this.evaluate(html => { + document.open(); + document.write(html); + document.close(); + }, html); + } + + /** + * @param {!{content?: string, path?: string, type?: string, url?: string}} options + * @return {!Promise} + */ + async addScriptTag(options) { + if (typeof options.url === 'string') { + const url = options.url; + try { + return (await this.evaluateHandle(addScriptUrl, url, options.type)).asElement(); + } catch (error) { + throw new Error(`Loading script from ${url} failed`); + } + } + + if (typeof options.path === 'string') { + let contents = await readFileAsync(options.path, 'utf8'); + contents += '//# sourceURL=' + options.path.replace(/\n/g, ''); + return (await this.evaluateHandle(addScriptContent, contents, options.type)).asElement(); + } + + if (typeof options.content === 'string') { + return (await this.evaluateHandle(addScriptContent, options.content, options.type)).asElement(); + } + + throw new Error('Provide an object with a `url`, `path` or `content` property'); + + /** + * @param {string} url + * @param {string} type + * @return {!Promise} + */ + async function addScriptUrl(url, type) { + const script = document.createElement('script'); + script.src = url; + if (type) + script.type = type; + const promise = new Promise((res, rej) => { + script.onload = res; + script.onerror = rej; + }); + document.head.appendChild(script); + await promise; + return script; + } + + /** + * @param {string} content + * @param {string} type + * @return {!HTMLElement} + */ + function addScriptContent(content, type = 'text/javascript') { + const script = document.createElement('script'); + script.type = type; + script.text = content; + let error = null; + script.onerror = e => error = e; + document.head.appendChild(script); + if (error) + throw error; + return script; + } + } + + /** + * @param {!{content?: string, path?: string, url?: string}} options + * @return {!Promise} + */ + async addStyleTag(options) { + if (typeof options.url === 'string') { + const url = options.url; + try { + return (await this.evaluateHandle(addStyleUrl, url)).asElement(); + } catch (error) { + throw new Error(`Loading style from ${url} failed`); + } + } + + if (typeof options.path === 'string') { + let contents = await readFileAsync(options.path, 'utf8'); + contents += '/*# sourceURL=' + options.path.replace(/\n/g, '') + '*/'; + return (await this.evaluateHandle(addStyleContent, contents)).asElement(); + } + + if (typeof options.content === 'string') { + return (await this.evaluateHandle(addStyleContent, options.content)).asElement(); + } + + throw new Error('Provide an object with a `url`, `path` or `content` property'); + + /** + * @param {string} url + * @return {!Promise} + */ + async function addStyleUrl(url) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + const promise = new Promise((res, rej) => { + link.onload = res; + link.onerror = rej; + }); + document.head.appendChild(link); + await promise; + return link; + } + + /** + * @param {string} content + * @return {!Promise} + */ + async function addStyleContent(content) { + const style = document.createElement('style'); + style.type = 'text/css'; + style.appendChild(document.createTextNode(content)); + const promise = new Promise((res, rej) => { + style.onload = res; + style.onerror = rej; + }); + document.head.appendChild(style); + await promise; + return style; + } + } + + /** + * @param {string} selector + * @param {!{delay?: number, button?: string, clickCount?: number}=} options + */ + async click(selector, options = {}) { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.click(options); + await handle.dispose(); + } + + /** + * @param {string} selector + */ + async focus(selector) { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.focus(); + await handle.dispose(); + } + + /** + * @param {string} selector + */ + async hover(selector) { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.hover(); + await handle.dispose(); + } + + /** + * @param {string} selector + * @param {!Array} values + * @return {!Promise>} + */ + select(selector, ...values) { + for (const value of values) + assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"'); + return this.$eval(selector, (element, values) => { + if (element.nodeName.toLowerCase() !== 'select') + throw new Error('Element is not a - - - - `; - this._contentElement = this.element.$('search-results'); - - this._items = []; - this._visible = false; - - this._defaultValue = ''; - - this._gotoHomeItem = html`Navigate Home`; - this._showOtherItem = html``; - - this._selectedElement = null; - - this.input = this.element.$('input'); - this.input.addEventListener('keydown', event => { - if (event.key === 'Escape' || event.keyCode === 27) { - event.preventDefault(); - event.stopPropagation(); - this.cancelSearch(); - } else if (event.key === 'ArrowDown') { - this._selectNext(event); - } else if (event.key === 'ArrowUp') { - this._selectPrevious(event); - } else if (event.key === 'Enter') { - event.preventDefault(); - event.stopPropagation(); - if (this._selectedElement); - this._selectedElement.click(); - } - }, false); - this.input.addEventListener('input', () => { - this.search(this.input.value); - }, false); - this.input.addEventListener('focus', () => { - this._defaultValue = this.input.value; - }, false); - - document.addEventListener('keydown', event => { - if (this.input === document.activeElement) - return; - if (event.keyCode === 8 || event.keyCode === 46) { - // Activate search on backspace - this.input.focus(); - } else if (/^\S$/.test(event.key) && !event.metaKey && !event.ctrlKey && !event.altKey) { - // Activate search on any keypress - this.input.focus(); - if (event.key !== '.') - this.input.value = ''; - } - }, false); - // Activate on paste - document.addEventListener('paste', event => { - if (this.input === document.activeElement) - return; - this.input.focus(); - }, false); - - document.addEventListener('click', event => { - if (!this._visible) - return; - if (this.input.contains(event.target)) - return; - let item = event.target; - while (item && item.parentElement !== this._contentElement) - item = item.parentElement; - if (!item) { - this.cancelSearch(); - return; - } - if (item === this._gotoHomeItem) { - event.preventDefault(); - this.cancelSearch(); - app.navigateHome(); - } else if (item === this._showOtherItem) { - // Render the rest. - for (const result of this._remainingResults) { - const element = this._renderResult(result); - this._contentElement.appendChild(element); - } - this._selectElement(this._showOtherItem.nextSibling); - this._showOtherItem.remove(); - this.input.focus(); - event.preventDefault(); - } else { - event.preventDefault(); - this.cancelSearch(); - app.navigateURL(item[SearchComponent._symbol].url()); - } - }, false); - } - - toggleSearch() { - if (this._visible) - this.cancelSearch(); - else - this.search(this._defaultValue); - } - - setItems(items) { - this._items = items; - } - - setInputValue(value) { - this.input.value = value; - - // Focus the input so that we can control its selection. - this.input.focus(); - this.input.selectionStart = value.length; - this.input.selectionEnd = value.length; - this._defaultValue = value; - } - - search(query) { - this._setVisible(true); - const results = [] - this._remainingResults = []; - - if (query) { - const fuzzySearch = new FuzzySearch(query); - for (const item of this._items) { - let matches = []; - let score = fuzzySearch.score(item.text(), matches); - if (score !== 0) { - results.push({item, score, matches}); - } - } - if (results.length === 0) { - this._contentElement.innerHTML = `No Results`; - return; - } - results.sort((a, b) => { - const scoreDiff = b.score - a.score; - if (scoreDiff) - return scoreDiff; - // Prefer left-most search results. - const startDiff = a.matches[0] - b.matches[0]; - if (startDiff) - return startDiff; - return a.item.text().length - b.item.text().length; - }); - } else { - for (const item of this._items) - results.push({item, score: 0, matches: []}); - } - this._contentElement.innerHTML = ''; - this._contentElement.scrollTop = 0; - if (!query) - this._contentElement.appendChild(this._gotoHomeItem); - - for (let i = 0; i < Math.min(results.length, SEARCH_RENDER_COUNT); ++i) { - const item = this._renderResult(results[i]); - this._contentElement.appendChild(item); - } - - this._remainingResults = results.slice(SEARCH_RENDER_COUNT); - if (this._remainingResults.length > 0) { - this._showOtherItem.textContent = `Show Remaining ${this._remainingResults.length} Results.`; - this._contentElement.appendChild(this._showOtherItem); - } - this._selectElement(this._contentElement.firstChild, true /* omitScroll */); - } - - cancelSearch() { - this.input.blur(); - this._setVisible(false); - this.input.value = this._defaultValue; - app.focusContent(); - } - - _selectNext(event) { - if (!this._selectedElement) - return; - event.preventDefault(); - let next = this._selectedElement.nextSibling; - if (!next) - next = this._contentElement.firstChild; - this._selectElement(next); - } - - _selectPrevious(event) { - if (!this._selectedElement) - return; - event.preventDefault(); - let previous = this._selectedElement.previousSibling; - if (!previous) - previous = this._contentElement.lastChild; - this._selectElement(previous); - } - - _selectElement(item, omitScroll) { - if (this._selectedElement) - this._selectedElement.classList.remove('selected'); - this._selectedElement = item; - if (this._selectedElement) { - if (!omitScroll) - this._selectedElement.scrollIntoViewIfNeeded(false); - this._selectedElement.classList.add('selected'); - } - } - - _renderResult(result) { - const icon = result.item.iconElement(); - const title = result.item.titleElement(result.matches); - const subtitle= result.item.subtitleElement(); - const item = html` - - ${icon ? html`${icon}` : ''} - ${title} - ${subtitle ? html`${subtitle}` : ''} - - `; - item[SearchComponent._symbol] = result.item; - return item; - } - - _setVisible(visible) { - if (visible === this._visible) - return; - this._visible = visible; - if (visible) { - const box = this.input.getBoundingClientRect(); - this.element.style.setProperty('--search-input-x', box.x + 'px'); - document.body.appendChild(this.element); - } else { - this.element.remove(); - } - } -} - -SearchComponent._symbol = Symbol('SearchComponent._symbol'); - -SearchComponent.Item = class { - text() {} - - url() {} - - iconElement() { } - - titleElement(matches) {} - - subtitleElement() {} -} diff --git a/src/ui/SettingsComponent.js b/src/ui/SettingsComponent.js deleted file mode 100644 index 1fd72d4..0000000 --- a/src/ui/SettingsComponent.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * 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 {EventEmitter} from './EventEmitter.js'; -import {html} from './html.js'; - -export class SettingsComponent extends EventEmitter { - constructor() { - super(); - this.element = html``; - - this._selectedItem = null; - document.body.addEventListener('keydown', event => { - if (!this.element.parentElement) - return; - if (event.key === 'Escape') { - this.hide(); - event.preventDefault(); - event.stopPropagation(); - } - }, false); - this.element.addEventListener('click', () => this.hide(), false); - } - - _selectItem(item) { - if (this._selectedItem) - this._selectedItem.classList.remove('selected'); - this._selectedItem = item; - if (this._selectedItem) - this._selectedItem.classList.add('selected'); - } - - show(product, version) { - const renderVersion = (description) => { - const selected = description.name === version.name(); - const item = html` - - ${description.name} - ${description.description} - ${formatDate(description.date)} - - `; - item[SettingsComponent._Symbol] = {product, versionName: description.name}; - return item; - }; - - this.element.innerHTML = ''; - this.element.appendChild(html` - - -

Settings

- -
- ${product.versionDescriptions().map(renderVersion)} - - ${product.settingsFooterElement()} - -
WebSite Version:${window.__WEBSITE_VERSION__ || 'tip-of-tree'} File a bug!
-
-
- `); - this.element.$('settings-content').addEventListener('click', event => { - event.stopPropagation(); - // Support clicks on links, e.g. "file a bug". - if (event.target.tagName === 'A') { - event.stopPropagation(); - return; - } - event.preventDefault(); - // Allow selecting versions. - if (!window.getSelection().isCollapsed) - return; - if (event.target.classList.contains('settings-close-icon')) { - this.hide(); - return; - } - let item = event.target; - while (item && item.tagName !== 'PRODUCT-VERSION') - item = item.parentElement; - if (!item) - return; - this._selectItem(item); - const {product, versionName} = item[SettingsComponent._Symbol]; - this.hide(); - this.emit(SettingsComponent.Events.VersionSelected, product, versionName); - }, false); - - document.body.appendChild(this.element); - this._selectedItem = this.element.$('product-version.selected'); - if (this._selectedItem) - this._selectedItem.scrollIntoViewIfNeeded(); - - function formatDate(date) { - if (!date) - return 'N/A'; - const monthNames = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; - const day = date.getDate(); - const month = date.getMonth(); - const year = date.getFullYear(); - return monthNames[month] + ' ' + day + ', ' + year; - } - } - - hide() { - this.element.remove(); - // Cleanup content to free some memory. - this.element.innerHTML = ''; - this._selectedItem = null; - } -} - -SettingsComponent._Symbol = Symbol('SettingsComponent._Symbol'); - -SettingsComponent.Events = { - VersionSelected: 'VersionSelected', -}; diff --git a/src/ui/SidebarComponent.js b/src/ui/SidebarComponent.js deleted file mode 100644 index 2a731f0..0000000 --- a/src/ui/SidebarComponent.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * 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 {html} from './html.js'; - -export class SidebarComponent { - constructor() { - this.element = html``; - this.element.addEventListener('click', this._onClick.bind(this), false); - this.glasspane = html``; - this.glasspane.addEventListener('click', event => { - this.hideOnMobile(); - event.stopPropagation(); - event.preventDefault(); - }, false); - - this._selectedItem = null; - } - - _onClick(event) { - let item = event.target; - while (item && item.parentElement !== this.element) - item = item.parentElement; - if (item && this._selectedItem !== item) { - this.hideOnMobile(); - if (this._selectedItem) - this._selectedItem.classList.remove('selected'); - this._selectedItem = item; - this._selectedItem.classList.add('selected'); - } - } - - setElements(elements) { - this.element.innerHTML = ''; - for (const element of elements) { - this.element.appendChild(html` - ${element} - `); - } - } - - hideOnMobile() { - this.element.classList.remove('show-on-mobile'); - this.glasspane.classList.remove('show-on-mobile'); - } - - toggleOnMobile() { - this.element.classList.toggle('show-on-mobile'); - this.glasspane.classList.toggle('show-on-mobile'); - } - - setSelected(element) { - if (this._selectedItem) { - this._selectedItem.classList.remove('selected'); - this._selectedItem = null; - } - if (!element) - return; - const item = element.parentElement; - if (!item || item.parentElement !== this.element) - return; - this._selectedItem = item; - this._selectedItem.classList.add('selected'); - } -} - diff --git a/src/ui/ToolbarComponent.js b/src/ui/ToolbarComponent.js deleted file mode 100644 index 5d3bd29..0000000 --- a/src/ui/ToolbarComponent.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 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 {html} from './html.js'; - -export class ToolbarComponent { - constructor() { - this.element = html` - - - - - - `; - } - - left() { - return this.element.$('.left'); - } - - middle() { - return this.element.$('.middle'); - } - - right() { - return this.element.$('.right'); - } -} - diff --git a/src/ui/content-component.css b/src/ui/content-component.css deleted file mode 100644 index b3d349d..0000000 --- a/src/ui/content-component.css +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license 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. - */ - -content-component { - background-color: #fafafa; - position: absolute; - left: var(--sidebar-width); - top: var(--search-height); - bottom: 0; - right: 0; - overflow-y: scroll; - -webkit-overflow-scrolling: touch; -} - -@media only screen and (max-width: 800px) { - content-component { - left: 0; - } -} diff --git a/src/ui/html.js b/src/ui/html.js deleted file mode 100644 index 7ce6218..0000000 --- a/src/ui/html.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * ZHTML 1.1.0 - * https://github.com/mezzoeditor/zhtml - */ -const templateCache = new Map(); - -const BOOLEAN_ATTRS = new Set([ - 'async', 'autofocus', 'autoplay', 'checked', 'contenteditable', 'controls', - 'default', 'defer', 'disabled', 'formNoValidate', 'frameborder', 'hidden', - 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', - 'open', 'readonly', 'required', 'reversed', 'scoped', 'selected', 'typemustmatch', -]); - -export function html(strings, ...values) { - let cache = templateCache.get(strings); - if (!cache) { - cache = prepareTemplate(strings); - templateCache.set(strings, cache); - } - const node = renderTemplate(cache.template, cache.subs, values); - if (node.querySelector) { - node.$ = node.querySelector.bind(node); - node.$$ = node.querySelectorAll.bind(node); - } - return node; -} - -const SPACE_REGEX = /^\s*\n\s*$/; -const MARKER_REGEX = /z-t-e-\d+-m-p-l-a-t-e/; - -function prepareTemplate(strings) { - const template = document.createElement('template'); - let html = '' - for (let i = 0; i < strings.length - 1; ++i) { - html += strings[i]; - html += `z-t-e-${i}-m-p-l-a-t-e`; - } - html += strings[strings.length - 1]; - template.innerHTML = html; - - const walker = template.ownerDocument.createTreeWalker( - template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null, false); - let valueIndex = 0; - const emptyTextNodes = []; - const subs = []; - while (walker.nextNode()) { - const node = walker.currentNode; - if (node.nodeType === Node.ELEMENT_NODE && MARKER_REGEX.test(node.tagName)) - throw new Error('Should not use a parameter as an html tag'); - - if (node.nodeType === Node.ELEMENT_NODE && node.hasAttributes()) { - for (let i = 0; i < node.attributes.length; i++) { - const name = node.attributes[i].name; - - const nameParts = name.split(MARKER_REGEX); - const valueParts = node.attributes[i].value.split(MARKER_REGEX); - const isSimpleValue = valueParts.length === 2 && valueParts[0] === '' && valueParts[1] === ''; - - if (nameParts.length > 1 || valueParts.length > 1) - subs.push({ node, nameParts, valueParts, isSimpleValue, attr: name}); - } - } else if (node.nodeType === Node.TEXT_NODE && MARKER_REGEX.test(node.data)) { - const texts = node.data.split(MARKER_REGEX); - node.data = texts[0]; - const anchor = node.nextSibling; - for (let i = 1; i < texts.length; ++i) { - const span = document.createElement('span'); - node.parentNode.insertBefore(span, anchor); - node.parentNode.insertBefore(document.createTextNode(texts[i]), anchor); - subs.push({ - node: span, - type: 'replace-node', - }); - } - if (shouldRemoveTextNode(node)) - emptyTextNodes.push(node); - } else if (node.nodeType === Node.TEXT_NODE && shouldRemoveTextNode(node)) { - emptyTextNodes.push(node); - } - } - - for (const emptyTextNode of emptyTextNodes) - emptyTextNode.remove(); - - const markedNodes = new Map(); - for (const sub of subs) { - let index = markedNodes.get(sub.node); - if (index === undefined) { - index = markedNodes.size; - sub.node.setAttribute('z-framework-marked-node', true); - markedNodes.set(sub.node, index); - } - sub.nodeIndex = index; - } - return {template, subs}; -} - -function shouldRemoveTextNode(node) { - if (!node.previousSibling && !node.nextSibling) - return !node.data.length; - return (!node.previousSibling || node.previousSibling.nodeType === Node.ELEMENT_NODE) && - (!node.nextSibling || node.nextSibling.nodeType === Node.ELEMENT_NODE) && - (!node.data.length || SPACE_REGEX.test(node.data)); -} - -function renderTemplate(template, subs, values) { - let node = null; - const content = template.ownerDocument.importNode(template.content, true); - if (content.firstChild === content.lastChild) - node = content.firstChild; - else - node = content; - - const boundElements = Array.from(content.querySelectorAll('[z-framework-marked-node]')); - for (const node of boundElements) - node.removeAttribute('z-framework-marked-node'); - - let valueIndex = 0; - const interpolateText = (texts) => { - let newText = texts[0]; - for (let i = 1; i < texts.length; ++i) { - newText += values[valueIndex++]; - newText += texts[i]; - } - return newText; - } - - for (const sub of subs) { - const node = boundElements[sub.nodeIndex]; - if (sub.attr) { - node.removeAttribute(sub.attr); - const name = interpolateText(sub.nameParts); - const value = sub.isSimpleValue ? values[valueIndex++] : interpolateText(sub.valueParts); - if (BOOLEAN_ATTRS.has(name)) - node.toggleAttribute(name, !!value); - else - node.setAttribute(name, value); - } else if (sub.type === 'replace-node') { - const replacement = values[valueIndex++]; - if (Array.isArray(replacement)) { - const fragment = document.createDocumentFragment(); - for (const node of replacement) - fragment.appendChild(node); - node.replaceWith(fragment); - } else if (replacement instanceof Node) { - node.replaceWith(replacement); - } else { - node.replaceWith(document.createTextNode(replacement)); - } - } - } - - return node; -} diff --git a/src/ui/main.css b/src/ui/main.css deleted file mode 100644 index 731ef32..0000000 --- a/src/ui/main.css +++ /dev/null @@ -1,225 +0,0 @@ -/** - * @license 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. - */ - -:root { - --divider-color: rgba(0, 0, 0, 0.14); - --sidebar-width: 200px; - --search-height: 50px; - --hover-color: rgba(225, 245, 254, 0.49); - --selected-color: #e3f2fd; - --monospace: Consolas, Menlo, monospace; - --non-monospace: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - --black: #24292e; - font-family: var(--non-monospace); - line-height: 1.5; - color: var(--black); -} - -body { - background-color: #fafafa; -} - -settings-button { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - opacity: 0.5; - flex-shrink: 0; - display: none; - transform: scale(0.8); -} - -settings-button img { - width: 31px; -} - -settings-button:hover { - opacity: 1; -} - -home-button { - display: flex; - align-items: center; - justify-content: center; - transform: scale(0.75); - cursor: pointer; - opacity: 0.5; - flex-shrink: 0; -} - -home-button:hover { - opacity: 1; -} - -menu-button { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - opacity: 0.5; - flex-shrink: 0; - display: none; -} - -menu-button:hover { - opacity: 1; -} - -search-button { - padding-right: 4px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - opacity: 0.5; - flex-shrink: 0; - display: none; -} - -search-button:hover { - opacity: 1; -} - -app-title { - font-size: 20px; - color: white; - white-space: nowrap; - flex-shrink: 0; - margin-left: 1ex; -} - -.show-mobile-search app-title { - display: none; -} - -app-title-version-name { - border-bottom: 1px dashed white; - cursor: pointer; - margin: 0 1ex; -} - -app-title-version-name:hover { - border-bottom: 2px dashed white; -} - -a { - font-weight: normal !important; - color: #0366d6; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -external-link-icon { - background-position: center right; - background-repeat: no-repeat; - background-image: linear-gradient(transparent,transparent),url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22%3E %3Cpath fill=%22%23fff%22 stroke=%22%2336c%22 d=%22M1.5 4.518h5.982V10.5H1.5z%22/%3E %3Cpath fill=%22%2336c%22 d=%22M5.765 1H11v5.39L9.427 7.937l-1.31-1.31L5.393 9.35l-2.69-2.688 2.81-2.808L4.2 2.544z%22/%3E %3Cpath fill=%22%23fff%22 d=%22M9.995 2.004l.022 4.885L8.2 5.07 5.32 7.95 4.09 6.723l2.882-2.88-1.85-1.852z%22/%3E %3C/svg%3E"); - width: 13px; - height: 13px; - display: inline-block; -} - -blockquote { - background: #FFFDE7; - padding: 1px 1em 1px 2em; - margin: 2em 0; - border-left: 0.25em solid #FFEB3B; -} - -loading-screen { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - left: 0; - right: 0; - top: 0; - bottom: 0; -} - -loading-screen loading-content { - display: flex; - flex-direction: column; - align-items: center; -} - -loading-screen img { - opacity: 0.1; -} - -loading-screen .text { - white-space: pre-wrap; - text-align: center; - margin: 2em; -} - -loading-screen .spinner { - width: 50px; - height: 40px; - text-align: center; - font-size: 10px; - margin-top: 30px; -} - -loading-screen .spinner > div { - background-color: #40b5a491; - height: 100%; - width: 6px; - display: inline-block; - -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; - animation: sk-stretchdelay 1.2s infinite ease-in-out; - margin-left: 3px; -} - -loading-screen .spinner .rect2 { - -webkit-animation-delay: -1.1s; - animation-delay: -1.1s; -} - -loading-screen .spinner .rect3 { - -webkit-animation-delay: -1.0s; - animation-delay: -1.0s; -} - -loading-screen .spinner .rect4 { - -webkit-animation-delay: -0.9s; - animation-delay: -0.9s; -} - -loading-screen .spinner .rect5 { - -webkit-animation-delay: -0.8s; - animation-delay: -0.8s; -} - -@-webkit-keyframes sk-stretchdelay { - 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } - 20% { -webkit-transform: scaleY(1.0) } -} - -@keyframes sk-stretchdelay { - 0%, 40%, 100% { - transform: scaleY(0.4); - -webkit-transform: scaleY(0.4); - } 20% { - transform: scaleY(1.0); - -webkit-transform: scaleY(1.0); - } -} - -@media only screen and (max-width: 800px) { - search-button, - settings-button, - menu-button { - display: flex; - } - - app-title { - display: none; - } -} diff --git a/src/ui/polyfills.js b/src/ui/polyfills.js deleted file mode 100644 index 0ec934a..0000000 --- a/src/ui/polyfills.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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. - */ - -if (!Element.prototype.scrollIntoViewIfNeeded) { - Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) { - centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; - - var parent = this.parentNode, - parentComputedStyle = window.getComputedStyle(parent, null), - parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')), - parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')), - overTop = this.offsetTop - parent.offsetTop < parent.scrollTop, - overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight), - overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft, - overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth), - alignWithTop = overTop && !overBottom; - - if ((overTop || overBottom) && centerIfNeeded) { - parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2; - } - - if ((overLeft || overRight) && centerIfNeeded) { - parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2; - } - - if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { - this.scrollIntoView(alignWithTop); - } - }; -} diff --git a/src/ui/search-component.css b/src/ui/search-component.css deleted file mode 100644 index 79ee564..0000000 --- a/src/ui/search-component.css +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @license 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. - */ - -search-component { - position: absolute; - left: 0; - right: 0; - top: var(--search-height); - bottom: 0; - background: rgba(0, 0, 0, 0.5); - /* display: flex; */ - justify-content: center; - overflow: hidden; - align-items: start; - --search-item-icon-width: 20px; - --search-item-gap: 13px; - --search-item-padding: 18px; - contain: strict; -} - -search-component search-results { - max-width: calc(100% - var(--results-left)); - --results-left: calc(var(--search-input-x) - var(--search-item-gap) - var(--search-item-icon-width) - var(--search-item-padding) + 1ex); - left: var(--results-left); - width: 700px; - max-height: 700px; - background: white; - overflow: auto; - position: relative; - display: block; -} - -@media only screen and (max-width: 800px) { - search-component search-results { - width: 100%; - max-width: 100%; - max-height: 100%; - left: 0; - right: 0; - top: 0; - bottom: 0; - position: absolute; - } -} - -search-highlight { - background: #ff0; -} - -search-item { - display: grid; - align-items: center; - grid-template-columns: var(--search-item-icon-width) auto; - grid-template-rows: auto auto; - grid-column-gap: var(--search-item-gap); - grid-template-areas: "icon title" "icon subtitle"; - padding: 4px var(--search-item-padding); - cursor: pointer; - min-height: 50px; - border-bottom: 1px solid rgba(51,51,51,.12); -} - -search-item.no-subtitle { - grid-template-areas: "icon title"; - grid-template-rows: auto; -} - -search-item-custom { - display: flex; - align-items: center; - justify-content: center; - font-family: var(--monospace); - height: 50px; - border-bottom: 1px solid rgba(51,51,51,.12); -} - -search-item-custom.selected, -search-item.selected { - background-color: #e3f2fd; -} - -search-item-custom:hover, -search-item:hover { - background-color: var(--hover-color); -} - -search-item-icon { - grid-area: icon; - display: flex; - align-items: center; - justify-content: center; -} - -search-item-title { - grid-area: title; - font-family: var(--monospace); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -search-item-subtitle { - grid-area: subtitle; - font-size: 90%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} diff --git a/src/ui/settings-component.css b/src/ui/settings-component.css deleted file mode 100644 index 9cfbeb4..0000000 --- a/src/ui/settings-component.css +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @license 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. - */ - -settings-component { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - overflow: hidden; - align-items: center; - z-index: 1000; -} - -settings-component settings-content { - max-width: 700px; - width: 70%; - max-height: 70%; - background: white; - overflow: hidden; - flex-grow: 0; - display: flex; - flex-direction: column; - padding: 1em; - position: relative; -} - -@media only screen and (max-width: 800px) { - settings-component settings-content { - max-width: 100%; - max-height: 100%; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - width: unset; - height: unset; - } -} - -settings-component product-versions { - display: block; - border: 0.25em solid var(--divider-color); - overflow: auto; - -webkit-overflow-scrolling: touch; -} - -settings-component settings-header { - border-bottom: 1px solid var(--divider-color); - display: flex; - justify-content: space-between; - padding: 0 1ex; - flex-shrink: 0; -} - -settings-component settings-header h3 { - margin: 0; - font-weight: normal; - font-size: 24px; - margin-bottom: 1ex; -} - -settings-component .settings-close-icon { - opacity: 0.5; - cursor: pointer; -} - -settings-component .settings-close-icon:hover { - opacity: 1; -} - -settings-component product-version { - display: grid; - grid-template-columns: 100px 100px auto; - grid-template-rows: auto; - grid-template-areas: "name date description"; - grid-column-gap: 10px; - padding: 1em; - align-items: center; - border-bottom: 1px solid rgba(51,51,51,.12); - cursor: pointer; -} - -settings-component product-version:hover { - background-color: var(--hover-color); -} - -settings-component product-version.selected { - background-color: var(--selected-color); -} - -settings-component product-version version-name { - grid-area: name; - font-family: var(--monospace); -} - -settings-component product-version version-description { - grid-area: description; - font-size: 90%; - justify-self: center; -} - -settings-component product-version version-date { - grid-area: date; - font-size: 90%; -} - -settings-component website-version { - display: flex; - flex-direction: row; - justify-content: center; - font-size: 90%; - align-items: baseline; -} - -settings-component website-version code { - margin: 0 1ex; -} diff --git a/src/ui/sidebar-component.css b/src/ui/sidebar-component.css deleted file mode 100644 index 67fe7e5..0000000 --- a/src/ui/sidebar-component.css +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license 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. - */ - -sidebar-component { - background: #fff; - position: absolute; - width: var(--sidebar-width); - top: var(--search-height); - bottom: 0; - left: 0; - overflow-y: scroll; - -webkit-overflow-scrolling: touch; - border-right: 1px solid var(--divider-color); - overflow-x: hidden; -} - -sidebar-component sidebar-item { - display: flex; -} - -sidebar-component sidebar-item:hover { - background-color: var(--hover-color); -} - -sidebar-component sidebar-item.selected { - background-color: var(--selected-color); -} - -sidebar-component sidebar-glasspane { - display: none; -} - -@media only screen and (max-width: 800px) { - sidebar-component { - display: none; - } - - sidebar-component.show-on-mobile { - display: block; - left: 0; - right: 0; - top: var(--search-height); - bottom: 0; - } - - sidebar-glasspane.show-on-mobile { - display: block; - position: absolute; - left: 0; - right: 0; - top: var(--search-height); - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - } -} diff --git a/src/ui/toolbar-component.css b/src/ui/toolbar-component.css deleted file mode 100644 index b173777..0000000 --- a/src/ui/toolbar-component.css +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license 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. - */ - -toolbar-component { - background: hsla(531, 48%, 48%, 1); - position: absolute; - left: 0; - right: 0; - top: 0; - height: var(--search-height); - border-bottom: 1px solid rgba(0, 0, 0, 0.14); - display: flex; - align-items: center; - justify-content: space-between; - overflow: hidden; - z-index: 100; - padding: 0 15px; -} - -toolbar-component -input[type=search] { - -webkit-appearance: none; - background: transparent; - text-align: left; - border: none; - border-bottom: 2px solid rgb(255, 255, 255); - padding: 0 1ex; - font-size: 20px; - color: white; - margin: 0 1ex; - text-overflow: ellipsis; - max-width: 250px; - flex-grow: 1; -} - -toolbar-component -input[type=search]::placeholder { - color: #eeeeeeb0; - text-align: center; -} - -toolbar-component -input[type=search]:focus { - outline: none; -} - -toolbar-component toolbar-section { - display: flex; - align-items: center; - flex-grow: 0; -} - -toolbar-component toolbar-section.left { - flex-grow: 1; -} - -@media only screen and (max-width: 800px) { - toolbar-section.right { - display: none; - } - - toolbar-component input[type=search] { - width: initial; - } -} diff --git a/src/web.ts b/src/web.ts new file mode 100644 index 0000000..a48a5cf --- /dev/null +++ b/src/web.ts @@ -0,0 +1,24 @@ +/** + * 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 { initializePuppeteerWeb } from './initialize-web.js'; +import { isNode } from './environment.js'; + +if (isNode) { + throw new Error('Trying to run Puppeteer-Web in a Node environment'); +} + +export default initializePuppeteerWeb('puppeteer'); diff --git a/test-browser/connection.spec.js b/test-browser/connection.spec.js new file mode 100644 index 0000000..eef8fe3 --- /dev/null +++ b/test-browser/connection.spec.js @@ -0,0 +1,55 @@ +/** + * 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 { Connection } from '../lib/esm/puppeteer/common/Connection.js'; +import { BrowserWebSocketTransport } from '../lib/esm/puppeteer/common/BrowserWebSocketTransport.js'; +import puppeteer from '../lib/esm/puppeteer/web.js'; +import expect from '../node_modules/expect/build-es5/index.js'; +import { getWebSocketEndpoint } from './helper.js'; + +describe('creating a Connection', () => { + it('can create a real connection to the backend and send messages', async () => { + const wsUrl = getWebSocketEndpoint(); + const transport = await BrowserWebSocketTransport.create(wsUrl); + + const connection = new Connection(wsUrl, transport); + const result = await connection.send('Browser.getVersion'); + /* We can't expect exact results as the version of Chrome/CDP might change + * and we don't want flakey tests, so let's assert the structure, which is + * enough to confirm the result was recieved successfully. + */ + expect(result).toEqual({ + protocolVersion: expect.any(String), + jsVersion: expect.any(String), + revision: expect.any(String), + userAgent: expect.any(String), + product: expect.any(String), + }); + }); +}); + +describe('puppeteer.connect', () => { + it('can connect over websocket and make requests to the backend', async () => { + const wsUrl = getWebSocketEndpoint(); + const browser = await puppeteer.connect({ + browserWSEndpoint: wsUrl, + }); + + const version = await browser.version(); + const versionLooksCorrect = /.+Chrome\/\d{2}/.test(version); + expect(version).toEqual(expect.any(String)); + expect(versionLooksCorrect).toEqual(true); + }); +}); diff --git a/test-browser/debug.spec.js b/test-browser/debug.spec.js new file mode 100644 index 0000000..971d3a5 --- /dev/null +++ b/test-browser/debug.spec.js @@ -0,0 +1,65 @@ +/** + * 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 { debug } from '../lib/esm/puppeteer/common/Debug.js'; +import expect from '../node_modules/expect/build-es5/index.js'; + +describe('debug', () => { + let originalLog; + let logs; + beforeEach(() => { + originalLog = console.log; + logs = []; + console.log = (...args) => { + logs.push(args); + }; + }); + + afterEach(() => { + console.log = originalLog; + }); + + it('should return a function', async () => { + expect(debug('foo')).toBeInstanceOf(Function); + }); + + it('does not log to the console if __PUPPETEER_DEBUG global is not set', async () => { + const debugFn = debug('foo'); + debugFn('lorem', 'ipsum'); + + expect(logs.length).toEqual(0); + }); + + it('logs to the console if __PUPPETEER_DEBUG global is set to *', async () => { + globalThis.__PUPPETEER_DEBUG = '*'; + const debugFn = debug('foo'); + debugFn('lorem', 'ipsum'); + + expect(logs.length).toEqual(1); + expect(logs).toEqual([['foo:', 'lorem', 'ipsum']]); + }); + + it('logs only messages matching the __PUPPETEER_DEBUG prefix', async () => { + globalThis.__PUPPETEER_DEBUG = 'foo'; + const debugFoo = debug('foo'); + const debugBar = debug('bar'); + debugFoo('a'); + debugBar('b'); + + expect(logs.length).toEqual(1); + expect(logs).toEqual([['foo:', 'a']]); + }); +}); diff --git a/test-browser/helper.js b/test-browser/helper.js new file mode 100644 index 0000000..6cfbe93 --- /dev/null +++ b/test-browser/helper.js @@ -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. + */ + +/** + * Returns the web socket endpoint for the backend of the browser the tests run + * in. Used to create connections to that browser in Puppeteer for unit tests. + * + * It's available on window.__ENV__ because setup code in + * web-test-runner.config.js puts it there. If you're changing this code (or + * that code), make sure the other is updated accordingly. + */ +export function getWebSocketEndpoint() { + return window.__ENV__.wsEndpoint; +} diff --git a/test-ts-types/js-cjs-import-cjs-output/bad.js b/test-ts-types/js-cjs-import-cjs-output/bad.js new file mode 100644 index 0000000..be51279 --- /dev/null +++ b/test-ts-types/js-cjs-import-cjs-output/bad.js @@ -0,0 +1,18 @@ +const puppeteer = require('puppeteer'); + +async function run() { + // Typo in "launch" + const browser = await puppeteer.launh(); + // Typo: "devices" + const devices = puppeteer.devics; + console.log(devices); + const browser2 = await puppeteer.launch(); + // 'foo' is invalid argument + const page = await browser2.newPage('foo'); + /** + * @type {puppeteer.ElementHandle} + */ + const div = await page.$('div'); + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/js-cjs-import-cjs-output/good.js b/test-ts-types/js-cjs-import-cjs-output/good.js new file mode 100644 index 0000000..f466300 --- /dev/null +++ b/test-ts-types/js-cjs-import-cjs-output/good.js @@ -0,0 +1,17 @@ +const puppeteer = require('puppeteer'); + +async function run() { + const browser = await puppeteer.launch(); + const devices = puppeteer.devices; + console.log(devices); + const page = await browser.newPage(); + const div = await page.$('div'); + if (div) { + /** + * @type {puppeteer.ElementHandle} + */ + const newDiv = div; + console.log('got a div!', newDiv); + } +} +run(); diff --git a/test-ts-types/js-cjs-import-cjs-output/package.json b/test-ts-types/js-cjs-import-cjs-output/package.json new file mode 100644 index 0000000..6141e95 --- /dev/null +++ b/test-ts-types/js-cjs-import-cjs-output/package.json @@ -0,0 +1,12 @@ +{ + "name": "test-ts-types-ts-esm", + "version": "1.0.0", + "private": true, + "description": "Test project with TypeScript, ESM output", + "scripts": { + "compile": "../../node_modules/.bin/tsc" + }, + "dependencies": { + "puppeteer": "file:../../puppeteer.tgz" + } +} diff --git a/test-ts-types/js-cjs-import-cjs-output/tsconfig.json b/test-ts-types/js-cjs-import-cjs-output/tsconfig.json new file mode 100644 index 0000000..2889972 --- /dev/null +++ b/test-ts-types/js-cjs-import-cjs-output/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "checkJs": true, + "allowJs": true, + "strict": true, + "outDir": "dist", + "moduleResolution": "node" + }, + "files": ["good.js", "bad.js"] +} diff --git a/test-ts-types/js-cjs-import-esm-output/bad.js b/test-ts-types/js-cjs-import-esm-output/bad.js new file mode 100644 index 0000000..be51279 --- /dev/null +++ b/test-ts-types/js-cjs-import-esm-output/bad.js @@ -0,0 +1,18 @@ +const puppeteer = require('puppeteer'); + +async function run() { + // Typo in "launch" + const browser = await puppeteer.launh(); + // Typo: "devices" + const devices = puppeteer.devics; + console.log(devices); + const browser2 = await puppeteer.launch(); + // 'foo' is invalid argument + const page = await browser2.newPage('foo'); + /** + * @type {puppeteer.ElementHandle} + */ + const div = await page.$('div'); + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/js-cjs-import-esm-output/good.js b/test-ts-types/js-cjs-import-esm-output/good.js new file mode 100644 index 0000000..f466300 --- /dev/null +++ b/test-ts-types/js-cjs-import-esm-output/good.js @@ -0,0 +1,17 @@ +const puppeteer = require('puppeteer'); + +async function run() { + const browser = await puppeteer.launch(); + const devices = puppeteer.devices; + console.log(devices); + const page = await browser.newPage(); + const div = await page.$('div'); + if (div) { + /** + * @type {puppeteer.ElementHandle} + */ + const newDiv = div; + console.log('got a div!', newDiv); + } +} +run(); diff --git a/test-ts-types/js-cjs-import-esm-output/package.json b/test-ts-types/js-cjs-import-esm-output/package.json new file mode 100644 index 0000000..063237e --- /dev/null +++ b/test-ts-types/js-cjs-import-esm-output/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-ts-types-ts-esm", + "version": "1.0.0", + "private": true, + "description": "Test project with TypeScript, ESM output", + "scripts": { + "compile": "../../node_modules/.bin/tsc" + }, + "devDependencies": { + "typescript": "^4.1.3" + }, + "dependencies": { + "puppeteer": "file:../../puppeteer.tgz" + } +} diff --git a/test-ts-types/js-cjs-import-esm-output/tsconfig.json b/test-ts-types/js-cjs-import-esm-output/tsconfig.json new file mode 100644 index 0000000..e2ce292 --- /dev/null +++ b/test-ts-types/js-cjs-import-esm-output/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "esnext", + "checkJs": true, + "allowJs": true, + "strict": true, + "outDir": "dist", + "moduleResolution": "node" + }, + "files": ["good.js", "bad.js"] +} diff --git a/test-ts-types/js-esm-import-cjs-output/bad.js b/test-ts-types/js-esm-import-cjs-output/bad.js new file mode 100644 index 0000000..ba7b561 --- /dev/null +++ b/test-ts-types/js-esm-import-cjs-output/bad.js @@ -0,0 +1,18 @@ +import * as puppeteer from 'puppeteer'; + +async function run() { + // Typo in "launch" + const browser = await puppeteer.launh(); + // Typo: "devices" + const devices = puppeteer.devics; + console.log(devices); + const browser2 = await puppeteer.launch(); + // 'foo' is invalid argument + const page = await browser2.newPage('foo'); + /** + * @type {puppeteer.ElementHandle} + */ + const div = await page.$('div'); + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/js-esm-import-cjs-output/good.js b/test-ts-types/js-esm-import-cjs-output/good.js new file mode 100644 index 0000000..544c43c --- /dev/null +++ b/test-ts-types/js-esm-import-cjs-output/good.js @@ -0,0 +1,17 @@ +import * as puppeteer from 'puppeteer'; + +async function run() { + const browser = await puppeteer.launch(); + const devices = puppeteer.devices; + console.log(devices); + const page = await browser.newPage(); + const div = await page.$('div'); + if (div) { + /** + * @type {puppeteer.ElementHandle} + */ + const newDiv = div; + console.log('got a div!', newDiv); + } +} +run(); diff --git a/test-ts-types/js-esm-import-cjs-output/package.json b/test-ts-types/js-esm-import-cjs-output/package.json new file mode 100644 index 0000000..063237e --- /dev/null +++ b/test-ts-types/js-esm-import-cjs-output/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-ts-types-ts-esm", + "version": "1.0.0", + "private": true, + "description": "Test project with TypeScript, ESM output", + "scripts": { + "compile": "../../node_modules/.bin/tsc" + }, + "devDependencies": { + "typescript": "^4.1.3" + }, + "dependencies": { + "puppeteer": "file:../../puppeteer.tgz" + } +} diff --git a/test-ts-types/js-esm-import-cjs-output/tsconfig.json b/test-ts-types/js-esm-import-cjs-output/tsconfig.json new file mode 100644 index 0000000..2889972 --- /dev/null +++ b/test-ts-types/js-esm-import-cjs-output/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "checkJs": true, + "allowJs": true, + "strict": true, + "outDir": "dist", + "moduleResolution": "node" + }, + "files": ["good.js", "bad.js"] +} diff --git a/test-ts-types/js-esm-import-esm-output/bad.js b/test-ts-types/js-esm-import-esm-output/bad.js new file mode 100644 index 0000000..ba7b561 --- /dev/null +++ b/test-ts-types/js-esm-import-esm-output/bad.js @@ -0,0 +1,18 @@ +import * as puppeteer from 'puppeteer'; + +async function run() { + // Typo in "launch" + const browser = await puppeteer.launh(); + // Typo: "devices" + const devices = puppeteer.devics; + console.log(devices); + const browser2 = await puppeteer.launch(); + // 'foo' is invalid argument + const page = await browser2.newPage('foo'); + /** + * @type {puppeteer.ElementHandle} + */ + const div = await page.$('div'); + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/js-esm-import-esm-output/good.js b/test-ts-types/js-esm-import-esm-output/good.js new file mode 100644 index 0000000..544c43c --- /dev/null +++ b/test-ts-types/js-esm-import-esm-output/good.js @@ -0,0 +1,17 @@ +import * as puppeteer from 'puppeteer'; + +async function run() { + const browser = await puppeteer.launch(); + const devices = puppeteer.devices; + console.log(devices); + const page = await browser.newPage(); + const div = await page.$('div'); + if (div) { + /** + * @type {puppeteer.ElementHandle} + */ + const newDiv = div; + console.log('got a div!', newDiv); + } +} +run(); diff --git a/test-ts-types/js-esm-import-esm-output/package.json b/test-ts-types/js-esm-import-esm-output/package.json new file mode 100644 index 0000000..063237e --- /dev/null +++ b/test-ts-types/js-esm-import-esm-output/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-ts-types-ts-esm", + "version": "1.0.0", + "private": true, + "description": "Test project with TypeScript, ESM output", + "scripts": { + "compile": "../../node_modules/.bin/tsc" + }, + "devDependencies": { + "typescript": "^4.1.3" + }, + "dependencies": { + "puppeteer": "file:../../puppeteer.tgz" + } +} diff --git a/test-ts-types/js-esm-import-esm-output/tsconfig.json b/test-ts-types/js-esm-import-esm-output/tsconfig.json new file mode 100644 index 0000000..e2ce292 --- /dev/null +++ b/test-ts-types/js-esm-import-esm-output/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "esnext", + "checkJs": true, + "allowJs": true, + "strict": true, + "outDir": "dist", + "moduleResolution": "node" + }, + "files": ["good.js", "bad.js"] +} diff --git a/test-ts-types/ts-cjs-import-cjs-output/bad.ts b/test-ts-types/ts-cjs-import-cjs-output/bad.ts new file mode 100644 index 0000000..9199f8a --- /dev/null +++ b/test-ts-types/ts-cjs-import-cjs-output/bad.ts @@ -0,0 +1,17 @@ +import puppeteer = require('puppeteer'); + +async function run() { + // Typo in "launch" + const browser = await puppeteer.launh(); + // Typo: "devices" + const devices = puppeteer.devics; + console.log(devices); + const browser2 = await puppeteer.launch(); + // 'foo' is invalid argument + const page = await browser2.newPage('foo'); + const div = (await page.$('div')) as puppeteer.ElementHandle< + HTMLAnchorElement + >; + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/ts-cjs-import-cjs-output/good.ts b/test-ts-types/ts-cjs-import-cjs-output/good.ts new file mode 100644 index 0000000..a17870e --- /dev/null +++ b/test-ts-types/ts-cjs-import-cjs-output/good.ts @@ -0,0 +1,16 @@ +import puppeteer = require('puppeteer'); + +async function run() { + const browser = await puppeteer.launch(); + const devices = puppeteer.devices; + console.log(devices); + const page = await browser.newPage(); + page.on('request', (request) => { + const resourceType = request.resourceType(); + }); + const div = (await page.$('div')) as puppeteer.ElementHandle< + HTMLAnchorElement + >; + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/ts-cjs-import-cjs-output/package.json b/test-ts-types/ts-cjs-import-cjs-output/package.json new file mode 100644 index 0000000..063237e --- /dev/null +++ b/test-ts-types/ts-cjs-import-cjs-output/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-ts-types-ts-esm", + "version": "1.0.0", + "private": true, + "description": "Test project with TypeScript, ESM output", + "scripts": { + "compile": "../../node_modules/.bin/tsc" + }, + "devDependencies": { + "typescript": "^4.1.3" + }, + "dependencies": { + "puppeteer": "file:../../puppeteer.tgz" + } +} diff --git a/test-ts-types/ts-cjs-import-cjs-output/tsconfig.json b/test-ts-types/ts-cjs-import-cjs-output/tsconfig.json new file mode 100644 index 0000000..a417e47 --- /dev/null +++ b/test-ts-types/ts-cjs-import-cjs-output/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "commonjs", + "strict": true, + "outDir": "dist", + "moduleResolution": "node" + }, + "files": ["good.ts", "bad.ts"] +} diff --git a/test-ts-types/ts-esm-import-cjs-output/bad.ts b/test-ts-types/ts-esm-import-cjs-output/bad.ts new file mode 100644 index 0000000..4aeb970 --- /dev/null +++ b/test-ts-types/ts-esm-import-cjs-output/bad.ts @@ -0,0 +1,18 @@ +// eslint-disable-next-line import/extensions +import * as puppeteer from 'puppeteer'; + +async function run() { + // Typo in "launch" + const browser = await puppeteer.launh(); + // Typo: "devices" + const devices = puppeteer.devics; + console.log(devices); + const browser2 = await puppeteer.launch(); + // 'foo' is invalid argument + const page = await browser2.newPage('foo'); + const div = (await page.$('div')) as puppeteer.ElementHandle< + HTMLAnchorElement + >; + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/ts-esm-import-cjs-output/good.ts b/test-ts-types/ts-esm-import-cjs-output/good.ts new file mode 100644 index 0000000..ed77641 --- /dev/null +++ b/test-ts-types/ts-esm-import-cjs-output/good.ts @@ -0,0 +1,13 @@ +// eslint-disable-next-line import/extensions +import * as puppeteer from 'puppeteer'; +import type { ElementHandle } from 'puppeteer'; + +async function run() { + const browser = await puppeteer.launch(); + const devices = puppeteer.devices; + console.log(devices); + const page = await browser.newPage(); + const div = (await page.$('div')) as ElementHandle; + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/ts-esm-import-cjs-output/package.json b/test-ts-types/ts-esm-import-cjs-output/package.json new file mode 100644 index 0000000..063237e --- /dev/null +++ b/test-ts-types/ts-esm-import-cjs-output/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-ts-types-ts-esm", + "version": "1.0.0", + "private": true, + "description": "Test project with TypeScript, ESM output", + "scripts": { + "compile": "../../node_modules/.bin/tsc" + }, + "devDependencies": { + "typescript": "^4.1.3" + }, + "dependencies": { + "puppeteer": "file:../../puppeteer.tgz" + } +} diff --git a/test-ts-types/ts-esm-import-cjs-output/tsconfig.json b/test-ts-types/ts-esm-import-cjs-output/tsconfig.json new file mode 100644 index 0000000..a417e47 --- /dev/null +++ b/test-ts-types/ts-esm-import-cjs-output/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "commonjs", + "strict": true, + "outDir": "dist", + "moduleResolution": "node" + }, + "files": ["good.ts", "bad.ts"] +} diff --git a/test-ts-types/ts-esm-import-esm-output/bad.ts b/test-ts-types/ts-esm-import-esm-output/bad.ts new file mode 100644 index 0000000..4aeb970 --- /dev/null +++ b/test-ts-types/ts-esm-import-esm-output/bad.ts @@ -0,0 +1,18 @@ +// eslint-disable-next-line import/extensions +import * as puppeteer from 'puppeteer'; + +async function run() { + // Typo in "launch" + const browser = await puppeteer.launh(); + // Typo: "devices" + const devices = puppeteer.devics; + console.log(devices); + const browser2 = await puppeteer.launch(); + // 'foo' is invalid argument + const page = await browser2.newPage('foo'); + const div = (await page.$('div')) as puppeteer.ElementHandle< + HTMLAnchorElement + >; + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/ts-esm-import-esm-output/good.ts b/test-ts-types/ts-esm-import-esm-output/good.ts new file mode 100644 index 0000000..ed77641 --- /dev/null +++ b/test-ts-types/ts-esm-import-esm-output/good.ts @@ -0,0 +1,13 @@ +// eslint-disable-next-line import/extensions +import * as puppeteer from 'puppeteer'; +import type { ElementHandle } from 'puppeteer'; + +async function run() { + const browser = await puppeteer.launch(); + const devices = puppeteer.devices; + console.log(devices); + const page = await browser.newPage(); + const div = (await page.$('div')) as ElementHandle; + console.log('got a div!', div); +} +run(); diff --git a/test-ts-types/ts-esm-import-esm-output/package.json b/test-ts-types/ts-esm-import-esm-output/package.json new file mode 100644 index 0000000..063237e --- /dev/null +++ b/test-ts-types/ts-esm-import-esm-output/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-ts-types-ts-esm", + "version": "1.0.0", + "private": true, + "description": "Test project with TypeScript, ESM output", + "scripts": { + "compile": "../../node_modules/.bin/tsc" + }, + "devDependencies": { + "typescript": "^4.1.3" + }, + "dependencies": { + "puppeteer": "file:../../puppeteer.tgz" + } +} diff --git a/test-ts-types/ts-esm-import-esm-output/tsconfig.json b/test-ts-types/ts-esm-import-esm-output/tsconfig.json new file mode 100644 index 0000000..f9e9bb5 --- /dev/null +++ b/test-ts-types/ts-esm-import-esm-output/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "esnext", + "strict": true, + "outDir": "dist", + "moduleResolution": "node" + }, + "files": ["good.ts", "bad.ts"] +} diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 0000000..9d86da2 --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + rules: { + 'no-restricted-imports': [ + 'error', + { + /** The mocha tests run on the compiled output in the /lib directory + * so we should avoid importing from src. + */ + patterns: ['*src*'], + }, + ], + }, +}; diff --git a/test/CDPSession.spec.ts b/test/CDPSession.spec.ts new file mode 100644 index 0000000..2ebf10f --- /dev/null +++ b/test/CDPSession.spec.ts @@ -0,0 +1,106 @@ +/** + * 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 { waitEvent } from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('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(() => globalThis.foo); + expect(foo).toBe('bar'); + }); + it('should send events', async () => { + const { page, server } = getTestState(); + + const client = await page.target().createCDPSession(); + await client.send('Network.enable'); + const events = []; + client.on('Network.requestWillBeSent', (event) => events.push(event)); + await page.goto(server.EMPTY_PAGE); + expect(events.length).toBe(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 = null; + try { + await client.send('Runtime.evaluate', { + expression: '3 + 1', + returnByValue: true, + }); + } catch (error_) { + error = 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) => 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'); + } + }); +}); diff --git a/test/EventEmitter.spec.ts b/test/EventEmitter.spec.ts new file mode 100644 index 0000000..bf20e7f --- /dev/null +++ b/test/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 { EventEmitter } from '../lib/cjs/puppeteer/common/EventEmitter.js'; +import sinon from 'sinon'; +import expect from 'expect'; + +describe('EventEmitter', () => { + let emitter; + + 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/test/README.md b/test/README.md new file mode 100644 index 0000000..9c2f536 --- /dev/null +++ b/test/README.md @@ -0,0 +1,87 @@ +# Puppeteer unit tests + +Unit tests in Puppeteer are written using [Mocha] as the test runner and [Expect] as the assertions library. + +## Test state + +We have some common setup that runs before each test and is defined in `mocha-utils.js`. + +You can use the `getTestState` function to read state. It exposes the following that you can use in your tests. These will be reset/tidied between tests automatically for you: + +- `puppeteer`: an instance of the Puppeteer library. This is exactly what you'd get if you ran `require('puppeteer')`. +- `puppeteerPath`: the path to the root source file for Puppeteer. +- `defaultBrowserOptions`: the default options the Puppeteer browser is launched from in test mode, so tests can use them and override if required. +- `server`: a dummy test server instance (see `utils/testserver` for more). +- `httpsServer`: a dummy test server HTTPS instance (see `utils/testserver` for more). +- `isFirefox`: true if running in Firefox. +- `isChrome`: true if running Chromium. +- `isHeadless`: true if the test is in headless mode. + +If your test needs a browser instance, you can use the `setupTestBrowserHooks()` function which will automatically configure a browser that will be cleaned between each test suite run. You access this via `getTestState()`. + +If your test needs a Puppeteer page and context, you can use the `setupTestPageAndContextHooks()` function which will configure these. You can access `page` and `context` from `getTestState()` once you have done this. + +The best place to look is an existing test to see how they use the helpers. + +## Skipping tests in specific conditions + +Tests that are not expected to pass in Firefox can be skipped. You can skip an individual test by using `itFailsFirefox` rather than `it`. Similarly you can skip a describe block with `describeFailsFirefox`. + +There is also `describeChromeOnly` and `itChromeOnly` which will only execute the test if running in Chromium. Note that this is different from `describeFailsFirefox`: the goal is to get any `FailsFirefox` calls passing in Firefox, whereas `describeChromeOnly` should be used to test behaviour that will only ever apply in Chromium. + +There are also tests that assume a normal install flow, with browser binaries ending up in `.local-`, for example. Such tests are skipped with +`itOnlyRegularInstall` which checks `BINARY` and `PUPPETEER_ALT_INSTALL` environment variables. + +[mocha]: https://mochajs.org/ +[expect]: https://www.npmjs.com/package/expect + +## Running tests + +Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected. + +- To run all tests: + +```bash +npm run unit +``` + +- **Important**: don't forget to first run TypeScript if you're testing local changes: + +```bash +npm run tsc && npm run unit +``` + +- To run a specific test, substitute the `it` with `it.only`: + +```js + ... + it.only('should work', async function() { + const {server, page} = getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '_cross it_'): + +```js + ... + // Using "xit" to skip specific test + xit('should work', async function({server, page}) { + const {server, page} = getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To run tests in non-headless mode: + +```bash +HEADLESS=false npm run unit +``` + +- To run tests with custom browser executable: + +```bash +BINARY= npm run unit +``` diff --git a/test/accessibility.spec.ts b/test/accessibility.spec.ts new file mode 100644 index 0000000..605915b --- /dev/null +++ b/test/accessibility.spec.ts @@ -0,0 +1,520 @@ +/** + * 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, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeFailsFirefox('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: 'WebArea', + name: 'Accessibility Test', + children: [ + { role: 'text', 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', + 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: 'text', + name: 'hi', + }, + ], + }, + ], + }; + expect( + findFocusedNode( + await page.accessibility.snapshot({ interestingOnly: false }) + ) + ).toEqual(golden); + }); + it('roledescription', async () => { + const { page } = getTestState(); + + await page.setContent( + '
Hi
' + ); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].roledescription).toEqual('foo'); + }); + it('orientation', async () => { + const { page } = getTestState(); + + await page.setContent( + '11' + ); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].orientation).toEqual('vertical'); + }); + it('autocomplete', async () => { + const { page } = getTestState(); + + await page.setContent(''); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].autocomplete).toEqual('list'); + }); + it('multiselectable', async () => { + const { page } = getTestState(); + + await page.setContent( + '
hey
' + ); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].multiselectable).toEqual(true); + }); + it('keyshortcuts', async () => { + const { page } = getTestState(); + + await page.setContent( + '
hey
' + ); + const snapshot = await page.accessibility.snapshot(); + 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: 'WebArea', + 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: 'text', + name: 'my fake image', + }, + ], + } + : { + role: 'generic', + name: '', + value: 'Edit this image: ', + children: [ + { + role: 'text', + name: 'Edit this image:', + }, + { + role: 'img', + name: 'my fake image', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + 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 +
`); + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'Edit this image: my fake image', + children: [ + { + role: 'text', + name: 'my fake image', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'Edit this image: ', + children: [ + { + role: 'text', + name: 'Edit this image:', + }, + { + role: 'img', + name: 'my fake image', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual(golden); + }); + + // Firefox does not support contenteditable="plaintext-only". + describeFailsFirefox('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(); + expect(snapshot.children[0]).toEqual({ + role: 'textbox', + name: '', + value: 'Edit this image:', + }); + }); + it('plain text field without role should not have content', async () => { + const { page } = getTestState(); + + await page.setContent(` +
Edit this image:my fake image
`); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual({ + role: 'generic', + name: '', + }); + }); + it('plain text field with tabindex and without role should not have content', async () => { + const { page } = getTestState(); + + await page.setContent(` +
Edit this image:my fake image
`); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual({ + role: 'generic', + name: '', + }); + }); + }); + 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(); + 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(); + 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(); + 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' }, + ], + }); + }); + 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) => 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: 'text', name: 'My Button' }], + }, + ], + }); + }); + }); + }); + function findFocusedNode(node) { + if (node.focused) return node; + for (const child of node.children || []) { + const focusedChild = findFocusedNode(child); + if (focusedChild) return focusedChild; + } + return null; + } +}); diff --git a/test/ariaqueryhandler.spec.ts b/test/ariaqueryhandler.spec.ts new file mode 100644 index 0000000..fcc3eef --- /dev/null +++ b/test/ariaqueryhandler.spec.ts @@ -0,0 +1,594 @@ +/** + * 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; +import utils from './utils.js'; + +describeChromeOnly('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) => { + const id = await button.evaluate((button: Element) => 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/[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/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"]'); + const id = await button.evaluate((button: Element) => 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"]'); + const id = await button.evaluate((button: Element) => 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'); + const id = await div.evaluate((div: Element) => 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'); + const id = await menu.evaluate((div: Element) => 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'); + const id = await menu.evaluate((div: Element) => 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'); + const ids = await Promise.all( + divs.map((n) => n.evaluate((div: Element) => div.id)) + ); + expect(ids.join(', ')).toBe('mnu1, mnu2'); + }); + }); + describe('queryAllArray', () => { + it('$$eval should handle many elements', async () => { + 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) => + buttons.reduce((acc, button) => acc + Number(button.textContent), 0) + ); + expect(sum).toBe(50005000); + }); + }); + + describe('waitForSelector (aria)', function () { + const addElement = (tag) => + 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 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, b) => 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(() => delete window.MutationObserver); + const [handle] = await Promise.all([ + page.waitForSelector('aria/anything'), + page.setContent(`

anything

`), + ]); + expect( + await page.evaluate((x: HTMLElement) => 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 elementHandle + .getProperty('tagName') + .then((element) => element.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( + () => + (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 utils.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.executionContext().frame()).toBe(page.mainFrame()); + }); + + it('should run in specified frame', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.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.executionContext().frame()).toBe(frame2); + }); + + it('should throw when frame is detached', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError = null; + const waitPromise = frame + .waitForSelector('aria/does-not-exist') + .catch((error) => (waitError = error)); + await utils.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(() => (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(() => (divFound = true)); + await page.setContent( + `
1
` + ); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divFound).toBe(false); + await page.evaluate(() => + 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(() => (divVisible = true)); + await page.setContent( + `
hi
` + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + 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( + `
` + ); + const waitForSelector = page + .waitForSelector('aria/[role="button"]', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('aria/[role="button"]'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + 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(`
`); + const waitForSelector = page + .waitForSelector('aria/[role="main"]', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('aria/[role="main"]'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + 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(`
`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('aria/[role="main"]', { hidden: true }) + .then(() => (divRemoved = true)); + await page.waitForSelector('aria/[role="main"]'); // do a round trip + expect(divRemoved).toBe(false); + await page.evaluate(() => 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, puppeteer } = getTestState(); + + let error = null; + await page + .waitForSelector('aria/[role="button"]', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `[role="button"]` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const { page } = getTestState(); + + await page.setContent(`
`); + let error = null; + await page + .waitForSelector('aria/[role="main"]', { hidden: true, timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `[role="main"]` to be hidden failed: timeout' + ); + }); + + it('should respond to node attribute mutation', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('aria/zombo') + .then(() => (divFound = true)); + await page.setContent(`
`); + expect(divFound).toBe(false); + await page.evaluate(() => + 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: HTMLElement) => x.textContent, + await waitForSelector + ) + ).toBe('anything'); + }); + + it('should have correct stack trace for timeout', async () => { + const { page } = getTestState(); + + let error; + await page + .waitForSelector('aria/zombo', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error.stack).toContain('waiting for selector `zombo` failed'); + }); + }); + + 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[]) => + Promise.all( + elements.map((element) => + element.evaluate((element: Element) => 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"]'); + const ids = await getIds(found); + expect(ids).toEqual(['node5', 'node6', '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', 'node11', 'node13']); + }); + it('should not find ignored', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/title'); + const ids = await getIds(found); + expect(ids).toEqual(['shown']); + }); + }); +}); diff --git a/test/assert-coverage-test.js b/test/assert-coverage-test.js new file mode 100644 index 0000000..6e26a11 --- /dev/null +++ b/test/assert-coverage-test.js @@ -0,0 +1,25 @@ +const { describe, it } = require('mocha'); +const { getCoverageResults } = require('./coverage-utils'); +const expect = require('expect'); + +describe('API coverage test', () => { + it('calls every method', () => { + if (!process.env.COVERAGE) return; + + const coverageMap = getCoverageResults(); + const missingMethods = []; + for (const method of coverageMap.keys()) { + if (!coverageMap.get(method)) missingMethods.push(method); + } + if (missingMethods.length) { + console.error( + '\nCoverage check failed: not all API methods called. See above output for list of missing methods.' + ); + console.error(missingMethods.join('\n')); + } + + // We know this will fail because we checked above + // but we need the actual test to fail. + expect(missingMethods.length).toEqual(0); + }); +}); diff --git a/test/assets/beforeunload.html b/test/assets/beforeunload.html new file mode 100644 index 0000000..3cef676 --- /dev/null +++ b/test/assets/beforeunload.html @@ -0,0 +1,10 @@ +
beforeunload demo.
+ + diff --git a/test/assets/cached/one-style-font.css b/test/assets/cached/one-style-font.css new file mode 100644 index 0000000..6178de0 --- /dev/null +++ b/test/assets/cached/one-style-font.css @@ -0,0 +1,9 @@ +@font-face { + font-family: 'one-style'; + src: url('./one-style.woff') format('woff'); +} + +body { + background-color: pink; + font-family: 'one-style', sans-serif; +} diff --git a/test/assets/cached/one-style-font.html b/test/assets/cached/one-style-font.html new file mode 100644 index 0000000..8e7236d --- /dev/null +++ b/test/assets/cached/one-style-font.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/assets/cached/one-style.css b/test/assets/cached/one-style.css new file mode 100644 index 0000000..04e7110 --- /dev/null +++ b/test/assets/cached/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/cached/one-style.html b/test/assets/cached/one-style.html new file mode 100644 index 0000000..4760f2b --- /dev/null +++ b/test/assets/cached/one-style.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/assets/chromium-linux.zip b/test/assets/chromium-linux.zip new file mode 100644 index 0000000000000000000000000000000000000000..9c00ec080d0e9ef688dca5442a75f1095c02f0e3 GIT binary patch literal 325 zcmWIWW@h1H0D;A=Za!cJl;C9$U`Wm=%Fj*J&B@FwtZxSQYgUh!>sg5Doxp z6=C22LUgUd=vpD>fDCwqWWeW-U$}b%3O;=)=IMIE#1P=k&SB2ywUr;}0A3&t@MdHZ zWya+=9++Dl-a3MqXinvTI~8IK!lB4k2s0pzS<+~YZVb%X9B^j`c(byBY-0k#DL{HQ Hh{FH?_oYdx literal 0 HcmV?d00001 diff --git a/test/assets/consolelog.html b/test/assets/consolelog.html new file mode 100644 index 0000000..4a27803 --- /dev/null +++ b/test/assets/consolelog.html @@ -0,0 +1,17 @@ + + + + console.log test + + + + + diff --git a/test/assets/csp.html b/test/assets/csp.html new file mode 100644 index 0000000..34fc1fc --- /dev/null +++ b/test/assets/csp.html @@ -0,0 +1 @@ + diff --git a/test/assets/csscoverage/Dosis-Regular.ttf b/test/assets/csscoverage/Dosis-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4b208624e8c69294152da06af90f878cb91e05e9 GIT binary patch literal 136940 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 O8fzM)coqDmlKl&Wz|SiH literal 0 HcmV?d00001 diff --git a/test/assets/csscoverage/OFL.txt b/test/assets/csscoverage/OFL.txt new file mode 100644 index 0000000..a9b3c8b --- /dev/null +++ b/test/assets/csscoverage/OFL.txt @@ -0,0 +1,95 @@ +Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com), +Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com), +with Reserved Font Names "Dosis". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/test/assets/csscoverage/involved.html b/test/assets/csscoverage/involved.html new file mode 100644 index 0000000..bcd9845 --- /dev/null +++ b/test/assets/csscoverage/involved.html @@ -0,0 +1,26 @@ + +
woof!
+fancy text + diff --git a/test/assets/csscoverage/media.html b/test/assets/csscoverage/media.html new file mode 100644 index 0000000..bfb89f8 --- /dev/null +++ b/test/assets/csscoverage/media.html @@ -0,0 +1,4 @@ + +
hello, world
+ diff --git a/test/assets/csscoverage/multiple.html b/test/assets/csscoverage/multiple.html new file mode 100644 index 0000000..0fd97e9 --- /dev/null +++ b/test/assets/csscoverage/multiple.html @@ -0,0 +1,8 @@ + + + diff --git a/test/assets/csscoverage/simple.html b/test/assets/csscoverage/simple.html new file mode 100644 index 0000000..3beae21 --- /dev/null +++ b/test/assets/csscoverage/simple.html @@ -0,0 +1,6 @@ + +
hello, world
+ diff --git a/test/assets/csscoverage/sourceurl.html b/test/assets/csscoverage/sourceurl.html new file mode 100644 index 0000000..df4e9c2 --- /dev/null +++ b/test/assets/csscoverage/sourceurl.html @@ -0,0 +1,7 @@ + + diff --git a/test/assets/csscoverage/stylesheet1.css b/test/assets/csscoverage/stylesheet1.css new file mode 100644 index 0000000..60f1eab --- /dev/null +++ b/test/assets/csscoverage/stylesheet1.css @@ -0,0 +1,3 @@ +body { + color: red; +} diff --git a/test/assets/csscoverage/stylesheet2.css b/test/assets/csscoverage/stylesheet2.css new file mode 100644 index 0000000..a87defb --- /dev/null +++ b/test/assets/csscoverage/stylesheet2.css @@ -0,0 +1,4 @@ +html { + margin: 0; + padding: 0; +} diff --git a/test/assets/csscoverage/unused.html b/test/assets/csscoverage/unused.html new file mode 100644 index 0000000..5b8186a --- /dev/null +++ b/test/assets/csscoverage/unused.html @@ -0,0 +1,7 @@ + + diff --git a/test/assets/detect-touch.html b/test/assets/detect-touch.html new file mode 100644 index 0000000..80a4123 --- /dev/null +++ b/test/assets/detect-touch.html @@ -0,0 +1,12 @@ + + + + Detect Touch Test + + + + + + diff --git a/test/assets/digits/0.png b/test/assets/digits/0.png new file mode 100644 index 0000000000000000000000000000000000000000..ac3c4768edfbe7bd47c436b1451938fa83483a0c GIT binary patch literal 434 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr+(3y?gqul)d!Vo7)Ob!1@J*w6hZkrl{SNcITw zWnidMV_;}#VPN<`%l_%+p4TGxUM+vVxv7$R|b>IB0qra%Ul`~MX*b+&Ovv2B|q(!_F5JEB7P zed0v5sZ%E?Yh1hbjAP{`Rxy#BDWWlty*FwfV(a#?G%)Vx{1iU1`q`mv2gSp$&p2~* z-}_~cq6GO$WM?Q#u2eqHG5>*&;LICE?MK)yaHod0^GXWrZw)prU@KA-JFv3vi--cJ zxiY6feZzXKeM^rjA81T>7dtU=-Ac=*J)Oz+GF_*I;`H`Ro8h-tLFm*jTUKAWRU1F- zfBotg>*8DSc8V=VKyNdsmbgZgq$HN4S|t~y0x1R~14DCN12bJivk*g5D-%O2V@q8F mb1MUbn=hryP&DM`r(~v8;?}TY%i>c&4Gf;HelF{r5}E+JGmpjq literal 0 HcmV?d00001 diff --git a/test/assets/digits/1.png b/test/assets/digits/1.png new file mode 100644 index 0000000000000000000000000000000000000000..6768222729b7a487d338fdd7691e44ca4604b216 GIT binary patch literal 346 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr+(3y?gqul)d!Vo7)Ob!1@J*w6hZkrl{SNcITw zWnidMV_;}#VPN<`%l_%+p4TGxUMOY(Ga43W5;{O5cF=M0GtoN~YQ|NXb^`8@xj{o(qyZXTZh zNB`|_{xm~E;-A5pf9w0RzSOs614Wzvrz@>Y`EmY3PvY_a|9PY*vP4Qqs7R_vN=noT zzEz3h*rdQ|G)c~jg_Tj2N2{sHz90fFVdQ&MBb@0M_4YL;wH) literal 0 HcmV?d00001 diff --git a/test/assets/digits/2.png b/test/assets/digits/2.png new file mode 100644 index 0000000000000000000000000000000000000000..b1daa4735d8a8c8dcce6be0fc2db02beb265860b GIT binary patch literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr+(3y?gqul)d!Vo7)Ob!1@J*w6hZkrl{SNcITw zWnidMV_;}#VPN<`%l_%$`QSZ$aB6k6)(;us=vdFljxE~Y{OxBK}UC0Lr@F!n?rT`(_E{}JO6 z<{7z73T*rf7DVR9aWl3E8iRZQbg$2f$a-VDO8#vl&KeD#DWG<1jqwYkJ(8lLS zB4RWT1dM<5ebWu_3N}u7Cea|(Ea3RP_Gw+vkLTX@hooODEC@-v@oNsy!Kx*$5hW>! zC8<`)MX5lF!N|bST-U%%*U&7)(A3Jr(8|O>*TCG$z`$)w_#PAux%nxXX_dG&n7@1v QRL{WR>FVdQ&MBb@0Eyv<`%l_%$`QSZ$aB6x!wK;us=vdFq6XS%(-TTK3zgoV~U1u8i>$)?1-gCGWI< z@aS2U3)ZwcEm*ubc;$ti-mV3rF6`=#s{>A4SDeUI_Oe&LoV{Fey4Mfsycbe!vf}Dh z3k0h9j%Z)+Fc6$CIUO_QmvAUQh^kMk%6JPu7R1Zp;?Hbsg;SLm5G6_ nfw`4|f!mhwJt!J-^HVa@DsgKtfB7D$o`J#B)z4*}Q$iB}nZcB~ literal 0 HcmV?d00001 diff --git a/test/assets/digits/4.png b/test/assets/digits/4.png new file mode 100644 index 0000000000000000000000000000000000000000..a721071e2cc4f4d3aeb9a939fb353bee19a655b5 GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr+(3y?gqul)d!Vo7)Ob!1@J*w6hZkrl{SNcITw zWnidMV_;}#VPN<`%l_%$^T+cq!(g=TxYIEF}Eo;tyhk1Daqt63f# z{2;4qbecI;$g&|gJoK~I{dMMIqQ-q^Z?t=`WG!sjc4tSKLf;*&Y1X25Kle?TG^sF6 z>D=BYXVM%qnde@pu;0vgB#G~T{mbP)S=PRcF1)|-gago-swJ)wB`Jv|saDBFsX&Us z$iUEC*T78I&@9Bz)XKyVh;$9itqcqv9FE~Y(U6;;l9^VCTf@Y8PqqLxFnGH9xvX
<`%l_%$^T+cq!(h1PkxIEF}E?wz22-my@kZT1K|fI9blJ2l{+r3^|)} z*8RfYYV*z8_c%_x;UgqEd!^9Poh`*TW`=*TdCT>^*4BT*|H;1(oM7&m8^G|u$3Da% zDeBX$-lmw9w+s(E_C1K%nffs5$fStkT(0({CkN(r>}Jt>Q{|STYPMH+)2ATwXX;N9 zUY^;$J4woIp1{qfw?{sQ>->K1(PN|HYUInB{Z4ScYq_3-VJ}bo{&&hX4DrV0Symw> zhk!0uEpd$~Nl7e8wMs5Z1yT$~28QOk24=d3W+8^ARwjl(q-$VqWnl2&a0~~EhTQy= Z%(P0}8Ya$rvIVGt!PC{xWt~$(69C)6lY0OF literal 0 HcmV?d00001 diff --git a/test/assets/digits/6.png b/test/assets/digits/6.png new file mode 100644 index 0000000000000000000000000000000000000000..639f38439d94e856e41a6750952b6e06c418cf3f GIT binary patch literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr+(3y?gqul)d!Vo7)Ob!1@J*w6hZkrl{SNcITw zWnidMV_;}#VPN<`%l_%*enOfK#K3LW)yaSV~TJavMh*Wm;PxBK(YEu72bwVdZ}kbt`XgDKWG zHhQxkJbRS+g$`@}15O#2T~i(@MY241a<;z7)g=_)Zn4zjOK<*{ z^|H0Og+AWm-uT?SDW)^nHSdUci~p}UEv0yy-ENEcy{zt3Kiarmu2tfJsEduUXmP#3 zy#q&>em;$vqY=pZB*h^?VgbV<=ODgW9}Ojw!x+xA)XDx=DXab9=X3YU$4?nbk5(4D zIQzm0Xkxq!^4049#^7%ybRS xLJUo5~f0{rI44$rjF6*2UngAmjmB9c2 literal 0 HcmV?d00001 diff --git a/test/assets/digits/7.png b/test/assets/digits/7.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1150b005a9fe3c2b970617dcb8801d98408fe7 GIT binary patch literal 387 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr+(3y?gqul)d!Vo7)Ob!1@J*w6hZkrl{SNcITw zWnidMV_;}#VPN<`%l_%*enOfK#K3UzzBIEF}E?!B;(m&sAU?cw&>o3As9E-=s#a$@F?F}=VL zyP)8Tqo9h03QMC?e9=i(rpg_=V;%1F{Qf6sEc|5qBl*P@8;rI@9-ZW>@#KW&%vnq~ zzOl}^kHZ9KZ=-wR{MdtU&MofI@cqgkb+dDrc!nov}_HpAKo1^uZt{iyr``c~- z=k|r)r~i7)zL771W73?7Q-RJ>Epd$~Nl7e8wMs5Z1yT$~28QOk24=d3W+8^ARwjm4 pCPumj=2iv<7q#6`HRR@}WTsW(*07-LpC-@-22WQ%mvv4FO#tK!e`o*z literal 0 HcmV?d00001 diff --git a/test/assets/digits/8.png b/test/assets/digits/8.png new file mode 100644 index 0000000000000000000000000000000000000000..abb8b48b0b1e5ac8aabb667265d9469d48b7fa24 GIT binary patch literal 447 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr+(3y?gqul)d!Vo7)Ob!1@J*w6hZkrl{SNcITw zWnidMV_;}#VPN<`%l_%*e4jUVa(g^qi=IEF}Eo;pF-zbR3qZT<755iFO44ID4;ZXsr|G6Z-3AQ~YMF#9yB;s?AMz zKS@rU5cTWfbfKwA)i>7f%FSiSf0jRY%Pf;;KrgA5xJHzuB$lLFB^RXvDF!10LvvjN zGhIWo5JOWd6GJN#V_gGtD+7b_7+*yc4Y~O#nQ4`{HLx*$oeR{!;OXk;vd$@?2>`+@ BpQr!; literal 0 HcmV?d00001 diff --git a/test/assets/digits/9.png b/test/assets/digits/9.png new file mode 100644 index 0000000000000000000000000000000000000000..6a40a21c6f58545cab61ea346f1b6bb8b9300c6d GIT binary patch literal 437 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr+(3y?gqul)d!Vo7)Ob!1@J*w6hZkrl{SNcITw zWnidMV_;}#VPN<`%l_%*e4jUVa(h4y;7IEF}Eo;pD>k10`t_4)k-!5MN3m>MJ651DFC-ODU@ zpZg1!gh!athOH(nj7nKY-017|7X(lneSP5`X2ea_zN%F?Q`q4 z_N=>MvQe>5^dWanr4!!~yIt-xI*MIYJ|5&1TI#UWB`1>;t5@F(qUrTrX|{*Mm$Cl}5-eYz`MaCM!?KE?X)(klhI z7Jr!kGOw6X)u&2!`$KOxphr|oTq8 +window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost'; + url.pathname = '/grid.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); +}, false); + diff --git a/test/assets/empty.html b/test/assets/empty.html new file mode 100644 index 0000000..e69de29 diff --git a/test/assets/error.html b/test/assets/error.html new file mode 100644 index 0000000..130400c --- /dev/null +++ b/test/assets/error.html @@ -0,0 +1,15 @@ + diff --git a/test/assets/es6/.eslintrc b/test/assets/es6/.eslintrc new file mode 100644 index 0000000..1903e17 --- /dev/null +++ b/test/assets/es6/.eslintrc @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "sourceType": "module" + } +} \ No newline at end of file diff --git a/test/assets/es6/es6import.js b/test/assets/es6/es6import.js new file mode 100644 index 0000000..9aac2d4 --- /dev/null +++ b/test/assets/es6/es6import.js @@ -0,0 +1,2 @@ +import num from './es6module.js'; +window.__es6injected = num; diff --git a/test/assets/es6/es6module.js b/test/assets/es6/es6module.js new file mode 100644 index 0000000..7a4e8a7 --- /dev/null +++ b/test/assets/es6/es6module.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/assets/es6/es6pathimport.js b/test/assets/es6/es6pathimport.js new file mode 100644 index 0000000..eb17a9a --- /dev/null +++ b/test/assets/es6/es6pathimport.js @@ -0,0 +1,2 @@ +import num from './es6/es6module.js'; +window.__es6injected = num; diff --git a/test/assets/favicon.ico b/test/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d4edd507993e83c78dba94ca415c6ee42793bd77 GIT binary patch literal 70 ncmZQzU<5%%1|TWHV8Fn@AO^%5KnxUOU;@(;KoS@D50(G`CpiJ9 literal 0 HcmV?d00001 diff --git a/test/assets/file-to-upload.txt b/test/assets/file-to-upload.txt new file mode 100644 index 0000000..b4ad118 --- /dev/null +++ b/test/assets/file-to-upload.txt @@ -0,0 +1 @@ +contents of the file \ No newline at end of file diff --git a/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 b/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..be6d1880276d5efa4b65beaf1c364585a89356f9 GIT binary patch literal 211 zcmV;^04)DPT4*^jL0KkKS^PvgW&i-Ze~{9d0DwRM|9~?kvS4pwp6~*+gqZ)>q7y=pwfNe)&bw4*ux`+}2JqZA%V*vC( zFhVj4{HSRG2#P;0j7;1K7p{Qj48j}WqXH8YymE-^g+#`9Uu1Pelwwb676jyCL_#V+ zpf^utwby0I-VFJ#T<1xWj==bG3N0bVqa5Mjoel@VQcODrBI8%l + + +
Hi, I'm frame
diff --git a/test/assets/frames/frameset.html b/test/assets/frames/frameset.html new file mode 100644 index 0000000..4d56f88 --- /dev/null +++ b/test/assets/frames/frameset.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/assets/frames/nested-frames.html b/test/assets/frames/nested-frames.html new file mode 100644 index 0000000..de19875 --- /dev/null +++ b/test/assets/frames/nested-frames.html @@ -0,0 +1,25 @@ + + + + diff --git a/test/assets/frames/one-frame-url-fragment.html b/test/assets/frames/one-frame-url-fragment.html new file mode 100644 index 0000000..d146264 --- /dev/null +++ b/test/assets/frames/one-frame-url-fragment.html @@ -0,0 +1 @@ + diff --git a/test/assets/frames/one-frame.html b/test/assets/frames/one-frame.html new file mode 100644 index 0000000..e941d79 --- /dev/null +++ b/test/assets/frames/one-frame.html @@ -0,0 +1 @@ + diff --git a/test/assets/frames/script.js b/test/assets/frames/script.js new file mode 100644 index 0000000..be22256 --- /dev/null +++ b/test/assets/frames/script.js @@ -0,0 +1 @@ +console.log('Cheers!'); diff --git a/test/assets/frames/style.css b/test/assets/frames/style.css new file mode 100644 index 0000000..5b5436e --- /dev/null +++ b/test/assets/frames/style.css @@ -0,0 +1,3 @@ +div { + color: blue; +} diff --git a/test/assets/frames/two-frames.html b/test/assets/frames/two-frames.html new file mode 100644 index 0000000..b2ee853 --- /dev/null +++ b/test/assets/frames/two-frames.html @@ -0,0 +1,13 @@ + + + diff --git a/test/assets/global-var.html b/test/assets/global-var.html new file mode 100644 index 0000000..b6be975 --- /dev/null +++ b/test/assets/global-var.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/assets/grid.html b/test/assets/grid.html new file mode 100644 index 0000000..0bdbb12 --- /dev/null +++ b/test/assets/grid.html @@ -0,0 +1,52 @@ + + + diff --git a/test/assets/historyapi.html b/test/assets/historyapi.html new file mode 100644 index 0000000..bacaf9e --- /dev/null +++ b/test/assets/historyapi.html @@ -0,0 +1,5 @@ + diff --git a/test/assets/idle-detector.html b/test/assets/idle-detector.html new file mode 100644 index 0000000..83b496c --- /dev/null +++ b/test/assets/idle-detector.html @@ -0,0 +1,23 @@ + +
+ diff --git a/test/assets/injectedfile.js b/test/assets/injectedfile.js new file mode 100644 index 0000000..c211b62 --- /dev/null +++ b/test/assets/injectedfile.js @@ -0,0 +1,2 @@ +window.__injected = 42; +window.__injectedError = new Error('hi'); diff --git a/test/assets/injectedstyle.css b/test/assets/injectedstyle.css new file mode 100644 index 0000000..aa1634c --- /dev/null +++ b/test/assets/injectedstyle.css @@ -0,0 +1,3 @@ +body { + background-color: red; +} diff --git a/test/assets/input/button.html b/test/assets/input/button.html new file mode 100644 index 0000000..d4c6e13 --- /dev/null +++ b/test/assets/input/button.html @@ -0,0 +1,16 @@ + + + + Button test + + + + + + + \ No newline at end of file diff --git a/test/assets/input/checkbox.html b/test/assets/input/checkbox.html new file mode 100644 index 0000000..ca56762 --- /dev/null +++ b/test/assets/input/checkbox.html @@ -0,0 +1,42 @@ + + + + Selection Test + + + + + + + diff --git a/test/assets/input/fileupload.html b/test/assets/input/fileupload.html new file mode 100644 index 0000000..55fd7c5 --- /dev/null +++ b/test/assets/input/fileupload.html @@ -0,0 +1,9 @@ + + + + File upload test + + + + + \ No newline at end of file diff --git a/test/assets/input/keyboard.html b/test/assets/input/keyboard.html new file mode 100644 index 0000000..fd962c7 --- /dev/null +++ b/test/assets/input/keyboard.html @@ -0,0 +1,42 @@ + + + + Keyboard test + + + + + + \ No newline at end of file diff --git a/test/assets/input/mouse-helper.js b/test/assets/input/mouse-helper.js new file mode 100644 index 0000000..4f2824d --- /dev/null +++ b/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/test/assets/input/rotatedButton.html b/test/assets/input/rotatedButton.html new file mode 100644 index 0000000..1bce66c --- /dev/null +++ b/test/assets/input/rotatedButton.html @@ -0,0 +1,21 @@ + + + + Rotated button test + + + + + + + + diff --git a/test/assets/input/scrollable.html b/test/assets/input/scrollable.html new file mode 100644 index 0000000..885d373 --- /dev/null +++ b/test/assets/input/scrollable.html @@ -0,0 +1,23 @@ + + + + Scrollable test + + + + + + \ No newline at end of file diff --git a/test/assets/input/select.html b/test/assets/input/select.html new file mode 100644 index 0000000..879a537 --- /dev/null +++ b/test/assets/input/select.html @@ -0,0 +1,69 @@ + + + + Selection Test + + + + + + diff --git a/test/assets/input/textarea.html b/test/assets/input/textarea.html new file mode 100644 index 0000000..6d77f31 --- /dev/null +++ b/test/assets/input/textarea.html @@ -0,0 +1,15 @@ + + + + Textarea test + + + + + + + diff --git a/test/assets/input/touches.html b/test/assets/input/touches.html new file mode 100644 index 0000000..4392cfa --- /dev/null +++ b/test/assets/input/touches.html @@ -0,0 +1,35 @@ + + + + Touch test + + + + + + + \ No newline at end of file diff --git a/test/assets/input/wheel.html b/test/assets/input/wheel.html new file mode 100644 index 0000000..3d093a9 --- /dev/null +++ b/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/test/assets/jscoverage/eval.html b/test/assets/jscoverage/eval.html new file mode 100644 index 0000000..838ae28 --- /dev/null +++ b/test/assets/jscoverage/eval.html @@ -0,0 +1 @@ + diff --git a/test/assets/jscoverage/involved.html b/test/assets/jscoverage/involved.html new file mode 100644 index 0000000..889c86b --- /dev/null +++ b/test/assets/jscoverage/involved.html @@ -0,0 +1,15 @@ + diff --git a/test/assets/jscoverage/multiple.html b/test/assets/jscoverage/multiple.html new file mode 100644 index 0000000..bdef598 --- /dev/null +++ b/test/assets/jscoverage/multiple.html @@ -0,0 +1,2 @@ + + diff --git a/test/assets/jscoverage/ranges.html b/test/assets/jscoverage/ranges.html new file mode 100644 index 0000000..a537a7d --- /dev/null +++ b/test/assets/jscoverage/ranges.html @@ -0,0 +1,2 @@ + diff --git a/test/assets/jscoverage/script1.js b/test/assets/jscoverage/script1.js new file mode 100644 index 0000000..3bd241b --- /dev/null +++ b/test/assets/jscoverage/script1.js @@ -0,0 +1 @@ +console.log(3); diff --git a/test/assets/jscoverage/script2.js b/test/assets/jscoverage/script2.js new file mode 100644 index 0000000..3bd241b --- /dev/null +++ b/test/assets/jscoverage/script2.js @@ -0,0 +1 @@ +console.log(3); diff --git a/test/assets/jscoverage/simple.html b/test/assets/jscoverage/simple.html new file mode 100644 index 0000000..49eeeea --- /dev/null +++ b/test/assets/jscoverage/simple.html @@ -0,0 +1,2 @@ + diff --git a/test/assets/jscoverage/sourceurl.html b/test/assets/jscoverage/sourceurl.html new file mode 100644 index 0000000..e477750 --- /dev/null +++ b/test/assets/jscoverage/sourceurl.html @@ -0,0 +1,4 @@ + diff --git a/test/assets/jscoverage/unused.html b/test/assets/jscoverage/unused.html new file mode 100644 index 0000000..59c4a5a --- /dev/null +++ b/test/assets/jscoverage/unused.html @@ -0,0 +1 @@ + diff --git a/test/assets/mobile.html b/test/assets/mobile.html new file mode 100644 index 0000000..8e94b2f --- /dev/null +++ b/test/assets/mobile.html @@ -0,0 +1 @@ + diff --git a/test/assets/modernizr.js b/test/assets/modernizr.js new file mode 100644 index 0000000..7991a4e --- /dev/null +++ b/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/test/assets/offscreenbuttons.html b/test/assets/offscreenbuttons.html new file mode 100644 index 0000000..005d465 --- /dev/null +++ b/test/assets/offscreenbuttons.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/test/assets/one-style.css b/test/assets/one-style.css new file mode 100644 index 0000000..7b26410 --- /dev/null +++ b/test/assets/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/one-style.html b/test/assets/one-style.html new file mode 100644 index 0000000..4760f2b --- /dev/null +++ b/test/assets/one-style.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/assets/playground.html b/test/assets/playground.html new file mode 100644 index 0000000..828cfb1 --- /dev/null +++ b/test/assets/playground.html @@ -0,0 +1,15 @@ + + + + Playground + + + + +
First div
+
+ Second div + Inner span +
+ + \ No newline at end of file diff --git a/test/assets/popup/popup.html b/test/assets/popup/popup.html new file mode 100644 index 0000000..b855162 --- /dev/null +++ b/test/assets/popup/popup.html @@ -0,0 +1,9 @@ + + + + Popup + + + I am a popup + + diff --git a/test/assets/popup/window-open.html b/test/assets/popup/window-open.html new file mode 100644 index 0000000..d138be1 --- /dev/null +++ b/test/assets/popup/window-open.html @@ -0,0 +1,11 @@ + + + + Popup test + + + + + diff --git a/test/assets/pptr.png b/test/assets/pptr.png new file mode 100644 index 0000000000000000000000000000000000000000..65d87c68e65902c058af18d2a595fc89f423f4ff GIT binary patch literal 6138 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 W{|p>bpZvrb0Q9tsH0#uzQ2zrM7gd%3 literal 0 HcmV?d00001 diff --git a/test/assets/resetcss.html b/test/assets/resetcss.html new file mode 100644 index 0000000..e4e04b1 --- /dev/null +++ b/test/assets/resetcss.html @@ -0,0 +1,50 @@ + diff --git a/test/assets/self-request.html b/test/assets/self-request.html new file mode 100644 index 0000000..88aff62 --- /dev/null +++ b/test/assets/self-request.html @@ -0,0 +1,5 @@ + diff --git a/test/assets/serviceworkers/empty/sw.html b/test/assets/serviceworkers/empty/sw.html new file mode 100644 index 0000000..bef85d9 --- /dev/null +++ b/test/assets/serviceworkers/empty/sw.html @@ -0,0 +1,3 @@ + diff --git a/test/assets/serviceworkers/empty/sw.js b/test/assets/serviceworkers/empty/sw.js new file mode 100644 index 0000000..e69de29 diff --git a/test/assets/serviceworkers/fetch/style.css b/test/assets/serviceworkers/fetch/style.css new file mode 100644 index 0000000..7b26410 --- /dev/null +++ b/test/assets/serviceworkers/fetch/style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/serviceworkers/fetch/sw.html b/test/assets/serviceworkers/fetch/sw.html new file mode 100644 index 0000000..a9d28ac --- /dev/null +++ b/test/assets/serviceworkers/fetch/sw.html @@ -0,0 +1,5 @@ + + diff --git a/test/assets/serviceworkers/fetch/sw.js b/test/assets/serviceworkers/fetch/sw.js new file mode 100644 index 0000000..2138148 --- /dev/null +++ b/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/test/assets/shadow.html b/test/assets/shadow.html new file mode 100644 index 0000000..3796ca7 --- /dev/null +++ b/test/assets/shadow.html @@ -0,0 +1,17 @@ + diff --git a/test/assets/simple-extension/content-script.js b/test/assets/simple-extension/content-script.js new file mode 100644 index 0000000..0fd83b9 --- /dev/null +++ b/test/assets/simple-extension/content-script.js @@ -0,0 +1,2 @@ +console.log('hey from the content-script'); +self.thisIsTheContentScript = true; diff --git a/test/assets/simple-extension/index.js b/test/assets/simple-extension/index.js new file mode 100644 index 0000000..a0bb3f4 --- /dev/null +++ b/test/assets/simple-extension/index.js @@ -0,0 +1,2 @@ +// Mock script for background extension +window.MAGIC = 42; diff --git a/test/assets/simple-extension/manifest.json b/test/assets/simple-extension/manifest.json new file mode 100644 index 0000000..da2cd08 --- /dev/null +++ b/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/test/assets/simple.json b/test/assets/simple.json new file mode 100644 index 0000000..6d95903 --- /dev/null +++ b/test/assets/simple.json @@ -0,0 +1 @@ +{"foo": "bar"} diff --git a/test/assets/tamperable.html b/test/assets/tamperable.html new file mode 100644 index 0000000..d027e97 --- /dev/null +++ b/test/assets/tamperable.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/assets/title.html b/test/assets/title.html new file mode 100644 index 0000000..88a86ce --- /dev/null +++ b/test/assets/title.html @@ -0,0 +1 @@ +Woof-Woof diff --git a/test/assets/worker/worker.html b/test/assets/worker/worker.html new file mode 100644 index 0000000..7de2d9f --- /dev/null +++ b/test/assets/worker/worker.html @@ -0,0 +1,14 @@ + + + + Worker test + + + + + \ No newline at end of file diff --git a/test/assets/worker/worker.js b/test/assets/worker/worker.js new file mode 100644 index 0000000..0626f13 --- /dev/null +++ b/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/test/assets/wrappedlink.html b/test/assets/wrappedlink.html new file mode 100644 index 0000000..429b6e9 --- /dev/null +++ b/test/assets/wrappedlink.html @@ -0,0 +1,32 @@ + + + diff --git a/test/browser.spec.ts b/test/browser.spec.ts new file mode 100644 index 0000000..0d06e7f --- /dev/null +++ b/test/browser.spec.ts @@ -0,0 +1,81 @@ +/** + * 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'; // eslint-disable-line import/extensions + +describe('Browser specs', function () { + setupTestBrowserHooks(); + + describe('Browser.version', function () { + it('should return whether we are in headless', async () => { + const { browser, isHeadless } = getTestState(); + + const version = await browser.version(); + expect(version.length).toBeGreaterThan(0); + expect(version.startsWith('Headless')).toBe(isHeadless); + }); + }); + + 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/test/browsercontext.spec.ts b/test/browsercontext.spec.ts new file mode 100644 index 0000000..88ff09b --- /dev/null +++ b/test/browsercontext.spec.ts @@ -0,0 +1,207 @@ +/** + * 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, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; + +describe('BrowserContext', function () { + setupTestBrowserHooks(); + it('should have default context', async () => { + const { browser } = getTestState(); + expect(browser.browserContexts().length).toEqual(1); + const defaultContext = browser.browserContexts()[0]; + expect(defaultContext.isIncognito()).toBe(false); + let error = null; + await defaultContext.close().catch((error_) => (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().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(context.isIncognito()).toBe(true); + expect(browser.browserContexts().length).toBe(2); + expect(browser.browserContexts().indexOf(context) !== -1).toBe(true); + await context.close(); + expect(browser.browserContexts().length).toBe(1); + }); + it('should close all belonging targets once closing context', async () => { + const { browser } = getTestState(); + + expect((await browser.pages()).length).toBe(1); + + const context = await browser.createIncognitoBrowserContext(); + await context.newPage(); + expect((await browser.pages()).length).toBe(2); + expect((await context.pages()).length).toBe(1); + + await context.close(); + expect((await browser.pages()).length).toBe(1); + }); + itFailsFirefox('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([ + utils.waitEvent(browser, 'targetcreated'), + page.evaluate<(url: string) => void>( + (url) => window.open(url), + server.EMPTY_PAGE + ), + ]); + expect(popupTarget.browserContext()).toBe(context); + await context.close(); + }); + itFailsFirefox('should fire target events', async () => { + const { browser, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const events = []; + context.on('targetcreated', (target) => + events.push('CREATED: ' + target.url()) + ); + context.on('targetchanged', (target) => + events.push('CHANGED: ' + target.url()) + ); + context.on('targetdestroyed', (target) => + events.push('DESTROYED: ' + target.url()) + ); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual([ + 'CREATED: about:blank', + `CHANGED: ${server.EMPTY_PAGE}`, + `DESTROYED: ${server.EMPTY_PAGE}`, + ]); + await context.close(); + }); + itFailsFirefox('should wait for a target', async () => { + const { browser, puppeteer, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + let resolved = false; + + const targetPromise = context.waitForTarget( + (target) => target.url() === server.EMPTY_PAGE + ); + targetPromise + .then(() => (resolved = true)) + .catch((error) => { + resolved = true; + if (error instanceof puppeteer.errors.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 puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + } + await context.close(); + }); + + it('should timeout waiting for a non-existent target', async () => { + const { browser, puppeteer, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const error = await context + .waitForTarget((target) => target.url() === server.EMPTY_PAGE, { + timeout: 1, + }) + .catch((error_) => error_); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + await context.close(); + }); + + itFailsFirefox('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().length).toBe(0); + expect(context2.targets().length).toBe(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().length).toBe(1); + expect(context2.targets().length).toBe(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().length).toBe(1); + expect(context1.targets()[0]).toBe(page1.target()); + expect(context2.targets().length).toBe(1); + expect(context2.targets()[0]).toBe(page2.target()); + + // Make sure pages don't share localstorage or cookies. + expect(await page1.evaluate(() => localStorage.getItem('name'))).toBe( + 'page1' + ); + expect(await page1.evaluate(() => document.cookie)).toBe('name=page1'); + expect(await page2.evaluate(() => localStorage.getItem('name'))).toBe( + 'page2' + ); + expect(await page2.evaluate(() => document.cookie)).toBe('name=page2'); + + // Cleanup contexts. + await Promise.all([context1.close(), context2.close()]); + expect(browser.browserContexts().length).toBe(1); + }); + + itFailsFirefox('should work across sessions', async () => { + const { browser, puppeteer } = getTestState(); + + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts().length).toBe(2); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts.length).toBe(2); + remoteBrowser.disconnect(); + await context.close(); + }); +}); diff --git a/test/chromiumonly.spec.ts b/test/chromiumonly.spec.ts new file mode 100644 index 0000000..7b64a70 --- /dev/null +++ b/test/chromiumonly.spec.ts @@ -0,0 +1,159 @@ +/** + * 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 expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('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(() => 7 * 8)).toBe(56); + browser1.disconnect(); + + const browser2 = await puppeteer.connect({ + browserURL: browserURL + '/', + }); + const page2 = await browser2.newPage(); + expect(await page2.evaluate(() => 8 * 7)).toBe(56); + browser2.disconnect(); + 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 = null; + await puppeteer + .connect({ + browserURL, + browserWSEndpoint: originalBrowser.wsEndpoint(), + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Exactly one of browserWSEndpoint, browserURL or transport' + ); + + 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 = null; + await puppeteer + .connect({ browserURL }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Failed to fetch browser webSocket URL from' + ); + 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()).length).toBe(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 = new Promise((resolve) => + browser.once('disconnected', resolve) + ); + // Emulate user exiting browser. + browser.process().kill(); + await disconnectedEventPromise; + }); + }); +}); + +describeChromeOnly('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) => + res.end(` + + `) + ); + server.setRedirect('/intervention.js', '/redirect.js'); + let serverRequest = null; + server.setRoute('/redirect.js', (req, res) => { + serverRequest = req; + res.end('console.log(1);'); + }); + + await page.setRequestInterception(true); + page.on('request', (request) => 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/test/click.spec.ts b/test/click.spec.ts new file mode 100644 index 0000000..cb81798 --- /dev/null +++ b/test/click.spec.ts @@ -0,0 +1,352 @@ +/** + * 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, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils 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(() => globalThis.result)).toBe('Clicked'); + }); + it('should click svg', async () => { + const { page } = getTestState(); + + await page.setContent(` + + + + `); + await page.click('circle'); + expect(await page.evaluate(() => globalThis.__CLICKED)).toBe(42); + }); + itFailsFirefox( + '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(() => delete window.Node); + await page.click('button'); + expect(await page.evaluate(() => globalThis.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(() => globalThis.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(() => globalThis.result)).toBe('Clicked'); + }); + itFailsFirefox('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 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(() => globalThis.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.click('textarea'); + await page.click('textarea', { clickCount: 2 }); + await page.click('textarea', { clickCount: 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 = []; + page.on('console', (msg) => messages.push(msg.text())); + for (let i = 0; i < 11; ++i) { + // We might've scrolled to click a button - reset to (0, 0). + await page.evaluate(() => 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(() => globalThis.__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(() => globalThis.result.check)).toBe(null); + await page.click('input#agree'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(true); + expect(await page.evaluate(() => globalThis.result.events)).toEqual([ + 'mouseover', + 'mouseenter', + 'mousemove', + 'mousedown', + 'mouseup', + 'click', + 'input', + 'change', + ]); + await page.click('input#agree'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(false); + }); + + itFailsFirefox('should click on checkbox label and toggle', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(null); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(true); + expect(await page.evaluate(() => globalThis.result.events)).toEqual([ + 'click', + 'input', + 'change', + ]); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => globalThis.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 = null; + await page + .click('button.does-not-exist') + .catch((error_) => (error = error_)); + expect(error.message).toBe( + 'No node 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, puppeteer } = getTestState(); + + await page.setViewport(puppeteer.devices['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(() => document.querySelector('#button-5').textContent) + ).toBe('clicked'); + await page.click('#button-80'); + expect( + await page.evaluate( + () => document.querySelector('#button-80').textContent + ) + ).toBe('clicked'); + }); + // See https://github.com/puppeteer/puppeteer/issues/7175 + itFailsFirefox('should double click the button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + globalThis.double = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', () => { + globalThis.double = true; + }); + }); + const button = await page.$('button'); + await button.click({ clickCount: 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(() => globalThis.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(() => globalThis.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(() => document.querySelector('#button-8').textContent) + ).toBe('context menu'); + }); + // @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'); + }); + itFailsFirefox('should click the button inside an iframe', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('
spacer
'); + await utils.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(() => globalThis.result)).toBe('Clicked'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4110 + xit('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 utils.attachFrame( + page, + 'button-test', + server.CROSS_PROCESS_PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + await frame.$eval('button', (button: HTMLElement) => + button.style.setProperty('position', 'fixed') + ); + await frame.click('button'); + expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + itFailsFirefox( + '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(() => window.devicePixelRatio)).toBe(5); + await page.setContent( + '
spacer
' + ); + await utils.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(() => globalThis.result)).toBe('Clicked'); + } + ); +}); diff --git a/test/cookies.spec.ts b/test/cookies.spec.ts new file mode 100644 index 0000000..ad7339b --- /dev/null +++ b/test/cookies.spec.ts @@ -0,0 +1,564 @@ +/** + * 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, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +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, + sourcePort: 8907, + 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.length).toBe(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.length).toBe(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.length).toBe(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) => 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, + sourcePort: 8907, + sourceScheme: 'NonSecure', + }, + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourcePort: 8907, + sourceScheme: 'NonSecure', + }, + ]); + }); + itFailsFirefox('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) => 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 () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + expect(await page.evaluate(() => document.cookie)).toEqual( + 'password=123456' + ); + }); + itFailsFirefox('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.length).toBe(1); + expect(cookies2.length).toBe(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(); + }); + itFailsFirefox('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) => 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); + }); + itFailsFirefox('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) => 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', + }, + ] + ); + }); + itFailsFirefox('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 = null; + try { + await page.setCookie({ name: 'example-cookie', value: 'best' }); + } catch (error_) { + error = 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 = null; + 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_; + } + 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 = null; + await page.goto('data:,Hello%2C%20World!'); + try { + await page.setCookie({ name: 'example-cookie', value: 'best' }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain( + 'At least one of the url and domain needs to be specified' + ); + }); + itFailsFirefox( + '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 unsecure 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); + }); + itFailsFirefox('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', + }, + ]); + }); + itFailsFirefox('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: string) => Promise>((src) => { + let fulfill; + const promise = new Promise((x) => (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', + }, + ]); + }); + itFailsFirefox( + '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: string) => Promise>((src) => { + let fulfill; + const promise = new Promise((x) => (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 () { + itFailsFirefox('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/test/coverage-utils.js b/test/coverage-utils.js new file mode 100644 index 0000000..c23e507 --- /dev/null +++ b/test/coverage-utils.js @@ -0,0 +1,164 @@ +/** + * 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. + */ + +// TODO (@jackfranklin): convert this to TypeScript and enable type-checking +// @ts-nocheck + +/* We want to ensure that all of Puppeteer's public API is tested via our unit + * tests but we can't use a tool like Istanbul because the way it instruments + * code unfortunately breaks in Puppeteer where some of that code is then being + * executed in a browser context. + * + * So instead we maintain this coverage code which does the following: + * * takes every public method that we expect to be tested + * * replaces it with a method that calls the original but also updates a Map of calls + * * in an after() test callback it asserts that every public method was called. + * + * We run this when COVERAGE=1. + */ + +const path = require('path'); +const fs = require('fs'); + +/** + * This object is also used by DocLint to know which classes to check are + * documented. It's a pretty hacky solution but DocLint is going away soon as + * part of the TSDoc migration. + */ +const MODULES_TO_CHECK_FOR_COVERAGE = { + Accessibility: '../lib/cjs/puppeteer/common/Accessibility', + Browser: '../lib/cjs/puppeteer/common/Browser', + BrowserContext: '../lib/cjs/puppeteer/common/Browser', + BrowserFetcher: '../lib/cjs/puppeteer/node/BrowserFetcher', + CDPSession: '../lib/cjs/puppeteer/common/Connection', + ConsoleMessage: '../lib/cjs/puppeteer/common/ConsoleMessage', + Coverage: '../lib/cjs/puppeteer/common/Coverage', + Dialog: '../lib/cjs/puppeteer/common/Dialog', + ElementHandle: '../lib/cjs/puppeteer/common/JSHandle', + ExecutionContext: '../lib/cjs/puppeteer/common/ExecutionContext', + EventEmitter: '../lib/cjs/puppeteer/common/EventEmitter', + FileChooser: '../lib/cjs/puppeteer/common/FileChooser', + Frame: '../lib/cjs/puppeteer/common/FrameManager', + JSHandle: '../lib/cjs/puppeteer/common/JSHandle', + Keyboard: '../lib/cjs/puppeteer/common/Input', + Mouse: '../lib/cjs/puppeteer/common/Input', + Page: '../lib/cjs/puppeteer/common/Page', + Puppeteer: '../lib/cjs/puppeteer/common/Puppeteer', + PuppeteerNode: '../lib/cjs/puppeteer/node/Puppeteer', + HTTPRequest: '../lib/cjs/puppeteer/common/HTTPRequest', + HTTPResponse: '../lib/cjs/puppeteer/common/HTTPResponse', + SecurityDetails: '../lib/cjs/puppeteer/common/SecurityDetails', + Target: '../lib/cjs/puppeteer/common/Target', + TimeoutError: '../lib/cjs/puppeteer/common/Errors', + Touchscreen: '../lib/cjs/puppeteer/common/Input', + Tracing: '../lib/cjs/puppeteer/common/Tracing', + WebWorker: '../lib/cjs/puppeteer/common/WebWorker', +}; + +function traceAPICoverage(apiCoverage, className, modulePath) { + const loadedModule = require(modulePath); + const classType = loadedModule[className]; + + if (!classType || !classType.prototype) { + console.error( + `Coverage error: could not find class for ${className}. Is src/api.ts up to date?` + ); + process.exit(1); + } + for (const methodName of Reflect.ownKeys(classType.prototype)) { + const method = Reflect.get(classType.prototype, methodName); + if ( + methodName === 'constructor' || + typeof methodName !== 'string' || + methodName.startsWith('_') || + typeof method !== 'function' + ) + continue; + apiCoverage.set(`${className}.${methodName}`, false); + Reflect.set(classType.prototype, methodName, function (...args) { + apiCoverage.set(`${className}.${methodName}`, true); + return method.call(this, ...args); + }); + } + + /** + * If classes emit events, those events are exposed via an object in the same + * module named XEmittedEvents, where X is the name of the class. For example, + * the Page module exposes PageEmittedEvents. + */ + const eventsName = `${className}EmittedEvents`; + if (loadedModule[eventsName]) { + for (const event of Object.values(loadedModule[eventsName])) { + if (typeof event !== 'symbol') + apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); + } + const method = Reflect.get(classType.prototype, 'emit'); + Reflect.set(classType.prototype, 'emit', function (event, ...args) { + if (typeof event !== 'symbol' && this.listenerCount(event)) + apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); + return method.call(this, event, ...args); + }); + } +} + +const coverageLocation = path.join(__dirname, 'coverage.json'); + +const clearOldCoverage = () => { + try { + fs.unlinkSync(coverageLocation); + } catch (error) { + // do nothing, the file didn't exist + } +}; +const writeCoverage = (coverage) => { + fs.writeFileSync(coverageLocation, JSON.stringify([...coverage.entries()])); +}; + +const getCoverageResults = () => { + let contents; + try { + contents = fs.readFileSync(coverageLocation, { encoding: 'utf8' }); + } catch (error) { + console.error('Warning: coverage file does not exist or is not readable.'); + } + + const coverageMap = new Map(JSON.parse(contents)); + return coverageMap; +}; + +const trackCoverage = () => { + clearOldCoverage(); + const coverageMap = new Map(); + + return { + beforeAll: () => { + for (const [className, moduleFilePath] of Object.entries( + MODULES_TO_CHECK_FOR_COVERAGE + )) { + traceAPICoverage(coverageMap, className, moduleFilePath); + } + }, + afterAll: () => { + writeCoverage(coverageMap); + }, + }; +}; + +module.exports = { + trackCoverage, + getCoverageResults, + MODULES_TO_CHECK_FOR_COVERAGE, +}; diff --git a/test/coverage.spec.ts b/test/coverage.spec.ts new file mode 100644 index 0000000..b2a8730 --- /dev/null +++ b/test/coverage.spec.ts @@ -0,0 +1,280 @@ +/** + * 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, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Coverage specs', function () { + describeChromeOnly('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.length).toBe(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.length).toBe(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.length).toBe(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) => entry.url.startsWith('debugger://')) + ).not.toBe(null); + expect(coverage.length).toBe(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(() => console.log('bar')); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(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.length).toBe(2); + coverage.sort((a, b) => 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.length).toBe(1); + const entry = coverage[0]; + expect(entry.ranges.length).toBe(1); + const range = entry.ranges[0]; + expect(entry.text.substring(range.start, range.end)).toBe( + `console.log('used!');` + ); + }); + 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.length).toBe(1); + const entry = coverage[0]; + expect(entry.url).toContain('unused.html'); + expect(entry.ranges.length).toBe(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}\//g, ':/') + ).toBeGolden('jscoverage-involved.txt'); + }); + 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.length).toBe(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.length).toBe(0); + }); + }); + // @see https://crbug.com/990945 + xit('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(); + }); + }); + + describeChromeOnly('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.length).toBe(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.length).toBe(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.length).toBe(2); + coverage.sort((a, b) => 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.length).toBe(1); + expect(coverage[0].url).toBe('unused.css'); + expect(coverage[0].ranges.length).toBe(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.length).toBe(1); + expect(coverage[0].url).toContain('/csscoverage/media.html'); + expect(coverage[0].ranges).toEqual([{ start: 17, end: 38 }]); + }); + 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}\//g, ':/') + ).toBeGolden('csscoverage-involved.txt'); + }); + 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( + () => window.getComputedStyle(document.body).margin + ); + expect(margin).toBe('10px'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(0); + }); + it('should work with a recently loaded stylesheet', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.evaluate<(url: string) => Promise>(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) => (link.onload = x)); + }, server.PREFIX + '/csscoverage/stylesheet1.css'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(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.length).toBe(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.length).toBe(0); + }); + }); + }); +}); diff --git a/test/defaultbrowsercontext.spec.ts b/test/defaultbrowsercontext.spec.ts new file mode 100644 index 0000000..b84428d --- /dev/null +++ b/test/defaultbrowsercontext.spec.ts @@ -0,0 +1,114 @@ +/** + * 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, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +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, + sourcePort: 8907, + sourceScheme: 'NonSecure', + }, + ]); + }); + itFailsFirefox('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(() => 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', + }, + ]); + }); + itFailsFirefox('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/test/dialog.spec.ts b/test/dialog.spec.ts new file mode 100644 index 0000000..39339f1 --- /dev/null +++ b/test/dialog.spec.ts @@ -0,0 +1,74 @@ +/** + * 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, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +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(() => 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'); + }); + + itFailsFirefox('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(() => 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(() => prompt('question?')); + expect(result).toBe(null); + }); +}); diff --git a/test/diffstyle.css b/test/diffstyle.css new file mode 100644 index 0000000..c58f0e9 --- /dev/null +++ b/test/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/test/elementhandle.spec.ts b/test/elementhandle.spec.ts new file mode 100644 index 0000000..f65027f --- /dev/null +++ b/test/elementhandle.spec.ts @@ -0,0 +1,453 @@ +/** + * 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, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +import utils from './utils.js'; +import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; + +describe('ElementHandle specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describeFailsFirefox('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) => void>( + (element) => (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'); + const pptrBoundingBox = await element.boundingBox(); + const webBoundingBox = await page.evaluate((e: HTMLElement) => { + const rect = e.getBoundingClientRect(); + return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; + }, element); + expect(pptrBoundingBox).toEqual(webBoundingBox); + }); + }); + + describeFailsFirefox('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 utils.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 () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const elementHandle = await page.$('#frame1'); + const frame = await elementHandle.contentFrame(); + expect(frame).toBe(page.frames()[1]); + }); + }); + + describe('ElementHandle.click', function () { + // See https://github.com/puppeteer/puppeteer/issues/7175 + itFailsFirefox('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(() => globalThis.result)).toBe('Clicked'); + }); + 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. + () => button + ); + await buttonHandle.click(); + expect( + await page.evaluate( + // @ts-expect-error clicked is expected to be in the page's scope. + () => clicked + ) + ).toBe(true); + }); + it('should work for TextNodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const buttonTextNode = await page.evaluateHandle( + () => document.querySelector('button').firstChild + ); + let error = null; + await buttonTextNode.click().catch((error_) => (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) => button.remove(), button); + let error = null; + await button.click().catch((error_) => (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) => (button.style.display = 'none'), + button + ); + const error = await button.click().catch((error_) => error_); + expect(error.message).toBe( + 'Node is either not visible 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) => (button.parentElement.style.display = 'none'), + button + ); + const error = await button.click().catch((error_) => error_); + expect(error.message).toBe( + 'Node is either not visible 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_) => error_); + expect(error.message).toBe( + 'Node is either not visible or not an HTMLElement' + ); + }); + }); + + describe('ElementHandle.hover', function () { + itFailsFirefox('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(() => 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); + } + }); + }); + + describe('Custom queries', function () { + this.afterEach(() => { + const { puppeteer } = getTestState(); + puppeteer.clearCustomQueryHandlers(); + }); + it('should register and unregister', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent('
'); + + // Register. + puppeteer.registerCustomQueryHandler('getById', { + queryOne: (element, selector) => + document.querySelector(`[id="${selector}"]`), + }); + const element = await page.$('getById/foo'); + expect( + await page.evaluate<(element: HTMLElement) => string>( + (element) => 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).toStrictEqual( + new Error( + 'Query set to use "getById", but no query handler of that name was found' + ) + ); + } + const handlerNamesAfterUnregistering = + puppeteer.customQueryHandlerNames(); + expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy(); + }); + it('should throw with invalid query names', () => { + try { + const { puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('1/2/3', { + queryOne: () => 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, puppeteer } = getTestState(); + await page.setContent( + '
Foo1
Foo2
' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (element, selector) => + document.querySelectorAll(`.${selector}`), + }); + const elements = await page.$$('getByClass/foo'); + const classNames = await Promise.all( + elements.map( + async (element) => + await page.evaluate<(element: HTMLElement) => string>( + (element) => element.className, + element + ) + ) + ); + + expect(classNames).toStrictEqual(['foo', 'foo baz']); + }); + it('should eval correctly', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '
Foo1
Foo2
' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (element, selector) => + document.querySelectorAll(`.${selector}`), + }); + const elements = await page.$$eval( + 'getByClass/foo', + (divs) => divs.length + ); + + expect(elements).toBe(2); + }); + it('should wait correctly with waitForSelector', async () => { + const { page, puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => 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 waitFor', async () => { + /* page.waitFor is deprecated so we silence the warning to avoid test noise */ + sinon.stub(console, 'warn').callsFake(() => {}); + const { page, puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + }); + const waitFor = page.waitFor('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, puppeteer } = getTestState(); + await page.setContent( + '
Foo2
' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + queryAll: (element, selector) => + element.querySelectorAll(`.${selector}`), + }); + + const element = await page.$('getByClass/foo'); + expect(element).toBeDefined(); + + const elements = await page.$$('getByClass/foo'); + expect(elements.length).toBe(3); + }); + it('should eval when both queryOne and queryAll are registered', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '
text
content
' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + queryAll: (element, selector) => + element.querySelectorAll(`.${selector}`), + }); + + const txtContent = await page.$eval( + 'getByClass/foo', + (div) => div.textContent + ); + expect(txtContent).toBe('text'); + + const txtContents = await page.$$eval('getByClass/foo', (divs) => + divs.map((d) => d.textContent).join('') + ); + expect(txtContents).toBe('textcontent'); + }); + }); +}); diff --git a/test/emulation.spec.ts b/test/emulation.spec.ts new file mode 100644 index 0000000..82d263b --- /dev/null +++ b/test/emulation.spec.ts @@ -0,0 +1,389 @@ +/** + * 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, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Emulation', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + let iPhone; + let iPhoneLandscape; + + before(() => { + const { puppeteer } = getTestState(); + iPhone = puppeteer.devices['iPhone 6']; + iPhoneLandscape = puppeteer.devices['iPhone 6 landscape']; + }); + + 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(() => window.innerWidth)).toBe(800); + await page.setViewport(iPhone.viewport); + expect(await page.evaluate(() => window.innerWidth)).toBe(375); + await page.setViewport({ width: 400, height: 300 }); + expect(await page.evaluate(() => 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(() => 'ontouchstart' in window)).toBe(false); + await page.setViewport(iPhone.viewport); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true); + expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); + await page.setViewport({ width: 100, height: 100 }); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); + + function dispatchTouch() { + let fulfill; + 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(() => document.body.textContent.trim())).toBe( + 'NO' + ); + await page.setViewport(iPhone.viewport); + await page.goto(server.PREFIX + '/detect-touch.html'); + expect(await page.evaluate(() => 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(() => globalThis.Modernizr.touchevents)).toBe( + true + ); + }); + itFailsFirefox('should support landscape emulation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => screen.orientation.type)).toBe( + 'portrait-primary' + ); + await page.setViewport(iPhoneLandscape.viewport); + expect(await page.evaluate(() => screen.orientation.type)).toBe( + 'landscape-primary' + ); + await page.setViewport({ width: 100, height: 100 }); + expect(await page.evaluate(() => 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(() => window.innerWidth)).toBe(375); + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'iPhone' + ); + }); + itFailsFirefox('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) => (button.style.marginTop = '200px'), + button + ); + await button.click(); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + }); + + describe('Page.emulateMediaType', function () { + itFailsFirefox('should work', async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + true + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe( + false + ); + await page.emulateMediaType('print'); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + false + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true); + await page.emulateMediaType(null); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + true + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe( + false + ); + }); + it('should throw in case of bad argument', async () => { + const { page } = getTestState(); + + let error = null; + await page.emulateMediaType('bad').catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported media type: bad'); + }); + }); + + describe('Page.emulateMediaFeatures', function () { + itFailsFirefox('should work', async () => { + const { page } = getTestState(); + + await page.emulateMediaFeatures([ + { name: 'prefers-reduced-motion', value: 'reduce' }, + ]); + expect( + await page.evaluate( + () => matchMedia('(prefers-reduced-motion: reduce)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-reduced-motion: no-preference)').matches + ) + ).toBe(false); + await page.emulateMediaFeatures([ + { name: 'prefers-color-scheme', value: 'light' }, + ]); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: light)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: dark)').matches + ) + ).toBe(false); + await page.emulateMediaFeatures([ + { name: 'prefers-color-scheme', value: 'dark' }, + ]); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: dark)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => 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( + () => matchMedia('(prefers-reduced-motion: reduce)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-reduced-motion: no-preference)').matches + ) + ).toBe(false); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: light)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: dark)').matches + ) + ).toBe(false); + await page.emulateMediaFeatures([{ name: 'color-gamut', value: 'srgb' }]); + expect( + await page.evaluate(() => matchMedia('(color-gamut: p3)').matches) + ).toBe(false); + expect( + await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches) + ).toBe(false); + await page.emulateMediaFeatures([{ name: 'color-gamut', value: 'p3' }]); + expect( + await page.evaluate(() => matchMedia('(color-gamut: p3)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches) + ).toBe(false); + await page.emulateMediaFeatures([ + { name: 'color-gamut', value: 'rec2020' }, + ]); + expect( + await page.evaluate(() => matchMedia('(color-gamut: p3)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches) + ).toBe(true); + }); + it('should throw in case of bad argument', async () => { + const { page } = getTestState(); + + let error = null; + await page + .emulateMediaFeatures([{ name: 'bad', value: '' }]) + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported media feature: bad'); + }); + }); + + describeFailsFirefox('Page.emulateTimezone', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.evaluate(() => { + globalThis.date = new Date(1479579154987); + }); + await page.emulateTimezone('America/Jamaica'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)' + ); + + await page.emulateTimezone('Pacific/Honolulu'); + expect(await page.evaluate(() => globalThis.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(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)' + ); + + await page.emulateTimezone('Europe/Berlin'); + expect(await page.evaluate(() => globalThis.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 = null; + await page.emulateTimezone('Foo/Bar').catch((error_) => (error = error_)); + expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); + await page.emulateTimezone('Baz/Qux').catch((error_) => (error = error_)); + expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); + }); + }); + + describeFailsFirefox('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 = null; + await page + // @ts-expect-error deliberately passign invalid deficiency + .emulateVisionDeficiency('invalid') + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported vision deficiency: invalid'); + }); + }); +}); diff --git a/test/evaluation.spec.ts b/test/evaluation.spec.ts new file mode 100644 index 0000000..e3803d5 --- /dev/null +++ b/test/evaluation.spec.ts @@ -0,0 +1,476 @@ +/** + * 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 utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +const bigint = typeof BigInt !== 'undefined'; + +describe('Evaluation specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluate', function () { + it('should work', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => 7 * 3); + expect(result).toBe(21); + }); + (bigint ? it : xit)('should transfer BigInt', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a: BigInt) => a, BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should transfer NaN', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should transfer -0', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should transfer Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should transfer -Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should transfer arrays', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => 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) => Array.isArray(a), [1, 2, 3]); + expect(result).toBe(true); + }); + it('should modify global environment', async () => { + const { page } = getTestState(); + + await page.evaluate(() => (globalThis.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); + }); + itFailsFirefox( + 'should return undefined for objects with symbols', + async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined); + } + ); + it('should work with function shorthands', async () => { + const { page } = getTestState(); + + const a = { + sum(a, b) { + return a + b; + }, + + async mult(a, b) { + 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) => a['中文字符'], { + 中文字符: 42, + }); + expect(result).toBe(42); + }); + itFailsFirefox('should throw when evaluation triggers reload', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + location.reload(); + return new Promise(() => {}); + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Protocol error'); + }); + it('should await promise', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => 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(() => 6 * 7); + }); + await page.goto(server.EMPTY_PAGE); + expect(await frameEvaluation).toBe(42); + }); + itFailsFirefox('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, b) { + return await page.evaluate<(a: number, b: number) => number>( + (a, b) => a * b, + a, + b + ); + }); + const result = await page.evaluate(async function () { + return await globalThis.callController(9, 3); + }); + expect(result).toBe(27); + }); + it('should reject promise with exception', async () => { + const { page } = getTestState(); + + let error = null; + await page + // @ts-expect-error we know the object doesn't exist + .evaluate(() => notExistingObject.property) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('notExistingObject'); + }); + it('should support thrown strings as error messages', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + throw 'qwerty'; + }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('qwerty'); + }); + it('should support thrown numbers as error messages', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + throw 100500; + }) + .catch((error_) => (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) => a, object); + expect(result).not.toBe(object); + expect(result).toEqual(object); + }); + (bigint ? it : xit)('should return BigInt', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should return NaN', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should return -0', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should return Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should return -Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should accept "undefined" as one of multiple parameters', async () => { + const { page } = getTestState(); + + const result = await page.evaluate( + (a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), + undefined, + 'foo' + ); + expect(result).toBe(true); + }); + it('should properly serialize null fields', async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => ({ a: undefined }))).toEqual({}); + }); + itFailsFirefox( + 'should return undefined for non-serializable objects', + async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => window)).toBe(undefined); + } + ); + itFailsFirefox('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); + }); + itFailsFirefox('should be able to throw a tricky error', async () => { + const { page } = getTestState(); + + const windowHandle = await page.evaluateHandle(() => window); + const errorText = await windowHandle + .jsonValue() + .catch((error_) => error_.message); + const error = await page + .evaluate<(errorText: string) => Error>((errorText) => { + throw new Error(errorText); + }, errorText) + .catch((error_) => 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: HTMLElement) => string>( + (e) => 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 = null; + await page + .evaluate((e: HTMLElement) => e.textContent, element) + .catch((error_) => (error = error_)); + expect(error.message).toContain('JSHandle is disposed'); + }); + itFailsFirefox( + 'should throw if elementHandles are from other frames', + async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const bodyHandle = await page.frames()[1].$('body'); + let error = null; + await page + .evaluate((body: HTMLElement) => body.innerHTML, bodyHandle) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'JSHandles can be evaluated only in the context they were created' + ); + } + ); + itFailsFirefox('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); + }); + itFailsFirefox('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(() => window.location.reload()), + ]); + const error = await executionContext + .evaluate(() => null) + .catch((error_) => error_); + expect((error as Error).message).toContain('navigation'); + }); + itFailsFirefox( + '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 () { + const { page } = getTestState(); + + const a = await page.evaluate<() => string>(() => + 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 = null; + await page + .evaluate( + () => + new Promise(() => { + throw new Error('Error in promise'); + }) + ) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Error in promise'); + }); + }); + + describeFailsFirefox('Page.evaluateOnNewDocument', function () { + it('should evaluate before anything else on the page', async () => { + const { page, server } = getTestState(); + + await page.evaluateOnNewDocument(function () { + globalThis.injected = 123; + }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect(await page.evaluate(() => globalThis.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.injected = 123; + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(await page.evaluate(() => globalThis.injected)).toBe(123); + + // Make sure CSP works. + await page + .addScriptTag({ content: 'window.e = 10;' }) + .catch((error) => void error); + expect(await page.evaluate(() => (window as any).e)).toBe(undefined); + }); + }); + + describe('Frame.evaluate', function () { + itFailsFirefox('should have different execution contexts', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + await page.frames()[0].evaluate(() => (globalThis.FOO = 'foo')); + await page.frames()[1].evaluate(() => (globalThis.FOO = 'bar')); + expect(await page.frames()[0].evaluate(() => globalThis.FOO)).toBe('foo'); + expect(await page.frames()[1].evaluate(() => globalThis.FOO)).toBe('bar'); + }); + itFailsFirefox('should have correct execution contexts', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames().length).toBe(2); + expect( + await page.frames()[0].evaluate(() => document.body.textContent.trim()) + ).toBe(''); + expect( + await page.frames()[1].evaluate(() => 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(() => window.location.href)).toContain( + 'localhost' + ); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(await mainFrame.evaluate(() => window.location.href)).toContain( + '127' + ); + }); + }); +}); diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts new file mode 100644 index 0000000..8eca362 --- /dev/null +++ b/test/fixtures.spec.ts @@ -0,0 +1,93 @@ +/** + * 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 expect from 'expect'; +import { getTestState, itChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +import path from 'path'; + +describe('Fixtures', function () { + itChromeOnly('dumpio option should work with pipe option ', async () => { + const { defaultBrowserOptions, puppeteerPath } = getTestState(); + + let dumpioData = ''; + const { spawn } = require('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) => (dumpioData += data.toString('utf8'))); + await new Promise((resolve) => 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 } = require('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) => (dumpioData += data.toString('utf8'))); + await new Promise((resolve) => 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 } = require('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; + const wsEndPointPromise = new Promise( + (x) => (wsEndPointCallback = x) + ); + let output = ''; + res.stdout.on('data', (data) => { + output += data; + if (output.indexOf('\n')) + wsEndPointCallback(output.substring(0, output.indexOf('\n'))); + }); + const browser = await puppeteer.connect({ + browserWSEndpoint: await wsEndPointPromise, + }); + const promises = [ + new Promise((resolve) => browser.once('disconnected', resolve)), + new Promise((resolve) => 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/test/fixtures/closeme.js b/test/fixtures/closeme.js new file mode 100644 index 0000000..dbe798f --- /dev/null +++ b/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/test/fixtures/dumpio.js b/test/fixtures/dumpio.js new file mode 100644 index 0000000..40b9714 --- /dev/null +++ b/test/fixtures/dumpio.js @@ -0,0 +1,8 @@ +(async () => { + const [, , puppeteerRoot, options] = process.argv; + const browser = await require(puppeteerRoot).launch(JSON.parse(options)); + const page = await browser.newPage(); + await page.evaluate(() => console.error('message from dumpio')); + await page.close(); + await browser.close(); +})(); diff --git a/test/frame.spec.ts b/test/frame.spec.ts new file mode 100644 index 0000000..269da7d --- /dev/null +++ b/test/frame.spec.ts @@ -0,0 +1,270 @@ +/** + * 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 utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Frame specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Frame.executionContext', function () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(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.frame()).toBe(frame1); + expect(context2.frame()).toBe(frame2); + + await Promise.all([ + context1.evaluate(() => (globalThis.a = 1)), + context2.evaluate(() => (globalThis.a = 2)), + ]); + const [a1, a2] = await Promise.all([ + context1.evaluate(() => globalThis.a), + context2.evaluate(() => globalThis.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(() => window); + expect(windowHandle).toBeTruthy(); + }); + }); + + describe('Frame.evaluate', function () { + itFailsFirefox('should throw for detached frames', async () => { + const { page, server } = getTestState(); + + const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.detachFrame(page, 'frame1'); + let error = null; + await frame1.evaluate(() => 7 * 8).catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Execution context is not available in detached frame' + ); + }); + }); + + describe('Frame Management', function () { + itFailsFirefox('should handle nested frames', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(utils.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)', + ]); + }); + itFailsFirefox( + 'should send events when frames are manipulated dynamically', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + // validate frameattached events + const attachedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + await utils.attachFrame(page, 'frame1', './assets/frame.html'); + expect(attachedFrames.length).toBe(1); + expect(attachedFrames[0].url()).toContain('/assets/frame.html'); + + // validate framenavigated events + const navigatedFrames = []; + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await utils.navigateFrame(page, 'frame1', './empty.html'); + expect(navigatedFrames.length).toBe(1); + expect(navigatedFrames[0].url()).toBe(server.EMPTY_PAGE); + + // validate framedetached events + const detachedFrames = []; + page.on('framedetached', (frame) => detachedFrames.push(frame)); + await utils.detachFrame(page, 'frame1'); + expect(detachedFrames.length).toBe(1); + expect(detachedFrames[0].isDetached()).toBe(true); + } + ); + itFailsFirefox( + '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'), + utils.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', () => (hasEvents = true)); + page.on('framedetached', () => (hasEvents = true)); + await page.goto(server.EMPTY_PAGE); + expect(hasEvents).toBe(false); + }); + itFailsFirefox('should detach child frames on navigation', async () => { + const { page, server } = getTestState(); + + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + page.on('framedetached', (frame) => detachedFrames.push(frame)); + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(attachedFrames.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + itFailsFirefox('should support framesets', async () => { + const { page, server } = getTestState(); + + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + page.on('framedetached', (frame) => detachedFrames.push(frame)); + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await page.goto(server.PREFIX + '/frames/frameset.html'); + expect(attachedFrames.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + itFailsFirefox('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) => (frame.onload = x)); + }, server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + expect(page.frames()[1].url()).toBe(server.EMPTY_PAGE); + }); + itFailsFirefox('should report frame.name()', async () => { + const { page, server } = getTestState(); + + await utils.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) => (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'); + }); + itFailsFirefox('should report frame.parent()', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.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()); + }); + itFailsFirefox( + 'should report different frame instance when frame re-attaches', + async () => { + const { page, server } = getTestState(); + + const frame1 = await utils.attachFrame( + page, + 'frame1', + server.EMPTY_PAGE + ); + await page.evaluate(() => { + globalThis.frame = document.querySelector('#frame1'); + globalThis.frame.remove(); + }); + expect(frame1.isDetached()).toBe(true); + const [frame2] = await Promise.all([ + utils.waitEvent(page, 'frameattached'), + page.evaluate(() => document.body.appendChild(globalThis.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().length).toBe(2); + expect(page.frames()[1].url()).toBe( + server.PREFIX + '/frames/frame.html?param=value#fragment' + ); + }); + }); +}); diff --git a/test/golden-chromium/csscoverage-involved.txt b/test/golden-chromium/csscoverage-involved.txt new file mode 100644 index 0000000..9b851d0 --- /dev/null +++ b/test/golden-chromium/csscoverage-involved.txt @@ -0,0 +1,16 @@ +[ + { + "url": "http://localhost:/csscoverage/involved.html", + "ranges": [ + { + "start": 149, + "end": 297 + }, + { + "start": 327, + "end": 433 + } + ], + "text": "\n@charset \"utf-8\";\n@namespace svg url(http://www.w3.org/2000/svg);\n@font-face {\n font-family: \"Example Font\";\n src: url(\"./Dosis-Regular.ttf\");\n}\n\n#fluffy {\n border: 1px solid black;\n z-index: 1;\n /* -webkit-disabled-property: rgb(1, 2, 3) */\n -lol-cats: \"dogs\" /* non-existing property */\n}\n\n@media (min-width: 1px) {\n span {\n -webkit-border-radius: 10px;\n font-family: \"Example Font\";\n animation: 1s identifier;\n }\n}\n" + } +] \ No newline at end of file diff --git a/test/golden-chromium/grid-cell-0.png b/test/golden-chromium/grid-cell-0.png new file mode 100644 index 0000000000000000000000000000000000000000..ff282e989b7eae67216b80a63e7b9c5b55142e3d GIT binary patch literal 436 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0FXZV666ZaSVxQeS6K`gDFws_{aH0 z(R!vHR}>x=G`%ShT|3M6M)~r39RKrdD+Bs)`Xz0~#xd&&?IC@peb^jIqDWwG_iK(IytPYE>Pdz*19rMNPWSc@C S)&O8^FnGH9xvX@1PLn2z=UNPi4K-5=as z15f-nRN6H6eA~Gn0o!5?N)G3raWddOsKc^Z0K#Z_AG!Q@@}8*dwJTGP7OpW>)r#Ex zOh#wgJL~r%n>OiqUp6^k{9W_(4*x5qb6p<(D_XniRP9^Whg&CUh)mq~{_b*}`}{3> zX^J)Hjocnyi^z(c{qjkfO~%@v|2lWgXiX4nisVRBgfn_NrsSHJ9NVsba<;s^d-3Ic QKyNa5y85}Sb4q9e0L%S&y#N3J literal 0 HcmV?d00001 diff --git a/test/golden-chromium/grid-cell-2.png b/test/golden-chromium/grid-cell-2.png new file mode 100644 index 0000000000000000000000000000000000000000..7b01753b6a63d4b7bad71f18f0918edcf2304789 GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0FXZU@Z1@aSVxQeS5>u`*46r+r!Hf zmO48aOv|%6a!^6GC&0KwVAes!fP;&gngUpMdn{|RcQEj7cUrc?!s5s!y_F(Mn!1C( zmuZG(w9IlWIQ2?^Q;Sb7Pr>z^b?mthA9W%p70Ixt>B}*k(hj=RlW*Ftte1KCy&$*n zMgwHx^8eJivG)rsTTg4gc0c}oeg2!~1LvadTATQ84qJV5+uO={%QELapWYJrZnC)q zPt@9LM-S$y?l*f|ni{z+*E%uGYVNrdquZywFS>??rU@}9ubwe?{zqd_3NHqC1#c++0%IRsR%>H z_S>qN6Ki#QrcTw(<81oy?}tFQT32W4y?a0R@iQEI8Y;l@Fi_;^=f87RA}60zsWFl2 zeX-)T6@#|+QwzSoQa`hg9CFd|K_iS$Jz`v7DWiJ!$4&M{?2)%sclf>q#sY(;UU#EbrvoWO}X4q=nxHU5xIDQ;9}4cH?~?LTn=(Ahb>ml zAv_Ehr*IHkqMW1QKuTI3f8#>_-|61L{rSMVd)$<5+xUM40T}AM-;5tsTAhI3hTmhB zOw5vrSu!z8CT7XRESZ=k)8w+utD8Vn3q-X;>ykdrD@c5PX?I@;k;~=6;czCCaU7>t zcYS6(zJ3THip64jRRy4F_lZQJ_R`;q%n@0nP28AOEX(pt-V#8!+a;Q)EC5HF>%;4< z<1B!KwH4w_(BCbu?UU15tp*?#i;ZRAcn%2n;;hkV7=|H)$lDHm;MmSBeP-IuXLVgK zl}hw!RMz?J-S=fY9v=(_#F-$n%FVWEn)Tk>@Q&-cp-_mdqOtat7Xc)b$#S#((0^Br zv=E|DD9~BdFY)vFNj1``R0@EiDAj6pXCdN03qY%KO&>DS/jscoverage/involved.html", + "ranges": [ + { + "start": 0, + "end": 35 + }, + { + "start": 50, + "end": 100 + }, + { + "start": 107, + "end": 141 + }, + { + "start": 148, + "end": 160 + }, + { + "start": 168, + "end": 207 + } + ], + "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 z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n" + } +] \ No newline at end of file diff --git a/test/golden-chromium/mock-binary-response.png b/test/golden-chromium/mock-binary-response.png new file mode 100644 index 0000000000000000000000000000000000000000..8595e0598edf24deb4f3f9c35c2b16cd86720a4d GIT binary patch literal 6789 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 S5dr?a1g9*oAy*+|8S+0LA10Fk literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-clip-odd-size.png b/test/golden-chromium/screenshot-clip-odd-size.png new file mode 100644 index 0000000000000000000000000000000000000000..b010d1f87f0cdc2193383fdcf193ca77ee3ae570 GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqh5}q!OAsQ2t|NQ^|zn)ErQFtSl eyC{FqQHF!BS%f5Jy6_0nzJ~w$rfoRGpJnv$PSpoJ{vqkyX(c>Cv$_KwCt4o=uUdkTs_% z)m*Z^*WDZ9@>U5Xce^MJQxOyDv@7`h`gBQJ`h{P27Spu1>S{sDS2EeNx=L}BLx6F7 zPDv@7vdY!~0Pay7$#($4HN=sI6fd*9eZbCpz9xt7qv|~l=EC)mCMq2fKgu4?b*n?_ zXLRc+|CcWp&-$>qge>?ZOtdAAvjgQzwn_$Qw35rrpIsua~v_P~Jy;x9XP zJnQ_AL%3IAZI5|_MS7gYOh~1un5NTBj8Rz#_TN>==+L=z=~5lxQ&OgjuCQudyR9ao zWaTTg9;x6?f;fBD$t*94W;@H1exgwH)a3K*Zymd?WyX#ak@c8$bFuRLnf2e(wqXcG z9>(YMu*Gx}3KTRt#D0KS__DfDm3{W99Zsy%)c*BYDL!2pPar5i;H;<4(I^)a)Qz2- zwBc|#l8NVM3yh78AC-a+N+apfAe$s%tcW%N5+Mt9vzB1b6owdfY$9F5yL0LlVcjEi zJX-*uP^dFplct=&#j>1lAGf+sPVsm@|BI-v(m~L+rB+tH8ALa>O*^SO8PEBh7O63A z8Ks{uUh5lRD!;zWXqN-(TGLA@7e6)#bGwIg^0GPa#%O5n^9O0#Mh5|5bBgOHbYpO- zAqO`ID>q^jH7g5Ke(RD0Sj8|*L+FKNE+~*WV0U8`vb8dPC#Rh8zJ0kG-$jm0FZ@$p zi4H=wjhkv|>d$=3dL|mLDCeDB@7eM(9p!JSSG70G?Ck8sCMLos>EQRRf@)0gwFHkx zuA9XHtBihz8Jk$nB(~2B?m18EOiWDFrEz``jE?$O&&J@PP^j1mn?-87?snUNL$-pQ zAA+F^(M=SUPW4nemzE2(e2(!&PX?4RpBqO^BI>%${R;q!ve*xUg;Ny1%HF z(c-mHs}1O$_^6e&HTB7ph%VQ;^>-ZexM6)$%$O0s=oCTe0Y?fLD4OOpUK!opL57Fx zh7l9**sgPg=h zS&GnMi`<%s(01JV^yM`H<7yYDvZ_i-e=m2C`_J8|k;6R5yMZFp$bz*Ys-wxVD%ZqB zQ-C#pZT9`!r^x0dtSSA-Y%(tUsytb9yF$n9yT3;2FKFwW(eom+ULy7Nz`z^B?ORiO z9ba`b2@;0O`XL6Y4xAHT!{y1H{|mVPIi*2u`A!d37=ueLo9)n)FS6gWCqO+h$?PTf zybs509V7>5>8-fj@@!J73zNW(Z?1=KfWQlyS}VSiy$f+;>FiC={E*J~c~8pAzX_=D z>fqM|r>u;b7kkmB67olv;pfi%IO%z1Gb#siS`Nm4%+o!bOO=3bU|r23CS*Hb7JDC*OPcHbw8EqQ58Yg)#%p(hQP9g}aav zqZMy-iuM(X3y!A;h6jieRZ;AC_7PhWU-bSS&#+s~`m__ut1#YQbXo_0d}1iE(}nF1Ot zhTY-wgB-SnohH7|7osma7OFXFOZsszr~e{UTSZOkt5s<3PJRjr(dPle)NUcc7f zM{<5_VoEhf!b9HUnuwc`NlWe5KfY9EEDJ{pK+^@VUBBL0Tig8fQzKcwL4pOEmY9&f zhkq)_Oe-t8>s>N_GT7XmfxffYhOo)6#o*ZSFCrwIV#m^hws-urmhWyzRTM?}^h_zdhT}pNUJM*KKk2oYPOM)*VeUbl^R* xL1(qH{YFzSgD6u`Nc+rW&s8 l$xQXpPL7hitfgaR5|>%Ts?Gg~>mblP22WQ%mvv4FO#tsSEqDL` literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-element-larger-than-viewport.png b/test/golden-chromium/screenshot-element-larger-than-viewport.png new file mode 100644 index 0000000000000000000000000000000000000000..5fcdb923555dcdff72047ca5a53974015cb17b6e GIT binary patch literal 2807 zcmeAS@N?(olHy`uVBq!ia0y~yV2T1^4mP03_vn|e7#O(rdAc};RLpsM?I7m?1Azk@ zykGv)UhaK`rOBjmew=YNKLf-5>hv{Y3|>mU^ExdB^RWeeN$`2#PGmPUZ{j?bP0l+XkKPxw46 literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-element-rotate.png b/test/golden-chromium/screenshot-element-rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..52e2a0f6d3c66bd455961250051d416897790ffd GIT binary patch literal 2342 zcmYLLc|6ox8$Tn<*qS!Gq)5u3RfNXP%$3R-YC;oP%aYeNikZn$DHSEMG+c?y*zQO# zSq5p*$l4%op{|RuFJl>I++%t_pZAa7@Av(k=lMS8`#k47=X2t1tu17wl%)UwvX&>Y z_V7zte~{wv{r0#U3jpMVCH6N5V&=p*!WkDwHHqn|GZKx~eg5AVn;7Mn{P`G|q9+{rIi^KOcH zx=zk_{g6^Ry&{Zll`Ew;nud~bBnD0Vv>*$#6JFjtZ!S}{FKlller65_(gnS- zNhKDOyyaQlAwxl3P@_grrXf*e$oZ38b#cXBR9(-w;zs^ZTOM<=_QurPCf?&a7C(r7 zt(TGN>Vu*0Y$CuLiK>}TZ8~Cn>I(Tkm}APuZR{^`F&I9;pOY;14ux%tgGJUG(T*H)QQpAkdMS(McCj*H%@YJ5#*=A>#-Xwgfg<29rI z_Ux54>o!xkZgIUbtLi6pyN~f-;u`MR5rK!IZ>ldE-;HB{X8qRZyoc=b6=mx4)bxoX zhU{xnu2!x-OZb?!s*qeXXgaecUl4SP^DK#m{E=jsaOWn+pKU+2pyP?~v3mWZsI}id z=m-q=VA5Dh`(71A`^?@lIPM8O|d1r|ufvBeH zj>ka6=X;+FoXUf5>8Iac%(HZ{MAe;EAT-c-)a;u(6|nDw9FbR_?}n8z;oi&NBz4OH z({!i+F<{5%;fA_SH7dLB&kwJoOEG%$`>FuIr4e&0W5Tf zh&r9em3B?R=Zcx(pwJf2ln;w0#XO_2lhTw=oNk-(h{2Gbz65mHNl8i;jewbXd+=T| zSSp9CCoHtRNVfSeR!Xn@AgiQdL$)(_Y|p~%*y#Jth7^3PgxN3*g4HcU%9gPtrWY2P~l3Flc(6jsx(p++-3i?O2x;DWhc`K%#Yq_xbxs8kDKZT zy+zE}B}^k|6s6Dzre7ut;}Ul-Q5_!Mxh&~D!JSy$keF_5H&qgU-aLT}9;IncX~cte znsz!ZV@bS@fzGUnOc_qXCKl)EfdLiDAMram{3Eb*ZS6e>N-*$Q>U`TCWnrgOELwK} z%(3Q1m>sQShP!MFcfMY@9DsnoHZ@G~22ZzF+XI|eo;m{`UblJQpn;!G|LcVVsh}C% zJkYVUTg_r~{rG7A``sYwdDNGctJz`Rx)&B+7p@rTA+kpaD5w-E+7+mDVgrJtE6T?BjF+HWD1d(kotGJz#pPp7*n_b@nS-TpTX)^DJdxA z`bS4T10q@Sb=c!Gg=~GDDPYT`Ka9MEasDn;GsxI)t!k3ASkh_?4)e#{$~dV{&cVfD zThw+kbxYR0Z%5JyP8Wq~E;}#_tu{a{$EDH){u$zw<15_6KrZW(5@0dfj`j}o?6A}m zw=vEwK*mbOEi9;uT@ME!zXi0Vl1;bS^BD~B^{gtC1b+DSNNQ3=B>ex8$}j6Pfxi%$ zRuqu%5ap4DC|sBy5zY+}oO|Fk&$CE+z2wu!R=;+wTTO|OF(zV05O4=xK&y~3ttjxj zD$MsHozQgwjIfw$)lSSp^=T$2+{gV+-yc}ji!fg#MZn3IRD*RwLb z?(Rf)MfnPHOprb05c8OZ2M}e5*7?qMX~Mm~WmL3-kRN9f?V+G2eVMn|>C%VEx7sj} zysQ;NbwIUvv|QpWBqvHFagRNJHsUL{c(goP@F4N>yzKn_g+clhb4w7ey^7aIKhcZX zVE9?DB`5F|Dn3Lc%PK1Ghh(js)UEy8s=WdCvMRNuDV^nHg1HUt_CIlJTVWSPZUz}* z(mQNO3D{#dgHu}v`*zei@-=s3otS6>OmX>xXLazzcFhjS>Xmm@=LJD{?&I7|u8^n# z@+)5$X&$nCs-bm!xqA;hw3A0TsV{IK^F33=B9Nv4C3FuL(@7h{bLfPWFvK-p2i@jUIotmc(896Wb&Kt7rl&bETeCcO%py9 d&BB%;BgmKzm3Ksu%kcgKEOFM@;uDvm{{`<2gQEZd literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-element-scrolled-into-view.png b/test/golden-chromium/screenshot-element-scrolled-into-view.png new file mode 100644 index 0000000000000000000000000000000000000000..917dd48188d45ea1b07c1fa460a691a9e4a2d050 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL1|)l2v+e?^d`}n0kc`H+w+y)&3bP0l+XkKPxw46 literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-grid-fullpage.png b/test/golden-chromium/screenshot-grid-fullpage.png new file mode 100644 index 0000000000000000000000000000000000000000..d6d38217f7f8f646573ba81a44901b03513809bb GIT binary patch literal 74972 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 literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-offscreen-clip.png b/test/golden-chromium/screenshot-offscreen-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..e503f801ec6a112abf23373dca38891a34056607 GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^DImwh4)bmss0Gyl(=*}He| z?%lgLZQ8VY_3D)?S5BKYt*@^yIXT(a*Voh2)6~?|$jC@fPftrrOG!ydOiT=D24B0v z79iE;>EaktaqI03Pre2N4u?R|B2Zu+ahi$k7SbmAbl6mK2XLEo#D%MYA xWG1Xwe0ayZvy73p6J9jP+`6s2>qh={Ze1TK!)u2gJ_fpo!PC{xWt~$(6988PWRw5^ literal 0 HcmV?d00001 diff --git a/test/golden-chromium/screenshot-sanity.png b/test/golden-chromium/screenshot-sanity.png new file mode 100644 index 0000000000000000000000000000000000000000..ecab61fe179e8c95f9cf7cf8399f5ff3e34b0b16 GIT binary patch literal 36252 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≥_k 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 JGp`tW{|{lD?$`hT literal 0 HcmV?d00001 diff --git a/test/golden-chromium/transparent.png b/test/golden-chromium/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf45d8688fc68c85c4d250d14eaf6fdf153ec82 GIT binary patch literal 119 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

1S9n@|k#3(Z~7B48S`p{?9-{0zm8bcY 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 EU-|1?!2kdN literal 0 HcmV?d00001 diff --git a/test/golden-chromium/vision-deficiency-protanopia.png b/test/golden-chromium/vision-deficiency-protanopia.png new file mode 100644 index 0000000000000000000000000000000000000000..bede7c1ed050a7797dcc4370a5a7aa4a6401a30e GIT binary patch literal 36282 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 literal 0 HcmV?d00001 diff --git a/test/golden-chromium/vision-deficiency-tritanopia.png b/test/golden-chromium/vision-deficiency-tritanopia.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f6bbec2e8587c6fa836b237f91e46fcf9a4743 GIT binary patch literal 37282 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®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 aZ}qX3f7?3*4l-994qG_w`ZND`MBiSf?k6e$ literal 0 HcmV?d00001 diff --git a/test/golden-firefox/grid-cell-0.png b/test/golden-firefox/grid-cell-0.png new file mode 100644 index 0000000000000000000000000000000000000000..4677bdbc4f84999748dc38c3c2038b729acfae17 GIT binary patch literal 331 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFba9PIEG|2zP+N@*J>cr`fzj8 zx&>m*4ih%8@pvaHa`14uHmy}y!YRbu*7fdXPnCXR+7pAFwL2e~A2*4=BzDfS&Z*$s zL_@W?PD_>wWNNxZaZQ|tU|w4+@ZPr9kmbN4jUDf+tAHHF4LaWEKKo?rGJe>#Zi_^iMl6^sIFFR;$i1 z@A*u#mXF_8e{K5nHL~CNTIt^0#}>=J-`K44`{UJTm44Z|Q4Cq9DL>U~R*r zJFKRG2U%G$6Zp78%KeJP zqGyv&+?&00${#_7oyu!XnNG+v9(cVgBKxdtWcI}KXD!p;oml<-U59d?#Uu@*V;0Jv kSV5YOp_}tZS^WoN`?ktMuij+c1GYfz4YTLM*0l$nZm*nTVzx|}Nzgt&ybl+@P7%FD}Vf6mX(pQXY%CnD80?Br3Q)o*Hich>FiSG{)Y)~v1$Ia%44 z877xhJVRW*{QI}>lcf@qdRT1#`zJ}Aq2+ew*I$1HN<4Yua{k=8uKk=X5#{COT1r*L z#hVqPXD#FYdHVF}N1{zC({3N0uHk!mY4OG*MdzQNp0+IWguIQogMGqym|5OpWT#*Ylr7Y_oo7NZTkGP*m}OTG+#TnrQpSj86xp( zv?i%&ie{V57B!ch6cHNHAFy+Ng|^P&bK%o(-ntbeco4`*PEK}qb(ueJo|f_T>(@Ud z?X-0Qy8G$8^>04Def##&W6$;14~t|M7YDn#oIig)yZ8k{h2HmHzgF$uy&LG6s;Vy4 zu{gvty>FGz zv{dfR2NMLIXtygQsd_G&>zBUrxk6W}t<%$_&eZ3tPxl;ueCd4b-Z=ip$4+fpQ&B1- zCzrMVzP^Q>U0!d5ukEz6z?gVh^Kb37mn9`Oa<4ysF8+8lt3$dtbt4}iUzd_yYP`9` z<(DE}YnI8KewNgEwZzUo-)Lsa<(DO?E1UL;DjleIN`}WdRT2)@Mh;Hiqe}l-X8Nf8 zTKPFFIn@rBCf6L_eJ+}pk^~8JoU6gcKfZH)D{pV;Qk!=Fv9{&ygf5k7_mi*2h-_SC zR(j3$x%SNYVyjoR&tDCcQ3wm|fB$Sz$J4x5U(ze*=|B0)yIKPneSae7JwJNpZo(^2 zA}iT=NA91_`Oiy>1M}BBEQqL5?sZ#yY3J@lQP(y7_O^d!e%&xXw|V9K&AiJt9^nDCNc;4r)|p>FJb%Y_d4f{A0EGL) zPp>QD-7>%OyxCT_m4LZvZZv;+nzWDe{XWGbMb$fsUvJFOd(|7dedT7|q?@`SR}b&K z`pRs^F|pTs7alJ!x_IQ*l~2BNOF?0)a?Rb9|NF;fAm8i_{}Q1p?z`s3hxr%q6dAA> jBB9*rcG7Wj`YbKZ>AzxsJFf|_&}8s*^>bP0l+XkK?WceC literal 0 HcmV?d00001 diff --git a/test/golden-firefox/screenshot-element-bounding-box.png b/test/golden-firefox/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..f4e059c300ccd27816427fb0dc2c466edc640418 GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETf1WOmAsLNtuPUx%ItmnP*&); zA!ETTwe{^!ML*d)Un~9E^?^%J&@J$Z>;q9Fp2M}$zLz(wTD5!M`@@Y_N^70%xc~j; z{gf$rM1k$%bjK~J0=bed(QFr|ftj}apIZJgoR3e=`RDR`Ixr*{JYD@<);T3K0RRV7 Bh8_R_ literal 0 HcmV?d00001 diff --git a/test/golden-firefox/screenshot-element-fractional-offset.png b/test/golden-firefox/screenshot-element-fractional-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..f554b1d62c4ab19291c200519abdf2b7d12ac1f8 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^ra&yt!3HE(H?jKyDGN^*$B>BDx91&s84Ltk9RAGS z_wQ9B?_>pr2I-#YOH*8f#e5@8QZJ$~>a*6e8_l=hFd<5IBhU;6Pgg&ebxsLQ0B}kn Ay#N3J literal 0 HcmV?d00001 diff --git a/test/golden-firefox/screenshot-element-fractional.png b/test/golden-firefox/screenshot-element-fractional.png new file mode 100644 index 0000000000000000000000000000000000000000..d1431bd91dea17468f279240d5eb2ef56c606e33 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^#y~8_!3HFA4=h;^q)a?r977^n-=1gWWMJTFariy` zUe<}njf^Zi7Vcm@8)2G!wI|d#42h9@Zp|0vjptcZ50p3p^)q<7`njxgN@xNAbnhd| literal 0 HcmV?d00001 diff --git a/test/golden-firefox/screenshot-element-larger-than-viewport.png b/test/golden-firefox/screenshot-element-larger-than-viewport.png new file mode 100644 index 0000000000000000000000000000000000000000..6d28cddcea336612c0e4ad4df15d34c91b7d0418 GIT binary patch literal 2797 zcmeAS@N?(olHy`uVBq!ia0y~yV2T1^4mP03_vn|e7#O&=c)B=-RLpsM&5)PDfak!5 zPwIDToiEgE6XBaVGuCsGis)G;hJrI0j0`S4Vhj#PMj4}lFq#rZbHQjX7|jKvxnMLG pjOK#TTtL5E@Gk?{B7M1!{a6Rx71&BPt}pZa{4t3!wSE@zDe9k_lqIWn}K@IaJs`|v;M&Qa#kfldWDx`1?>5cAfu z5Zy)Kq=q`DTl}Ny5AKO-274kgBk|nCmy;Wb5}!@MAg_+Uyy=$FBP|TSha<1A(`$1{ zMzXEkVr`n|`Iy%}0B|7cIgNEM`DQ4)vG7-)--^Hb2K$z837t78y_k_z31Cf6?*lPx zHu$|{qUhc6r3d+mgp!Xb+3hAjw<&v3r8Zki*Jx;McgJQgD!ujW_FK704EU(aW3+^r z#`%MA+PnS=KL4?FYkj8VV`*GYXNsQR@N<5FW|}0<+qT>YIb|9S(%Q_FzhAZ4R2+!j zd}BBBDlJ&sQl5RLsn}84|HLQ(ZnFN~FnWE!TFDY8Hj9;4lK%f!SO)lDyf#(O+6G!Ss~F!~U?4dG^2SVGxpaO` zK!x4Il`;n9`erKLV$n>$c$Dc6UM8k-FZZz;j_hjcU}gN0|O({i(Y1J;(f5w>x8kdH8RR;C)Rp=EUfi zahff{41ey&Mtp@)oTGLORiiF+({p_xbRi96j`dC@o(7)7m5A~K8esf-wYAuJq2|U>MDs4KK#NQ91%Vq680yX zD~5@ga$;OPnnM9Gb zFXG`N%hCIH5M6^gJ6+0<=OCRC4;@9UV(*V$Z_4o2egZ9dY0Y zai6t{)rRDf=eJL;>=!_m2(-6llDz{ak;2Qh+#00q&P!0Z9UR(oNX05c7Id;d0Be(T zB{BSjUy)*8QEj+xGIH~z6afXH5cUwV3I-Hi-*(MH6daE8uqXEy_5SlR1U`WVdUEvO zU68tHq1{M;@fSj(4g^7e9;Vn;Q6%JneMD7vir%xR1o-Jw;s+mAMakOma547t5x3X( zQ`&V}e0Nf$4-af0RCT)`4g#KL;SAC+$w8Zg42QLAJ0-YaKsH8(YLR#?Zv1{6(WZ-c z*`Me#^EC@}WdD|{xG~7)8ThD1!x2y_>(6Uuvk+UwYUa%yKAZN4?z;e`96`=gpDwwE zIQU-IED&^n@c~fns)E;+Gj%7Snf^{UE5#rBjfJ=d-{3arBI8uuy1v0RAHa@q7=Tj^ zb`^7jx0e$4L;gpHTLUEw6lv>Dgpj(GZF_R5k|z7pR)b003P^y`_fW>)tl2wz#eiv; zIl)GxJA_qo!jQ2_ox%fMH>RCUnhfL@{2ada)QVg{vNy3dARVfiTiD+7Y!02)%9O04 z@{bnFFp&HPclL-ji^Rvj{Q4v9vrtE*SU(^Ds%c?>1MkI(T`?-wtqi^XmF&LbQw|g9 z!>z#7d$GK}Yc%=ftsV8qmrgtnuEC*519L^&@;dJ9nZw{i<_V$_ZqtFIL_6^CoECQW zA$>Fan)(MWKWo(E5{hIwpr&yg=T*mj90MYN)OZhK?J3DB?I1{oBU-3fu}Xcj0F@Yi z7d*ps0bhMeCe{W@Ry}Dzs^}IzGH)to&XWo2M*Ow>9GgX3&Ue6+s|;g5BJ{GP)T`8m zDveVmI?2sq4n3k`*Ig@su@vlJb3ud7sw*{j2aaI)x0CFv`!w_+d_7S|KR|-Khy6%Ew1n{C(mN@ literal 0 HcmV?d00001 diff --git a/test/golden-firefox/screenshot-element-scrolled-into-view.png b/test/golden-firefox/screenshot-element-scrolled-into-view.png new file mode 100644 index 0000000000000000000000000000000000000000..2b72c7528b256daf8afa4ec296e50429eaa8d307 GIT binary patch literal 153 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 literal 0 HcmV?d00001 diff --git a/test/golden-firefox/screenshot-offscreen-clip.png b/test/golden-firefox/screenshot-offscreen-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..846b810386f7c69f2bb976e1426cd3509f9a6630 GIT binary patch literal 279 zcmeAS@N?(olHy`uVBq!ia0vp^DImCFH0Xa1i#vv1$N zJ$v?S*|KHr+O;cJuADx7dVha^a&oedkB_^%yOEKRo}QkTmX@ljs*H>bA0Ho3?+LZ^ z{XmMbB*-tA!Qt5rkhyK1E{-7;x8C0Habi$ZN=YTl$WntOAyQ;*rA&w+84c%@t;I+}_6B7O$(CU_ z+9uhCFohx6Cc6y7F#KNEXgQtx{(QdQ$M^T&=l$wfjTc0ZtZ0noUxRuBnYiIlR zX=Tg%vg6dnraObA@2$r0ZJ0jbJJq@T7`;Y$^X<7CvDi}7d}WQY9Z}m07ZTN_T~hqI zYRsNt)73NO>XJR?KD$bleg!?(j1(#N!mSJt{562M?8o2#@UO3KtRQ%L9+RY+3e`tO ziZWVS-WJeS2?{p4#kH*m_i$Ar)0_`1qB7!IiEhAM0UR` zOOo3l)6(1=nYoFbo15ETPI%D#?%h!?H&&M9Ge7w!)L_Hu-1~~8Tfwz8H33d%&YWoy z+vtc96QW$|ykC+2 z1Yh5>7+F_LXmj=MtL^?}xw#~cTvN1*R6s&tms+t1{@&!|WWP_p|J=OXRu7^2=;(E} z*49;7w3>#r6O`RmxAgV(p{~2sIHR39wpLaR&CSQ1FI-4(*GTxI+Qg9Q+EI2)sQ$){ z8*TiI$MS-!R%Om*WeyAs^s5~ z^I6MfS?7Rn4#6#@n`iiH&m`K~+S%2@jVPvKNDo4#G#kRggho6o z!)e#IZPL`#Y)?pp3Tj(HvDtw=di325qTAirPu1+2{l=%`!iC9mlz}1Z70uNn?QMJn zEY?s@*UQUmtH(Z4Y<#kah=`oZ>kHL1Hk&>1=@m3l_1(MgN{~W{#N!?MRPERgZwxAD zHwKgy79O|F5Ny=)olTQTirQiOHi9NeBh#yczAhP6crUseCerbujSSyYWE#tUhwerjDM4imD|cX^zg1_Voc22 zcv?_kpfRgtJX{o?YuVV|-hO20uWendEK5MAi-H29%YS zqB4w3O#0l9wn^Wl)x~WvgeT_CC4ftjH5>84{lq8~&W-Cf&`>LOchANzJwI$3> zxiJY%_4RIWau!roe)DXZm9_QWFgIfxW@_aHKJFg15ZhsITlcza?^bgzTxyB^^koa{ zU3ZEjGxe3h3ho|s%?Qmzc!lI|DK9-i{Cid6eUDA(dl3p2(*)?-T3!9|ti4JW0|hR9cr>I@Q9$ zqR9=lRtTnodgxOJNj~W96X!O0jBw0|t5&W|5B9fTwGVch5trjw6B89H7*(dJBTbnW z@-j~_Z1)tDy^T%d@UY9!z`!_jaL`|b9F=^)#Ka_&-7fz7?|-ZzSB7~!E-JFv7IBDK zz#JHO2W4$focL21ez<(q&(FTKrNvw~|Hu|*clX8=nh`O-OG!zo-6w-Wu?5hxGhl~I zI>z4C7Um}R;>Y9T<5t^60XV!%HMs)apLj8|mp=xA49YiNw+_lSHkiZ5G*S!OQ|I{iDB2 zw$kVKKX8f)k#^k+bpe^aP=EimwfOD&hwK4=+GP=3?_^|KGWst5_3PK>x|o85goGF+&O1^39{rd> zsBBDuS$fK=(?a@8nIy3tHZ*{bj{wqnxVY>vmynRiEY|gF!G!jlf1b18Vsj4bh#6TG+ZlhB*7 zwSD=})H;stGaN>aE??1|wqb4o2wh*pdHp)D-*qus7+>9x7X9#k&#UBQ+22X77cZvG z?YJX*u9(~TB8Z%ypOTf8)zZ*lM)AIQ>0&H$DOyKnVzS+QYmw_QU60eJ7av0Ijnlf1 zm?D*zp{Aknxcry}olb{-Py6NBAXpP9X${D7$1Am>fhDZciu=bm*3A1Uk|DsmygKk zsGED+)YEhBAaWQ#wfbi$wO@!_Q5ClPmMV+Iy5i%jzIX2_f0v!;{)vdHbIwjqPgSyq zMtEJTccyo}%IgBIB=kO&Tjk#44rva)?A`-RYinzH8FER9A!*49#vm&G{{1JrT|_Z- zdvmV}yV5JBdb;`t4!Fy*>g(%|Fsct+4DpG<*jrm??l`vGNm|?2T&UhKYDdo@X8jpe zm$oB9^|0V*rlR=1NU_xWXJ@7TV*akf_S7}Fv2FaXl?M{TnG6AHUs z{E1Jy=jM}XHPu~};d0HdYYs633ky|X)ln=&4nBYMX!!8(aR20Fp z(Cqj}w0GxSTxug|UY9RtB#y@kP%O7^cDY%VNvjdr4v?z2!YISU$OtOH^rOlvLI{nb zpR`yx5Ac*1v}9t}dD4+5@?YMGl22T2ZEqKBqOgw{`%AsjFqsE<*rrenh}NRRB8=?q zpr(i!>F>WOsWeigv7+78TUr`m{WzlRJ5~@aMb@6-q82*EUgID~f99>;knJ{}zN*+o2jegvJ9_@z5x4eB; z7%<%Pq%kUjMj5!(xPYm`q^y;$Aajapb%9zlQ~9Xc1jF2{-!euGl@4mRkd%}>{<=WH zBV2aq**A=pw>#UDm+V6*9PUGM@oWF- z^8Q}&@UFxr;B%<+rmFP&sCjDuUhcDsJV2$*`@s+J*L{@VLznDb=($_u;y}#u>lo^_ zAAkSDzdnyaR(9sP0B#;W)GI116rMSbV68YZ;RL_tR{gBG>(`ffqLv#o4bbAB0Tmd~ zEpp(0tB@T|6Xp1cLwZY*?YkUNF|mS@66+ZbM}s%CC)|_GDgt(AKoSs^Fy4%K-o({4 z2c`)cww_K;WiS}zr%xAeV5Oi82+-ev(s(436oA<;CnrbphlA5y9UNW{U|n75a0@7s zs;X*cFta9B)${V@W|2vl#!v4S6HHUrDp?~D(HzEqD{;GPnHMr$zpmMoM{q9C*nV=%yOn|?kWB_SJlDZw7%`G z&rW^bfut5ji5H;H(NS8a(c!~s!Dn}{P7>3)_wL<0kf6xpj;8-s`2G9)HxH%(hm=h0 z`cYCxdE8a{G2l{|+&%~paJZwA)EiBJj}JG>B;jy4Qp@}ImY=3mk#WpQ6@AyC_bW`p z@H4uDn~jLBL(p1!IZ2*4LPbk=_u0oLx*nG=tKeW%tS46rWdW76T_ss>HL?x6@GaGv z*ss4Km!%M9E-1wKLzs6;5WAm)i>jVA3wn&k^_*jy>h8 zvOq+2%+6l@G#$OYSgupeIEj`_a9F2>C8eHn*Bt;@)Lt!Rz`SrDRmd?uQ>n?1BNX6U zT3e6l0w@jNiM|`z;^^Su;q1H;$dLP}ikK7P%9$1p-eXMW798&!Sir!I4Z(D>v6eZ1+yYp|&H!i;k7MnPp%Y1K;!|X0W!nd?ZGm@xQuCQv!@GA~L(?~I zO6$Rq1^Aeln8zXB!kAUP!BR>cs>^eC^%@|plHBb(-dpq14we*>!9ZM*I6yVt3zXirr#|cciBo*d->I!5a3==`roZ&uiU_#`%+pCCbJqeMn?Gc0YiT)ZCyUgGZfLaT zZkRzyMsAlds$Ne_5;P1n67ur_n&o#(Rh#CBoq)Sk;XD<_k8;X-etlrM9m5Zgbs?RS zo^AjD#?!~gTG77ju}o9^41j|`xD{$q934oL(PXR&+;EtOjc&eK`o`weM~_ZIA2ZE# zJ%9d9i&vlA2DqvGF2IbWmWql$ZytO>KY0``c0m(}&xDu2(PYYqUbu1*i%}ITdivBX zPmV0%3y|&f(YOuwyRaP79~GyV<#_Sp_Tv)b;!nRYV-(=I8r2hYVF`1ab{*Y-zkK=f z>5|TR-k>Pgj!~Mk+TX?fY885cC40WTI2JX*0Z1(}qnlCp znqEa zvjYkU4MH8pd5JvD%j@EHCc--%3~2A@NFY70tML@ng+2rmBBk>?1{+lGRrLg(EWuUL$nvQv* z^!0SDt*kt-SOHi-!=S+n_My}e;>0M?TN4JUn)>rFO@-RoQ|2sk?9zkxXuu+firdIm z*s;UO%d2Q|V!|8202*Zqusel42|NG=K-0=l=cCLq)L24$yB**p5rI=rSdGl5Sy?$bRL*rT%Y(?=-3)Oe=83bd;c%AQ6)SDFS&j5;r!-AbD(bYM22+)OQSDT!W_gC`W$tXLW1j0;T2 zzc}1Cisf>-RQeEoxTAd41`i8Ae4MP!IL?2QJ;i`E(+eR*$MI-B%|kvuB{vTyb-#=3 z6asRj`=ELu1Ua>v`wxp5F-;E>b3Vgz=>TlO)SJ2q!cm-t?{LzXX*!GQB>#YuX`Q=F z03RgEmbK0O6!0`CUH1lPnpxCm&2J%3{%vOxUOrOO)_>-5d@ZmkO-+eR4u`Bx#A&ob z1wdKfn%Nl(ZTcyaT$9pTyuN-Ad;gAzLpKb^p)`rp&~R4_mOk|W{-Poyfdj2E%_-** zdh7W$W0LD(LYAVGF~`i(J!ZLs(gR<5M{3xR> zNS1Yl$Z7}T+sMcWp*G;_mC)%jbpFkAr+}Lb?F+7JfzbQ9jOY%^5zM@Rrzdn~hQn@{=d4uY_FkAMQ61ai5}AOGN{PPJ5&VTqdy9`Ig(8ZQWHd`Zn(KZtL~!0;JYl z&b2(O;ou+z5Hwe%-C%q1yy^O&tg`KqZMNJqBb;l7Lr@)ntTJ0$Tk+Fh&aF)!zR#VV z#=Yo@e|fhg(@*@3X?am*W@e{8-S~7Rqe)w!n@@mZmi`h?*!l@5*1lkA1Rzc_rVVx! zmvF-O36{g+@n^CSIWF;6cmIJO5R|yR&T%sf`kJNl`SV)}KNzKuGu16)6A}VHtO0Ub zG&36IEzE(%+hYe<3-uZmJbik4;@9VaJSI^&yvql$^TN;Bid$HFVq#+@kTES2{ajf) z$iDT%hkD>5EfsHm+XE7Z8?m1N`P2wSU2aImX3srjFBORS_Op`evwFMvi8sfg<8{U) zCQlacgs}NjDiW}DDePR1?Ok~+M^OME@X~=^&KDVEwZiD;YerG)fruw4{}l+3_?l;U zm-P$}(_izP-xbk4UX6PUD1LT0*Lj6z+>4wU^sJSa7kljR&e{3Zy8y?^u`UUgBYhfJ zyIqyB<9T-OtNKOl&^?QrL z#+++t*uK%3>c7Q_Z{O60hChU=ftaTUK9hx;9;F2L#+xAxIs>5u{ue zEQd)C*KYKtwwB0Vej!8k#v-^SSJ_~|*U-vt5LQWP@F~a{o0zC-6e^JB)i&v8`HrVv z4D+8$zK5#lg26A{u=HSP)~25GDeyu{IMf?-i_8w6(q#u>biHthcqQ?}L)R@)RWQvO z!k!bbrvtedGycPyM&|oMf0xMmF7zkD+)QOcwKG4o%#`k6wx;i_8mla$Oe!P<4s&=U zStm526q+AhR!~q7NG+6SJw~S+xuw>?>UpoTG4R_LaAvWejAO%F)_AqVH9mUwt~W*H zRlJH^!Pen?UeW(Fsn!1j1?oAGgA3vB>hI_l*QR##z=t-nrHyQrBU^R2ki@PIcjN=v z(sut(t5gUs^T^AM6I1>D8S2DjnIr`T1#&^bxpOwH7&fkP7>USG-5w($S6^axmyV8( zyi6ERx@i21^+uMH``&ru-NJW0t$A1qqKS81gGw4rLypIorH~sBy zfeH>0!$m|y!0dz+{xZhp5+FQxR0u}D2e5wTMGBDp5FZ@TY+As%uycn8E-ZWCH8Jbo ziQkYbvn~-^2yYM!C;hKc=h&|$Msp#`Dw70sLjBm-*k9OdFl-WEBLnRe=MxfgzT2D_ zL3=@8tL>sC0(e?oO%3L3SnXf90+ocETPZPg@4bYN`gq`aqA?`i#HewqD zr-xdglk@IoH22Rx|GcxjW~398d$3O)QRKw@J^0+U*9!&ff|owmye!iIHH?NrfM`K5 zr!#?!$Mhxo06pe%e+ocPz#y^}5XWnT^O@y*(u4&7fZMFwUS@C(KIw8FcJ|F+DJdy@ z0m#P4ydbj%mCjg%2=p`n(Nl5$p`jrmuN+1|OLdr)=(~>JiO&_9=pa7`M2}CAU-TW5U%Q1Q*T=^;k79gM;(Yk< zA&^?MiPeqj=zthgc#fj?ulBZ|E7suMiI+@(#zQb_ok?^JFg)F;f;#kq(2n-w-G5Y941ttt1*N5aeM$spT`I`pt>Bx24A_)3ugsWPRp0XToU>LMVLVWiS^Pf5(n z&tEEse9?1n)XK5hvAnDb$R%H*UvT%q;SnZtRM$*CLR`G{5sizQUsE|oKp3v_GQS%( z_Rs2O-R0M;;4tfDCD*MRbKJfNdh)EsUD+K39T0mI^`+up#8QysL@UF5jX^UCZFiyT zQOot+U;5OrYe*+9UEf`_32wko*Zt)~KVUo+lA z2Vtek*Rved?t>tRYcI>lmoUDzrTy-rA*TCI_w;r)ft)gp@LLi2RRV)wS6 zy_-cF=$k5pC$O96^v`QUR6^X>BMTU~I-y}$$lsU`%~+Es0!NJDK=AjN;N@b-oWM;{ zAY21vlJ^>eZOenQ&Q5=~U%_dG9_c}IH@cHwFU?aB) zT(>^609Mx)iTVYyIBQH!{EHDu&=baMLp;5wUKtp__^wvY(>3^^fpH%o-iX?L_j#ot zjIjVy_v+80=J`dpljyZV!yvga{xqM=!Jpm6YJsKhs-ms6Cli+V0$d-wKOgu7J9n9T zF+gfloUFFtu<3Ic;80X_*_k3acWX?jFhIzfr`8ibe&xgEIZxN(?;(Syu_4p+IbBSv znZY2;mfh@FW1hIK78ZWU!B9b`4TpQVkrQ`)pfD@;kN$9JHRMb}TF0@0b_BfqUXnh4 zUvSm@w-7p;yZ0Uqt3WdO33uM7T0cSFeMiuCm^GgLBbuJsiG-u;!ZczP*Q|hb&4ZDS za&}qx(_LlA{4>Y`sROh&DqNyZVZV#B^XE^WlGx(P>R+d)zrjy~jP@-mD!lzpb^Ya& z=n0tWkcEk)p6S0Ud;ufN1)V_dhUPm@Nc$*>ri6rXJs>9n8 z2wLk>la~SERua$_M=&NeVGVwlE znZinCCx-2UOEzHqdYQ|K?q1b!=9)&Nd$B*}}yiLRQsttpb9;U3m zT1iQ1X|}z{dJ?@ljAlP_LEl&Ef@3(ksV8{kSrpMOP= z%D=}Qx9jqhpx@mV zJn;B3`s#6>5_EpF^ZL!;vMXaU$F1${gM`RYUEA;_Wb(-$`k%~t9DCRn1OOTuEGLM8 zHqn>q!ns{FOmy?JvQ|1&g1R&rB6yEEuF}Oqmk_$ojwr6#UQXyPJ)IjY>*4BPk?QzO zs#vyuJzV&zUPpj{7^}Z2^A09Q^cVGz^(O|eKpc_NC%$zw`WFxY<6BxTBS z5r`P7=P-5x)U9JH`{FcxjzjbtfnPi|D-5I)=7M)M2pmRSOxqMIuj6Jb`o4l ziha0R$y&LJA141@B>g(&s!=|8HL?vQz+MjfnLCot!`I~6VrYtd6bGVsA23ZIQe1C1 ztc=?j?C0QcfZp8v5qV)=FoCdQ=wc^a{H8`pI}{W^*pOM6pKtCeC2_A<0eSQ8wcllk z2cX?7U8XbK?_Ddy~m1PsV%pFS>Ar0dhb#`WX)W;B7cz-k>!{#u^*Y!iCKEHVr zto=f5m>>KbwO|7Ia{cUfXd7m=C#TCRM%@U{BWH_mePf{4&)*$)w6dS=EqF`~dW4@Lb4qHn`QpxG@hI70NtRo5aG z6$k(tLc`dna8I@~?(g_-A0MMiC@sK!*2dGrBcG$CuI1t&3&GB>rMo0x1xEHF{AtDH zEXH`DA=DX8|Hhe#MWN8DjSCg(vYim#eBG@wsYr$;f{2WFHYqM z{Q!DIx~BCZ#*vgx?2`#eyf8eC z^OY;_!>_M501-e~t79*R^8K1ON!@#DuIw`;s# z4K1F?V=OgcU4C53Hg5yG1W8hAeOudayiHR-XL#@I>>MKP)~nN>9;vN?dMYzxwu@{B zP~zSw1Zv2TN$(u}*GT6KpoK>cz@>q2lfb|Bi1T=wh77&#FC)^iyxFU*bkbnns zz%90+LO-glW@hCn7)XTDLI2&KmzReK#Wx14Pu=#;TE}T?v+4bMs=w;k!wi857* zvYw9%+Mf2iogm8Cv1125cF8pYumpC`z7dj*er-efNI!Us4qoVB8t=Bs;JKU$n!O6y zb3haXO})W!eUL33IdPMlW8Eul7hdJ@1P(+@gCZ zenLgaCXlp%C_zH1HIl|iOzlpqAJJ@IywQVgZ|1&-78O8a_#G-PyVv7A&Ug1K896+s zbBO4#HNrh>+6$|#Ia~NvhA1#OV3HE&ov7TPGK}WMy_x2^GtiR%VlvYS_Qjr#9v)J6aH_aFT=fyNIum%FZ8 z5M5^Xsz3Vg&e&tKcb#fWbV2~3E@GusR7Q-mhmlPa-lipDbJ5R6Yz9w;f5J&3aEZtd zjZDHsf5c6`6mUKW};v%ExoluT-Ili+q9#&6xJ=ckE6Jyc!(l_AhEY57L=g_ z@j$6a{7y=Rq2u1)d@nOO{C9W<#jYwDAS2k=REM+Qh90mQ??fCy#?K3MvB6yngd=F{ zB4yv>JUqGyg^5~(42x_Vs5vY>y8;RAKXE*Ha((yyai1$bJ`y@iuaR~z=D-k{qq1w4 zDR=Nh%vT=N$;CdD^eMZSPNR+<7h~SYav&wCD{HLh8F&`<&9YrriQMCc} z?AOz^FTEm^39)z`KFxbw&w6_fGC>i4ZOy{bLZ1~P4gp)rSKKZ2*3D+zorgkJ*(Ih& z4`g~}2cNgLmUsyo%%JpWR0Wu1b1kz{Q}rdOf3C(a?9=+8zWn{033qB2eI<9hjJm3- zSVl-l$gu#v^)TtN?hkCPtyZ*o_5d^&0bp#B#aDY$=Dz~WRI~KI5VWD$OC}|Pvi6Lo zOcG2stC7hibkiHohdjrzbqc{S!PTGP&vS8oxcmqp4CXF6Hydajmuy8VFWaJC?;qMr zp1RW(ZltZFBPRf}!Mwz8Uh6i9!_G52ZAZ#$&dP4#N|$7SKc-(zFc9pk^&n{IhuAaJ zw-Zo(6Hyma6xGkH+VMh`4^}Oh5wM|irX9y0+En39VXm<@`4-0K9p11c(L! z>J@v?~$FARl!>z)ol-nY1)=odd0&7l)qn>M(?t7n$h2b=FrZ1f+wSoy#jKH z8BQpLBy;B5lEZ0<3zjma_8N%-Fq^awZ9uVSo_7Ib&*Yz^y{C{W%{* z!g(6Z(s>tv9}I>qqrJTuSkBCYo_%wDb&03^!R{qI0J3b)I_P;$Q}xQg2LkChk)EFJ zI$UMOM6Q#taR|&Y*pB#C(Qh~8tu1y-4`L82Cs5!n4Bm5fB>0AkDv!72h1QHPN@iqyLj;S2< z^0f$XO)D=1{cP;Oj1?(X&XNX#%tjG_qgix-T%fWP!mmj)azg^wTVs1x=XDRyd43E_ z+n4wuI~3{ge_v{J#dN7bUWCUt+a4{Tef^Jp@MuR8*=36XBg-BU0B07JXW%iszsZYt zu$#>Q0Qs^U;g^08ovDUpFJB`DU6#wcC4uH^^r(#xt~h~s3M;I3Qt8#-6rVzmm*JK! z^8+u=5qnU84|-2j`@5)=Dt{GVGL6g|e^z`_LyHz6Ysq`K)v(@zg}(Qrqq;24!~m&$ zz2`u)0v~`*q3B#O@YYyb(jd0s(_x^j4&tzXV9`IzU=~1CAmp~I;rErD zC&7T*yVq(D2>yiz|G`V9jqv3A@q4{$OZXuKGd4GBa)cATg25f=-yQ3%`|4_EVk7%w z#dVtdCl<>CmeRuHnRoY4${es(>%n&TJ=C9HBjZ~_vx@zqA^n2lv)mMb&(P!+$~$gF zJF$G=QTZ9$Q<0fD+LjaOUfebc5%J+Uw?o8VIZsz}q>C%*QH_r6CMrlT*FIDk*2tb3 z>!QubXF;$XiN>2Hescq1uUyQ;)QMaFU;O)@hlBqAJkdXh%CCY?0FxrU<$$NK=I8t2@6(GfqJFouWeRshHRCS8;6ZmADhjcK%E?jDVv9v}o_?uzyYORqo6jxFZh_dg__>*02&MU%jR;pvW?e?wQoum)HC_19l9QhUmS zAScEy{{(*o@>%~~%?W@qmA?cg^y91APm%aF`NLn#&T=NOYlR>WiQoieqrf^k%K6j4 zk5c^)ClIhMBA4NGpK`OacN`?iopRq;2`utXRm0)U`cE$Z#9hkEKwLb9 zf}fBth56~E^MV;@;<)@;OT=}<0e4T*l%u2Lh5#4$jgmefth{*OmOL1Pj95sMPS9(Tk24$d2pwGOEqDEuY7{K zuoNUt`Qs;Mr55-9FDLe$8z)Mjhg59=6 z1!@S~Dd4Mu3<@N8=oE$jf_gjxO{Q$3birGlx0Pq74L*;6&Q!+Ivl~3ZUmOPlP?a8a zx%4aXco_mMG7Y23?;+iTX{O^3+EE4+9^my3@=)NxJg0Z>-d&3QbeLEwz{OsE9IJYa zHSMtxTKWYlxUGd<7hlvMI~)Z^ZwxO&9k`Gr!qHKm=fQ$omQ>^gn~`__gIT`@0qn$ilmRQ-Zw9E@Ic!rr*G(MsLY=iDZC}P{$K&&v5=RWy)8> zjsh5VE>J-nFXA{V)8rUviqw%LV#bDF*b)tQODa!gE}+WpMX8I=(@6BPFA_zFKe94M>nzI^{!a>kA?B2cmrrk2maj30i<{sv;mabie9c zK7F+CJTgQPA-7WMjxia{LI?N80RfloTOssKCOh>wJdcDL2=CWc0K}a^cl>JDj^($8 z!}=$Nz!M(Y9u0A1m(3U9O~61IL93Z)8u0m+4(HU@Q{V}`^;Y9EPR`CXZ)^OU*5M=H z$B`Uw)cl_}`2W|l{Ua|P@xSnF1LpFdF5n@rEZ!#t`8>k*Dt=uz7HiYhH+3v~3^yx7 zpF+tlYxKU^@UJAM`~F53il>Vogq9J!u@{h%S|VY7o6 zleb}wVmxdn$1BAPDMNaR!hnQ0+4iDm&(eTdXra^l>S3ug3N%0LF8&^5-h*i65J4Mj za?%M5s1#Aah%F38*0yciR))ZWtP)*T4g-&zy?rU--2R5u_P;n|Ey4i<%*|j>S^kuf z=D#*@@8TZBdyqwDZ72Jyv3sTMbqIjejtwD(@yUtluTJic7a-k-XES0# z5kC(3Rgb|PMK<>k<+2t;zL&7hy>jXDXI|BUK# zagggG@kP!wXqL7ij_zh8EQ9FXeTl9kFdffLOe@cGW@F}EN9aIyir#t^__}B_Yi`aL zJTIgc?hN}3WCVgkN_U(S7qz+YWZD|od2T-t65aRCP58}@H zFtZ+4vzrITd?hP|h8U1#r*=Q!I;yKIx<7tEO%QR>`bt-t4wOQ&!S~f#h5%C1W=f6_ zQyxx!&9ftdSEIVhpg$RoJ50RYt|%obsf@IVAl$6{UD?uqm~qmlZD%eBXvTGkg`q0@ zO27HLc^bqQFdE(MYF*IG`id5pIfYKMb9}R3U`#R`Wmxv)Id<;;YgB|#zh)!OW z&mzMAu7^(LlVe@_;Dz(}Y^Fm$sl)6@Kp0R`@W+0eJL6;kA!AjfQG?`s z!U;sG0H?^1m2m(9*NMr6Y2U))$j>P!o_SweW&~Q+E0h>SKq*Z2BaGoEI1P0HEo&^W zfl_TvF6~DuwDN4F_cIF|n85!P3*mW@>NpD=O0G}>5-Ke0C!L$(QIOvD8~7;AzY#zO zGlm-1K+-Hph|lCB4!kBX>v&yMEfw->P#QR>#Bw4!Wo2e=gA7aUu6%~Wa2{~!)z{V5 zs_f~#PzP3`vaD5Zfp!!v`%zvn92UpYBZnEiu-d@B_HGX?qK{XIvWg%lxWH=l0!Dl_$A5683DKK~kc z&oPxn{E*<{N&I^nqNcupa=)vHE>+h1fIXKe3WPrHst=+-ArO2!igA4!m5}-svJj=> z!}-?3@Q{JnzZFC-dw6V|)P(eojjEcO41_wI8@LBEIgf|rxE^(18oB^*lQaq^XYN7D zl%Ip*O*$G;@DBGs_OwL|PJg zKIU%M%*@l(WU{tlaoLssWg-{SSN^{zuKagQWX7;IFp&os<#k{xFRy`*`7rR9&nqC1 z7NnE7***m3jw2r?29QqBQ}(7)L7X+T{=8hMkNKzA9f>P z#s*S?97ii`XHQ@9?XRBg~!#a8ly2M`6!o(-MH!? zh9@KNHMm|{(9eIn|sBQo|$eLdMo3y)Opzjw+;L6$p^2U<;PgOmf2)= zVOL9r`AWX$*Oy-#{p;0rwtigE{k>>3+t?+h-JsW6?be7)N5bds*4RnhNOZK;)u}tR ziP|Qwefhs$je;TN|{QMrZ6;gdu5o0+v#b)QrOn-^TJ)xrC1HAZk-I??7b_cQ^Ki(K0KDDn8 zCETt;9X)gAm-r3=pSbIk1Ib~tgCAm}qYdWfUN7gJ^EZ33Wz6BwR4y%V9ypWkWx6hiytLs4?q(2sut1Gnnp5#D6t(Vx;;$k!(uY};_#a0C$ zrBMj{EUA-T!Q8!kCQhdr@>VsD>q%v)RblxFJkd|86Yx z^v+ekctq#hj0*pe4-fxU-O_{9Olh5dpocma!_(zVE(#d3wI0BsFaHPTr1-wwXF8H$E-2WueiCIFpBxv2I zvZ*EfSM3YV&RKopua?8TylB!}qV74D{OZ-K;npyZc!zBb<7D`KlFl62i>JX*oIlxo6O2bG>(LrlpVXm0Pa;%0hl^%ZCpnh}^yd zX!GXx65Kq9W&--)-S^MFt;{W#xR#_($1H;L^>yDd`2wghI`_9%&-SI(+N04=3xFoB zXs*?P>uKZMFBuhBJ%qq>Ct>@kec3ecFEykJDeNYQN<{YESDL`uRP(J>ksziP-^*@m8OW+&-(E&VfeU zIgR;6Q=#M0v&$v%vk6^`dD~6}{fsq1OIi6}R9=noi)!#r(dTlBpy1=9`Duw}7T4wS z^$%~g855=xA>YlHRpg*PR25}2xZ z+`;p$s5!jPmn8zcpK{CPJbdu-J80U^41OMl zeLtu$)!jZVUDrh{dbu3$OMJ0}HH#{I9shaHYuZD@sF!|s$dGIZ!Qe2_@C zeJ>+<-JtbLY2#qo8#Ob(Pu$1%ps(*E85-2(sMSD5e+J<%18bfyyLvHm8vU%+xf?uQ z;G%5OG`p{rBP9SkXZMX1oq+Z+j=RKw*v7gup#mer!wt}CA2h>G7(@$thKVbR?c0uL z_GFb1W(v>C0fR4^uopT~VzM7)#-i{bw2nkA^wa=ohzQoDeAMQ+S@cs+iiF{zhr-|p zCBxWfJv@iv4ln@iLKCq_M14>%GVDmV%Mn{Rk*qZxszra~cCJM9&05K)J;QZjSzUEU z)$RvfES`b+0aAP@YFSL;*SL-aIl&%ODU6wCu;y51S94l3jhbbeMGCb}w$hOQj siv2j|fBXqN|ICKzN 0 ? { diff: PNG.sync.write(diff) } : null; +} + +/** + * @param {?Object} actual + * @param {!Buffer} expectedBuffer + * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} + */ +function compareText(actual, expectedBuffer) { + if (typeof actual !== 'string') + return { errorMessage: 'Actual result should be string' }; + const expected = expectedBuffer.toString('utf-8'); + if (expected === actual) return null; + const diff = new Diff(); + const result = diff.main(expected, actual); + diff.cleanupSemantic(result); + let html = diff.prettyHtml(result); + const diffStylePath = path.join(__dirname, 'diffstyle.css'); + html = `` + html; + return { + diff: html, + diffExtension: '.html', + }; +} + +/** + * @param {?Object} actual + * @param {string} goldenName + * @returns {!{pass: boolean, message: (undefined|string)}} + */ +function compare(goldenPath, outputPath, actual, goldenName) { + 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); + const comparator = GoldenComparators[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.diff) { + const diffPath = addSuffix(actualPath, '-diff', result.diffExtension); + fs.writeFileSync(diffPath, result.diff); + } + + let message = goldenName + ' mismatch!'; + if (result.errorMessage) message += ' ' + result.errorMessage; + return { + pass: false, + message: message + ' ' + messageSuffix, + }; + + function ensureOutputDir() { + if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); + } +} + +/** + * @param {string} filePath + * @param {string} suffix + * @param {string=} customExtension + * @returns {string} + */ +function addSuffix(filePath, suffix, customExtension) { + const dirname = path.dirname(filePath); + const ext = path.extname(filePath); + const name = path.basename(filePath, ext); + return path.join(dirname, name + suffix + (customExtension || ext)); +} diff --git a/test/headful.spec.ts b/test/headful.spec.ts new file mode 100644 index 0000000..5379941 --- /dev/null +++ b/test/headful.spec.ts @@ -0,0 +1,269 @@ +/** + * 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 path from 'path'; +import os from 'os'; +import fs from 'fs'; +import { promisify } from 'util'; +import expect from 'expect'; +import { + getTestState, + describeChromeOnly, + itFailsWindows, +} from './mocha-utils'; // eslint-disable-line import/extensions +import rimraf from 'rimraf'; + +const rmAsync = promisify(rimraf); +const mkdtempAsync = promisify(fs.mkdtemp); + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +const extensionPath = path.join(__dirname, 'assets', 'simple-extension'); + +describeChromeOnly('headful tests', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20 * 1000); + + let headfulOptions; + let headlessOptions; + let extensionOptions; + let forcedOopifOptions; + + 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' + )}`, + ], + }); + }); + + describe('HEADFUL', function () { + it('background_page target type should be available', async () => { + const { puppeteer } = getTestState(); + const browserWithExtension = await puppeteer.launch(extensionOptions); + const page = await browserWithExtension.newPage(); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + (target) => target.type() === 'background_page' + ); + await page.close(); + await browserWithExtension.close(); + expect(backgroundPageTarget).toBeTruthy(); + }); + it('target.page() should return a background_page', async function () { + const { puppeteer } = getTestState(); + const browserWithExtension = await puppeteer.launch(extensionOptions); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + (target) => target.type() === 'background_page' + ); + const page = await backgroundPageTarget.page(); + expect(await page.evaluate(() => 2 * 3)).toBe(6); + expect(await page.evaluate(() => globalThis.MAGIC)).toBe(42); + await browserWithExtension.close(); + }); + it('should have default url when launching browser', async function () { + const { puppeteer } = getTestState(); + const browser = await puppeteer.launch(extensionOptions); + const pages = (await browser.pages()).map((page) => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + itFailsWindows( + '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 mkdtempAsync(TMP_FOLDER); + // Write a cookie in headful chrome + const headfulBrowser = await puppeteer.launch( + Object.assign({ userDataDir }, headfulOptions) + ); + const headfulPage = await headfulBrowser.newPage(); + await headfulPage.goto(server.EMPTY_PAGE); + await headfulPage.evaluate( + () => + (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 puppeteer.launch( + Object.assign({ userDataDir }, headlessOptions) + ); + const headlessPage = await headlessBrowser.newPage(); + await headlessPage.goto(server.EMPTY_PAGE); + const cookie = await headlessPage.evaluate(() => document.cookie); + await headlessBrowser.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + expect(cookie).toBe('foo=true'); + } + ); + // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548 + xit('OOPIF: should report google.com frame', async () => { + const { server, puppeteer } = getTestState(); + + // https://google.com is isolated by default in Chromium embedder. + const browser = await puppeteer.launch(headfulOptions); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', (r) => 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) => (frame.onload = x)); + }); + await page.waitForSelector('iframe[src="https://google.com/"]'); + const urls = page + .frames() + .map((frame) => 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 puppeteer.launch(forcedOopifOptions); + const page = await browser.newPage(); + + // Setup our session listeners to observe OOPIF activity. + const session = await page.target().createCDPSession(); + const networkEvents = []; + const otherSessions = []; + await session.send('Target.setAutoAttach', { + autoAttach: true, + flatten: true, + waitForDebuggerOnStart: true, + }); + session.connection().on('sessionattached', async (session) => { + otherSessions.push(session); + + session.on('Network.requestWillBeSent', (params) => + networkEvents.push(params) + ); + await session.send('Network.enable'); + }); + + // 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) => { + 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.runIfWaitingForDebugger'); + await iframeSession.send('Runtime.evaluate', { + expression: `fetch('/fetch')`, + awaitPromise: true, + }); + await browser.close(); + + const requests = networkEvents.map((event) => 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 puppeteer.launch(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 puppeteer.launch( + Object.assign({ devtools: true }, headfulOptions) + ); + const context = await browser.createIncognitoBrowserContext(); + await Promise.all([ + context.newPage(), + context.waitForTarget((target) => target.url().includes('devtools://')), + ]); + await browser.close(); + }); + }); + + describe('Page.bringToFront', function () { + it('should work', async () => { + const { puppeteer } = getTestState(); + const browser = await puppeteer.launch(headfulOptions); + const page1 = await browser.newPage(); + const page2 = await browser.newPage(); + + await page1.bringToFront(); + expect(await page1.evaluate(() => document.visibilityState)).toBe( + 'visible' + ); + expect(await page2.evaluate(() => document.visibilityState)).toBe( + 'hidden' + ); + + await page2.bringToFront(); + expect(await page1.evaluate(() => document.visibilityState)).toBe( + 'hidden' + ); + expect(await page2.evaluate(() => document.visibilityState)).toBe( + 'visible' + ); + + await page1.close(); + await page2.close(); + await browser.close(); + }); + }); +}); diff --git a/test/idle_override.spec.ts b/test/idle_override.spec.ts new file mode 100644 index 0000000..18e7da3 --- /dev/null +++ b/test/idle_override.spec.ts @@ -0,0 +1,94 @@ +/** + * 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeFailsFirefox('Emulate idle state', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + async function getIdleState() { + const { page } = getTestState(); + + const stateElement = await page.$('#state'); + return await page.evaluate((element: HTMLElement) => { + 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/test/ignorehttpserrors.spec.ts b/test/ignorehttpserrors.spec.ts new file mode 100644 index 0000000..de6f050 --- /dev/null +++ b/test/ignorehttpserrors.spec.ts @@ -0,0 +1,135 @@ +/** + * 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, + describeFailsFirefox, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +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; + let context; + let page; + + before(async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign( + { ignoreHTTPSErrors: true }, + defaultBrowserOptions + ); + browser = await puppeteer.launch(options); + }); + + after(async () => { + await browser.close(); + browser = null; + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + context = null; + page = null; + }); + + describeFailsFirefox('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.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 = []; + page.on('response', (response) => responses.push(response)); + const [serverRequest] = await Promise.all([ + httpsServer.waitForRequest('/plzredirect'), + page.goto(httpsServer.PREFIX + '/plzredirect'), + ]); + expect(responses.length).toBe(2); + expect(responses[0].status()).toBe(302); + const securityDetails = responses[0].securityDetails(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + }); + }); + + it('should work', async () => { + const { httpsServer } = getTestState(); + + let error = null; + const response = await page + .goto(httpsServer.EMPTY_PAGE) + .catch((error_) => (error = error_)); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + }); + itFailsFirefox('should work with request interception', async () => { + const { httpsServer } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const response = await page.goto(httpsServer.EMPTY_PAGE); + expect(response.status()).toBe(200); + }); + itFailsFirefox('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().length).toBe(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/test/input.spec.ts b/test/input.spec.ts new file mode 100644 index 0000000..9244356 --- /dev/null +++ b/test/input.spec.ts @@ -0,0 +1,343 @@ +/** + * 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt'); + +describe('input tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describeFailsFirefox('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._inputEvents = []; + e.addEventListener('change', (ev) => + globalThis._inputEvents.push(ev.type) + ); + e.addEventListener('input', (ev) => + globalThis._inputEvents.push(ev.type) + ); + }, input); + await input.uploadFile(filePath); + expect( + await page.evaluate((e: HTMLInputElement) => e.files[0].name, input) + ).toBe('file-to-upload.txt'); + expect( + await page.evaluate((e: HTMLInputElement) => e.files[0].type, input) + ).toBe('text/plain'); + expect(await page.evaluate(() => globalThis._inputEvents)).toEqual([ + 'input', + 'change', + ]); + expect( + await page.evaluate((e: HTMLInputElement) => { + const reader = new FileReader(); + const promise = new Promise((fulfill) => (reader.onload = fulfill)); + reader.readAsText(e.files[0]); + return promise.then(() => reader.result); + }, input) + ).toBe('contents of the file'); + }); + }); + + describeFailsFirefox('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, puppeteer } = getTestState(); + + let error = null; + await page + .waitForFileChooser({ timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(1); + let error = null; + await page.waitForFileChooser().catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(0); + let error = null; + await page + .waitForFileChooser({ timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with no timeout', async () => { + const { page } = getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser({ timeout: 0 }), + page.evaluate(() => + 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: HTMLInputElement) => input.click()), + ]); + expect(fileChooser1 === fileChooser2).toBe(true); + }); + }); + + describeFailsFirefox('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]), + new Promise((x) => page.once('metrics', x)), + ]); + expect( + await page.$eval( + 'input', + (input: HTMLInputElement) => input.files.length + ) + ).toBe(1); + expect( + await page.$eval( + 'input', + (input: HTMLInputElement) => input.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) => chooser.accept([FILE_TO_UPLOAD])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + const reader = new FileReader(); + const promise = new Promise((fulfill) => (reader.onload = fulfill)); + reader.readAsText(picker.files[0]); + return promise.then(() => 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) => chooser.accept([FILE_TO_UPLOAD])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + return picker.files.length; + }) + ).toBe(1); + page.waitForFileChooser().then((chooser) => chooser.accept([])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + return picker.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 = null; + await chooser + .accept([ + path.relative( + process.cwd(), + __dirname + '/assets/file-to-upload.txt' + ), + path.relative(process.cwd(), __dirname + '/assets/pptr.png'), + ]) + .catch((error_) => (error = error_)); + expect(error).not.toBe(null); + }); + it('should fail for non-existent files', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error = null; + await chooser + .accept(['file-does-not-exist.txt']) + .catch((error_) => (error = error_)); + expect(error).not.toBe(null); + }); + 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: HTMLInputElement) => input.click()), + ]); + await fileChooser.accept([]); + let error = null; + await fileChooser.accept([]).catch((error_) => (error = error_)); + expect(error.message).toBe( + 'Cannot accept FileChooser which is already handled!' + ); + }); + }); + + describeFailsFirefox('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: HTMLInputElement) => input.click()), + ]); + await fileChooser1.cancel(); + // If this resolves, than we successfully canceled file chooser. + await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.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: HTMLInputElement) => input.click()), + ]); + await fileChooser.cancel(); + let error = null; + + try { + fileChooser.cancel(); + } catch (error_) { + error = error_; + } + + expect(error.message).toBe( + 'Cannot cancel FileChooser which is already handled!' + ); + }); + }); + + describeFailsFirefox('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/test/jshandle.spec.ts b/test/jshandle.spec.ts new file mode 100644 index 0000000..7978d4a --- /dev/null +++ b/test/jshandle.spec.ts @@ -0,0 +1,300 @@ +/** + * 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, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('JSHandle', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluateHandle', function () { + it('should work', async () => { + const { page } = getTestState(); + + const windowHandle = await page.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + it('should accept object handle as an argument', async () => { + const { page } = getTestState(); + + const navigatorHandle = await page.evaluateHandle(() => navigator); + const text = await page.evaluate( + (e: Navigator) => e.userAgent, + navigatorHandle + ); + expect(text).toContain('Mozilla'); + }); + it('should accept object handle to primitive types', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => 5); + const isFive = await page.evaluate((e) => Object.is(e, 5), aHandle); + expect(isFive).toBeTruthy(); + }); + it('should warn on nested object handles', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => document.body); + let error = null; + await page + // @ts-expect-error we are deliberately passing a bad type here (nested object) + .evaluateHandle((opts) => opts.elem.querySelector('p'), { + elem: aHandle, + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Are you passing a nested JSHandle?'); + }); + it('should accept object handle to unserializable value', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => Infinity); + expect(await page.evaluate((e) => Object.is(e, Infinity), aHandle)).toBe( + true + ); + }); + it('should use the same JS wrappers', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + globalThis.FOO = 123; + return window; + }); + expect(await page.evaluate((e: { FOO: number }) => e.FOO, aHandle)).toBe( + 123 + ); + }); + it('should work with primitives', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + globalThis.FOO = 123; + return window; + }); + expect(await page.evaluate((e: { FOO: number }) => e.FOO, aHandle)).toBe( + 123 + ); + }); + }); + + describe('JSHandle.getProperty', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ + 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(() => ({ 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(() => ['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(() => 'foo'); + const json = await aHandle.jsonValue(); + expect(json).toEqual('foo'); + }); + + itFailsFirefox('should not work with dates', async () => { + const { page } = getTestState(); + + const dateHandle = await page.evaluateHandle( + () => 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, isChrome } = getTestState(); + + const windowHandle = await page.evaluateHandle('window'); + let error = null; + await windowHandle.jsonValue().catch((error_) => (error = error_)); + if (isChrome) + expect(error.message).toContain('Object reference chain is too long'); + else expect(error.message).toContain('Object is not serializable'); + }); + }); + + describe('JSHandle.getProperties', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ + 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(() => 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(() => 2); + const element = aHandle.asElement(); + expect(element).toBeFalsy(); + }); + it('should return ElementHandle for TextNodes', async () => { + const { page } = getTestState(); + + await page.setContent('

'); + const aHandle = await page.evaluateHandle( + () => document.querySelector('div').firstChild + ); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + expect( + await page.evaluate( + (e: HTMLElement) => e.nodeType === Node.TEXT_NODE, + element + ) + ); + }); + }); + + describe('JSHandle.toString', function () { + it('should work for primitives', async () => { + const { page } = getTestState(); + + const numberHandle = await page.evaluateHandle(() => 2); + expect(numberHandle.toString()).toBe('JSHandle:2'); + const stringHandle = await page.evaluateHandle(() => 'a'); + expect(stringHandle.toString()).toBe('JSHandle:a'); + }); + it('should work for complicated objects', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => 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/test/keyboard.spec.ts b/test/keyboard.spec.ts new file mode 100644 index 0000000..5aff0f9 --- /dev/null +++ b/test/keyboard.spec.ts @@ -0,0 +1,408 @@ +/** + * 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 utils from './utils.js'; +import os from 'os'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.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(() => document.querySelector('textarea').value) + ).toBe(text); + }); + itFailsFirefox('should press the metaKey', async () => { + const { page, isFirefox } = getTestState(); + + await page.evaluate(() => { + (window as any).keyPromise = new Promise((resolve) => + document.addEventListener('keydown', (event) => 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(() => 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(() => 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(() => document.querySelector('textarea').value) + ).toBe('Hello World!'); + }); + 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(() => document.querySelector('textarea').value) + ).toBe('a'); + + await page.evaluate(() => + window.addEventListener('keydown', (e) => e.preventDefault(), true) + ); + + await textarea.press('b'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('a'); + }); + itFailsFirefox( + '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(() => document.querySelector('textarea').value) + ).toBe('ё'); + } + ); + itFailsFirefox('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(() => document.querySelector('textarea').value) + ).toBe('嗨'); + await page.evaluate(() => + window.addEventListener('keydown', (e) => e.preventDefault(), true) + ); + await page.keyboard.sendCharacter('a'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('嗨a'); + }); + itFailsFirefox('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(() => globalThis.getResult())).toBe( + 'Keydown: ' + + modifierKey + + ' ' + + modifierKey + + 'Left ' + + modifierCode + + ' [' + + modifierKey + + ']' + ); + await keyboard.down('!'); + // Shift+! will generate a keypress + if (modifierKey === 'Shift') + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ! Digit1 49 [' + + modifierKey + + ']\nKeypress: ! Digit1 33 33 [' + + modifierKey + + ']' + ); + else + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ! Digit1 49 [' + modifierKey + ']' + ); + + await keyboard.up('!'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: ! Digit1 49 [' + modifierKey + ']' + ); + await keyboard.up(modifierKey); + expect(await page.evaluate(() => globalThis.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(() => globalThis.getResult())).toBe( + 'Keydown: Control ControlLeft 17 [Control]' + ); + await keyboard.down('Alt'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: Alt AltLeft 18 [Alt Control]' + ); + await keyboard.down(';'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ; Semicolon 186 [Alt Control]' + ); + await keyboard.up(';'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: ; Semicolon 186 [Alt Control]' + ); + await keyboard.up('Control'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: Control ControlLeft 17 [Alt]' + ); + await keyboard.up('Alt'); + expect(await page.evaluate(() => globalThis.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(() => globalThis.getResult())).toBe( + [ + 'Keydown: ! Digit1 49 []', + 'Keypress: ! Digit1 33 33 []', + 'Keyup: ! Digit1 49 []', + ].join('\n') + ); + await page.keyboard.type('^'); + expect(await page.evaluate(() => globalThis.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(() => globalThis.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(() => globalThis.textarea.value)).toBe( + 'He Wrd!' + ); + }); + itFailsFirefox('should specify repeat property', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => + document + .querySelector('textarea') + .addEventListener('keydown', (e) => (globalThis.lastEvent = e), true) + ); + await page.keyboard.down('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + await page.keyboard.press('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(true); + + await page.keyboard.down('b'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + await page.keyboard.down('b'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(true); + + await page.keyboard.up('a'); + await page.keyboard.down('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + }); + itFailsFirefox('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); + }); + itFailsFirefox('should specify location', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + (event) => (globalThis.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_) => error_); + expect(error.message).toBe('Unknown key: "NotARealKey"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('ё').catch((error_) => error_); + expect(error && error.message).toBe('Unknown key: "ё"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('😊').catch((error_) => error_); + expect(error && error.message).toBe('Unknown key: "😊"'); + }); + itFailsFirefox('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: HTMLInputElement) => textarea.value + ) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + itFailsFirefox('should type emoji into an iframe', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.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: HTMLInputElement) => textarea.value + ) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + itFailsFirefox('should press the meta key', async () => { + const { page, isFirefox } = getTestState(); + + await page.evaluate(() => { + globalThis.result = null; + document.addEventListener('keydown', (event) => { + globalThis.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/test/launcher.spec.ts b/test/launcher.spec.ts new file mode 100644 index 0000000..b434792 --- /dev/null +++ b/test/launcher.spec.ts @@ -0,0 +1,718 @@ +/** + * 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 os from 'os'; +import path from 'path'; +import sinon from 'sinon'; +import { promisify } from 'util'; +import Protocol from 'devtools-protocol'; +import { + getTestState, + itChromeOnly, + itFailsFirefox, + itOnlyRegularInstall, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; +import expect from 'expect'; +import rimraf from 'rimraf'; +import { Page } from '../lib/cjs/puppeteer/common/Page.js'; + +const rmAsync = promisify(rimraf); +const mkdtempAsync = promisify(fs.mkdtemp); +const readFileAsync = promisify(fs.readFile); +const statAsync = promisify(fs.stat); +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('BrowserFetcher', function () { + it('should download and extract chrome linux binary', async () => { + const { server, puppeteer } = getTestState(); + + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX, + }); + const expectedRevision = '123456'; + let revisionInfo = browserFetcher.revisionInfo(expectedRevision); + server.setRoute( + revisionInfo.url.substring(server.PREFIX.length), + (req, res) => { + server.serveFile(req, res, '/chromium-linux.zip'); + } + ); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(browserFetcher.product()).toBe('chrome'); + expect(!!browserFetcher.host()).toBe(true); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload(expectedRevision)).toBe(true); + + revisionInfo = await browserFetcher.download(expectedRevision); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe( + 'LINUX BINARY\n' + ); + const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755; + expect( + (await statAsync(revisionInfo.executablePath)).mode & 0o777 + ).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual([ + expectedRevision, + ]); + await browserFetcher.remove(expectedRevision); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + it('should download and extract firefox linux binary', async () => { + const { server, puppeteer } = getTestState(); + + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX, + product: 'firefox', + }); + const expectedVersion = '75.0a1'; + let revisionInfo = browserFetcher.revisionInfo(expectedVersion); + server.setRoute( + revisionInfo.url.substring(server.PREFIX.length), + (req, res) => { + server.serveFile( + req, + res, + `/firefox-${expectedVersion}.en-US.linux-x86_64.tar.bz2` + ); + } + ); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(browserFetcher.product()).toBe('firefox'); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload(expectedVersion)).toBe(true); + + revisionInfo = await browserFetcher.download(expectedVersion); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe( + 'FIREFOX LINUX BINARY\n' + ); + const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755; + expect( + (await statAsync(revisionInfo.executablePath)).mode & 0o777 + ).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual([ + expectedVersion, + ]); + await browserFetcher.remove(expectedVersion); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + }); + + 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_) => error_); + await server.waitForRequest('/one-style.css'); + remote.disconnect(); + const error = await navigationPromise; + expect(error.message).toBe( + 'Navigation failed because browser has disconnected!' + ); + 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_) => 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) => error), + newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => 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('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 = null; + const neverResolves = page + .evaluate(() => new Promise(() => {})) + .catch((error_) => (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 = null; + const options = Object.assign({}, defaultBrowserOptions, { + executablePath: 'random-invalid-path', + }); + await puppeteer.launch(options).catch((error) => (waitError = error)); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const userDataDir = await mkdtempAsync(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 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir argument', async () => { + const { isChrome, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(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 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir option should restore state', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(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(() => (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(() => localStorage.hey)).toBe('hello'); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + // This mysteriously fails on Windows on AppVeyor. See + // https://github.com/puppeteer/puppeteer/issues/4111 + xit('userDataDir option should restore cookies', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(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( + () => + (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(() => document.cookie)).toBe( + 'doSomethingOnlyOnce=true' + ); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(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'); + expect(puppeteer.defaultArgs()).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'); + }); + itFailsFirefox('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); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should filter out ignored default arguments', 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 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) => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + itFailsFirefox( + '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.length).toBe(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 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(); + }); + itChromeOnly( + 'should launch Chrome properly with --no-startup-window and waitForInitialPage=false', + async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = { + args: ['--no-startup-window'], + 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, + }; + const browser = await puppeteer.launch(options); + const pages = await browser.pages(); + expect(pages.length).toBe(0); + await browser.close(); + } + ); + }); + + describe('Puppeteer.launch', function () { + let productName; + + before(async () => { + const { puppeteer } = getTestState(); + productName = puppeteer._productName; + }); + + after(async () => { + const { puppeteer } = getTestState(); + // @ts-expect-error launcher is a private property that users can't + // touch, but for testing purposes we need to reset it. + puppeteer._lazyLauncher = undefined; + puppeteer._productName = productName; + }); + + 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('falls back to launching chrome if there is an unknown product but logs a warning', async () => { + const { puppeteer } = getTestState(); + const consoleStub = sinon.stub(console, 'warn'); + // @ts-expect-error purposeful bad input + const browser = await puppeteer.launch({ product: 'SO_NOT_A_PRODUCT' }); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Chrome'); + expect(consoleStub.callCount).toEqual(1); + expect(consoleStub.firstCall.args).toEqual([ + 'Warning: unknown product name SO_NOT_A_PRODUCT. Falling back to chrome.', + ]); + }); + + itOnlyRegularInstall( + '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(() => 7 * 8)).toBe(56); + otherBrowser.disconnect(); + + const secondPage = await originalBrowser.newPage(); + expect(await secondPage.evaluate(() => 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([ + utils.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 = null; + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE).catch((error_) => (error = error_)), + ]); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + expect(response.securityDetails()).toBeTruthy(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(response.securityDetails().protocol()).toBe(protocol); + await page.close(); + await browser.close(); + }); + 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) => + !targetInfo.url.includes('should-be-ignored'), + }); + + const pages = await browser.pages(); + + await page2.close(); + await page1.close(); + await browser.close(); + + expect(pages.map((p: Page) => p.url()).sort()).toEqual([ + 'about:blank', + server.EMPTY_PAGE, + ]); + }); + itFailsFirefox( + '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) => + page.url() === server.PREFIX + '/frames/nested-frames.html' + ); + expect(utils.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(() => 7 * 8)).toBe(56); + await browser.close(); + } + ); + // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410 + itFailsFirefox( + 'should be able to connect to the same page simultaneously', + async () => { + const { puppeteer } = getTestState(); + + const browserOne = await puppeteer.launch(); + const browserTwo = await puppeteer.connect({ + browserWSEndpoint: browserOne.wsEndpoint(), + }); + const [page1, page2] = await Promise.all([ + new Promise((x) => + browserOne.once('targetcreated', (target) => x(target.page())) + ), + browserTwo.newPage(), + ]); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + expect(await page2.evaluate(() => 7 * 6)).toBe(42); + await browserOne.close(); + } + ); + it('should be able to reconnect', async () => { + const { puppeteer, server } = getTestState(); + const browserOne = await puppeteer.launch(); + 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) => 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); + }); + }); + }); + + describe('Browser target events', function () { + itFailsFirefox('should work', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const browser = await puppeteer.launch(defaultBrowserOptions); + const events = []; + browser.on('targetcreated', () => events.push('CREATED')); + browser.on('targetchanged', () => events.push('CHANGED')); + browser.on('targetdestroyed', () => events.push('DESTROYED')); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); + 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', () => ++disconnectedOriginal); + remoteBrowser1.on('disconnected', () => ++disconnectedRemote1); + remoteBrowser2.on('disconnected', () => ++disconnectedRemote2); + + await Promise.all([ + utils.waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), + ]); + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + utils.waitEvent(remoteBrowser1, 'disconnected'), + utils.waitEvent(originalBrowser, 'disconnected'), + originalBrowser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + }); + }); +}); diff --git a/test/mocha-ts-require.js b/test/mocha-ts-require.js new file mode 100644 index 0000000..a0ac64f --- /dev/null +++ b/test/mocha-ts-require.js @@ -0,0 +1,11 @@ +const path = require('path'); + +require('ts-node').register({ + /** + * We ignore the lib/ directory because that's already been TypeScript + * compiled and checked. So we don't want to check it again as part of running + * the unit tests. + */ + ignore: ['lib/*', 'node_modules'], + project: path.join(__dirname, 'tsconfig.test.json'), +}); diff --git a/test/mocha-utils.ts b/test/mocha-utils.ts new file mode 100644 index 0000000..238addf --- /dev/null +++ b/test/mocha-utils.ts @@ -0,0 +1,309 @@ +/** + * 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 { TestServer } from '../utils/testserver/index.js'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import sinon from 'sinon'; +import puppeteer from '../lib/cjs/puppeteer/node.js'; +import { + Browser, + BrowserContext, +} from '../lib/cjs/puppeteer/common/Browser.js'; +import { Page } from '../lib/cjs/puppeteer/common/Page.js'; +import { PuppeteerNode } from '../lib/cjs/puppeteer/node/Puppeteer.js'; +import utils from './utils.js'; +import rimraf from 'rimraf'; +import expect from 'expect'; + +import { trackCoverage } from './coverage-utils.js'; +import Protocol from 'devtools-protocol'; + +const setupServer = async () => { + const assetsPath = path.join(__dirname, 'assets'); + const cachedPath = path.join(__dirname, 'assets', 'cached'); + + const port = 8907; + const server = await TestServer.create(assetsPath, 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 httpsPort = port + 1; + const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort); + 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 => + state as PuppeteerTestState; + +const product = + process.env.PRODUCT || process.env.PUPPETEER_PRODUCT || 'Chromium'; + +const alternativeInstall = process.env.PUPPETEER_ALT_INSTALL || false; + +const isHeadless = + (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true'; +const isFirefox = product === 'firefox'; +const isChrome = product === 'Chromium'; + +let extraLaunchOptions = {}; +try { + extraLaunchOptions = JSON.parse(process.env.EXTRA_LAUNCH_OPTIONS || '{}'); +} catch (error) { + console.warn( + `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.` + ); +} + +const defaultBrowserOptions = Object.assign( + { + handleSIGINT: true, + executablePath: process.env.BINARY, + headless: isHeadless, + dumpio: !!process.env.DUMPIO, + }, + extraLaunchOptions +); + +(async (): Promise => { + if (defaultBrowserOptions.executablePath) { + console.warn( + `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}` + ); + } else { + // TODO(jackfranklin): declare updateRevision in some form for the Firefox + // launcher. + // @ts-expect-error _updateRevision is defined on the FF launcher + // but not the Chrome one. The types need tidying so that TS can infer that + // properly and not error here. + if (product === 'firefox') await puppeteer._launcher._updateRevision(); + 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` + ); + } +})(); + +declare module 'expect/build/types' { + interface Matchers { + toBeGolden(x: string): R; + } +} + +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)) rimraf.sync(OUTPUT_DIR); + utils.extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR); +}; + +setupGoldenAssertions(); + +interface PuppeteerTestState { + browser: Browser; + context: BrowserContext; + page: Page; + puppeteer: PuppeteerNode; + defaultBrowserOptions: { + [x: string]: any; + }; + server: any; + httpsServer: any; + isFirefox: boolean; + isChrome: boolean; + isHeadless: boolean; + puppeteerPath: string; +} +const state: Partial = {}; + +export const itFailsFirefox = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isFirefox) return xit(description, body); + else return it(description, body); +}; + +export const itChromeOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isChrome) return it(description, body); + else return xit(description, body); +}; + +export const itOnlyRegularInstall = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (alternativeInstall || process.env.BINARY) return xit(description, body); + else return it(description, body); +}; + +export const itFailsWindowsUntilDate = ( + date: Date, + description: string, + body: Mocha.Func +): Mocha.Test => { + if (os.platform() === 'win32' && Date.now() < date.getTime()) { + // we are within the deferred time so skip the test + return xit(description, body); + } + + return it(description, body); +}; + +export const itFailsWindows = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (os.platform() === 'win32') { + return xit(description, body); + } + return it(description, body); +}; + +export const describeFailsFirefox = ( + description: string, + body: (this: Mocha.Suite) => void +): void | Mocha.Suite => { + if (isFirefox) return xdescribe(description, body); + else return describe(description, body); +}; + +export const describeChromeOnly = ( + description: string, + body: (this: Mocha.Suite) => void +): Mocha.Suite => { + if (isChrome) return describe(description, body); +}; + +let coverageHooks = { + beforeAll: (): void => {}, + afterAll: (): void => {}, +}; + +if (process.env.COVERAGE) { + coverageHooks = trackCoverage(); +} + +console.log( + `Running unit tests with: + -> product: ${product} + -> binary: ${ + defaultBrowserOptions.executablePath || + path.relative(process.cwd(), puppeteer.executablePath()) + }` +); + +export const setupTestBrowserHooks = (): void => { + before(async () => { + const browser = await puppeteer.launch(defaultBrowserOptions); + state.browser = browser; + }); + + after(async () => { + await state.browser.close(); + state.browser = null; + }); +}; + +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 = null; + state.page = null; + }); +}; + +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.puppeteerPath = path.resolve(path.join(__dirname, '..')); + }, + coverageHooks.beforeAll, + ], + + beforeEach: async (): Promise => { + state.server.reset(); + state.httpsServer.reset(); + }, + + afterAll: [ + async (): Promise => { + await state.server.stop(); + state.server = null; + await state.httpsServer.stop(); + state.httpsServer = null; + }, + coverageHooks.afterAll, + ], + + 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).toEqual(expectedCookies); +}; diff --git a/test/mouse.spec.ts b/test/mouse.spec.ts new file mode 100644 index 0000000..2670baf --- /dev/null +++ b/test/mouse.spec.ts @@ -0,0 +1,242 @@ +/** + * 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.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.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<() => MouseEvent>( + () => globalThis.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); + }); + itFailsFirefox('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>( + 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>(dimensions); + expect(newDimensions.width).toBe(Math.round(width + 104)); + expect(newDimensions.height).toBe(Math.round(height + 104)); + }); + itFailsFirefox('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(() => new Promise(requestAnimationFrame)); + await page.evaluate( + () => (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); + }); + itFailsFirefox('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(() => document.querySelector('button:hover').id) + ).toBe('button-6'); + await page.hover('#button-2'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-2'); + await page.hover('#button-91'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-91'); + }); + itFailsFirefox( + 'should trigger hover state with removed window.Node', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => delete window.Node); + await page.hover('#button-6'); + expect( + await page.evaluate(() => 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(() => + document + .querySelector('#button-3') + .addEventListener('mousedown', (e) => (globalThis.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') delete modifiers['Meta']; + for (const [modifier, key] of modifiers) { + await page.keyboard.down(modifier); + await page.click('#button-3'); + if ( + !(await page.evaluate((mod: string) => globalThis.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) => globalThis.lastEvent[mod], key)) + throw new Error(modifiers[modifier] + ' should be false'); + } + }); + itFailsFirefox('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, + }); + }); + itFailsFirefox('should tween mouse movement', async () => { + const { page } = getTestState(); + + await page.mouse.move(100, 100); + await page.evaluate(() => { + globalThis.result = []; + document.addEventListener('mousemove', (event) => { + globalThis.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 + itFailsFirefox( + '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.result = { x: event.clientX, y: event.clientY }; + }); + }); + + await page.mouse.click(30, 40); + + expect(await page.evaluate('result')).toEqual({ x: 30, y: 40 }); + } + ); +}); diff --git a/test/navigation.spec.ts b/test/navigation.spec.ts new file mode 100644 index 0000000..b8ff839 --- /dev/null +++ b/test/navigation.spec.ts @@ -0,0 +1,776 @@ +/** + * 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 utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import os from 'os'; + +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); + }); + itFailsFirefox('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); + }); + itFailsFirefox('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 = null; + await page + .goto(server.PREFIX + '/frames/one-frame.html') + .catch((error_) => (error = error_)); + expect(error).toBe(null); + }); + itFailsFirefox('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 = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (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', + () => history.replaceState(null, 'initial', window.location.href), + false + ); + }); + const response = await page.goto(server.PREFIX + '/grid.html'); + expect(response.status()).toBe(200); + }); + itFailsFirefox( + '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); + } + ); + itFailsFirefox( + '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); + } + ); + itFailsFirefox('should fail when navigating to bad url', async () => { + const { page, isChrome } = getTestState(); + + let error = null; + await page.goto('asdfasdf').catch((error_) => (error = error_)); + if (isChrome) + expect(error.message).toContain('Cannot navigate to invalid URL'); + else expect(error.message).toContain('Invalid url'); + }); + + /* If you are running this on pre-Catalina versions of macOS this will fail locally. + /* Mac OSX Catalina outputs a different message than other platforms. + * See https://support.google.com/chrome/thread/18125056?hl=en for details. + * If you're running pre-Catalina Mac OSX this test will fail locally. + */ + const EXPECTED_SSL_CERT_MESSAGE = + os.platform() === 'darwin' + ? 'net::ERR_CERT_INVALID' + : 'net::ERR_CERT_AUTHORITY_INVALID'; + + itFailsFirefox('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 = []; + page.on('request', () => requests.push('request')); + page.on('requestfinished', () => requests.push('requestfinished')); + page.on('requestfailed', () => requests.push('requestfailed')); + + let error = null; + await page + .goto(httpsServer.EMPTY_PAGE) + .catch((error_) => (error = error_)); + if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE); + else expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + + expect(requests.length).toBe(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 = null; + await page + .goto(httpsServer.PREFIX + '/redirect/1.html') + .catch((error_) => (error = error_)); + if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE); + else expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + }); + it('should throw if networkidle is passed as an option', async () => { + const { page, server } = getTestState(); + + let error = null; + await page + // @ts-expect-error purposefully passing an old option + .goto(server.EMPTY_PAGE, { waitUntil: 'networkidle' }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + '"networkidle" option is no longer supported' + ); + }); + it('should fail when main resources failed to load', async () => { + const { page, isChrome } = getTestState(); + + let error = null; + await page + .goto('http://localhost:44123/non-existing-url') + .catch((error_) => (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, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + await page + .goto(server.PREFIX + '/empty.html', { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should fail when exceeding default maximum navigation timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultNavigationTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should fail when exceeding default maximum timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should prioritize default navigation timeout over default timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultTimeout(0); + page.setDefaultNavigationTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should disable timeout when its set to 0', async () => { + const { page, server } = getTestState(); + + let error = null; + let loaded = false; + page.once('load', () => (loaded = true)); + await page + .goto(server.PREFIX + '/grid.html', { timeout: 0, waitUntil: ['load'] }) + .catch((error_) => (error = error_)); + expect(error).toBe(null); + 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); + }); + itFailsFirefox('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 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); + }); + itFailsFirefox( + 'should wait for network idle to succeed navigation', + async () => { + const { page, server } = getTestState(); + + let responses = []; + // Hold on to a bunch of requests without answering. + server.setRoute('/fetch-request-a.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-b.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-c.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-d.js', (req, res) => + 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'), + ]); + const secondFetchResourceRequested = server.waitForRequest( + '/fetch-request-d.js' + ); + + // 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(() => (navigationFinished = true)); + + // Wait for the page's 'load' event. + await new Promise((fulfill) => page.once('load', fulfill)); + 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 () => { + const { page, server } = getTestState(); + + let warning = null; + const warningHandler = (w) => (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 () => { + const { page } = getTestState(); + + let warning = null; + const warningHandler = (w) => (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 () => { + const { context, server } = getTestState(); + + let warning = null; + const warningHandler = (w) => (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); + }); + itFailsFirefox( + 'should navigate to dataURL and fire dataURL requests', + async () => { + const { page } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + const dataURL = 'data:text/html,
yo
'; + const response = await page.goto(dataURL); + expect(response.status()).toBe(200); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + } + ); + itFailsFirefox( + 'should navigate to URL with hash and fire requests without hash', + async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.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.length).toBe(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 = null; + try { + await page.goto(url); + } catch (error_) { + error = error_; + } + expect(error.message).toContain(url); + }); + itFailsFirefox('should send referer', 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', { + referer: 'http://google.com/', + }), + ]); + expect(request1.headers['referer']).toBe('http://google.com/'); + // Make sure subresources do not inherit referer. + expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + }); + }); + + describe('Page.waitForNavigation', function () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.evaluate( + (url: string) => (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 = null; + server.setRoute('/one-style.css', (req, res) => (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(() => (bothFired = true)); + + await server.waitForRequest('/one-style.css'); + await domContentLoadedPromise; + expect(bothFired).toBe(false); + response.end(); + await bothFiredPromise; + await navigationPromise; + }); + itFailsFirefox('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'); + }); + itFailsFirefox('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'); + }); + itFailsFirefox('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'); + }); + itFailsFirefox( + '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'); + } + ); + itFailsFirefox( + '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 utils.waitEvent(page, 'frameattached'); + await new Promise((fulfill) => { + page.on('framenavigated', (f) => { + if (f === frame) fulfill(); + }); + }); + await Promise.all([ + frame.evaluate(() => 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); + }); + itFailsFirefox('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'); + }); + }); + + describeFailsFirefox('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_) => error_); + await server.waitForRequest('/empty.html'); + + await page.$eval('iframe', (frame) => 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, chromium will cache similar requests. + await page.setCacheEnabled(false); + await page.goto(server.EMPTY_PAGE); + // Attach three frames. + const frames = await Promise.all([ + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame2', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame3', server.EMPTY_PAGE), + ]); + // Navigate all frames to the same URL. + const serverResponses = []; + server.setRoute('/one-style.html', (req, res) => + 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]); + } + }); + }); + + describeFailsFirefox('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) => (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 = null; + const navigationPromise = frame + .waitForNavigation() + .catch((error_) => (error = error_)); + await Promise.all([ + server.waitForRequest('/empty.html'), + frame.evaluate(() => ((window as any).location = '/empty.html')), + ]); + await page.$eval('iframe', (frame) => 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(() => (globalThis._foo = 10)); + await page.reload(); + expect(await page.evaluate(() => globalThis._foo)).toBe(undefined); + }); + }); +}); diff --git a/test/network.spec.ts b/test/network.spec.ts new file mode 100644 index 0000000..787fee1 --- /dev/null +++ b/test/network.spec.ts @@ -0,0 +1,610 @@ +/** + * 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 utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('network', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.Events.Request', function () { + it('should fire for navigation requests', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + }); + it('should fire for iframes', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(2); + }); + it('should fire for fetches', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => fetch('/empty.html')); + expect(requests.length).toBe(2); + }); + }); + + describe('Request.frame', function () { + it('should work for main frame navigation request', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.mainFrame()); + }); + itFailsFirefox('should work for subframe navigation request', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(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 = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.evaluate(() => fetch('/digits/1.png')); + requests = requests.filter( + (request) => !request.url().includes('favicon') + ); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.mainFrame()); + }); + }); + + describe('Request.headers', function () { + it('should work', async () => { + const { page, server, isChrome } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + if (isChrome) + expect(response.request().headers()['user-agent']).toContain('Chrome'); + else + 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'); + }); + }); + + describeFailsFirefox('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) => + !utils.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); + }); + }); + + describeFailsFirefox('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) => 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 () => await globalThis.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); + }); + }); + + describeFailsFirefox('Request.postData', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (req, res) => res.end()); + let request = null; + page.on('request', (r) => (request = r)); + await page.evaluate(() => + 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); + }); + }); + + describeFailsFirefox('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.length).toBe(1); + const redirected = redirectChain[0].response(); + expect(redirected.status()).toBe(302); + let error = null; + await redirected.text().catch((error_) => (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 = null; + 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) => (requestFinished = requestFinished || r.url().includes('/get')) + ); + // send request and wait for server response + const [pageResponse] = await Promise.all([ + page.waitForResponse((r) => !utils.isFavicon(r.request())), + page.evaluate(() => 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) => serverResponse.write('wor', x)); + // Finish response. + await new Promise((x) => serverResponse.end('ld!', x)); + expect(await responseText).toBe('hello world!'); + }); + }); + + describeFailsFirefox('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' }); + }); + }); + + describeFailsFirefox('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); + }); + }); + + 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!'); + }); + }); + + describeFailsFirefox('Network Events', function () { + it('Page.Events.Request', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on('request', (request) => requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(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 = []; + page.on('requestservedfromcache', (r) => + 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 = []; + page.on('response', (response) => responses.push(response)); + await page.goto(server.EMPTY_PAGE); + expect(responses.length).toBe(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 = []; + page.on('requestfailed', (request) => failedRequests.push(request)); + await page.goto(server.PREFIX + '/one-style.html'); + expect(failedRequests.length).toBe(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 = []; + page.on('requestfinished', (request) => requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(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 = []; + page.on('request', () => events.push('request')); + page.on('response', () => events.push('response')); + page.on('requestfinished', () => 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 = []; + page.on('request', (request) => + events.push(`${request.method()} ${request.url()}`) + ); + page.on('response', (response) => + events.push(`${response.status()} ${response.url()}`) + ); + page.on('requestfinished', (request) => + events.push(`DONE ${request.url()}`) + ); + page.on('requestfailed', (request) => + 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.length).toBe(1); + expect(redirectChain[0].url()).toContain('/foo.html'); + expect(redirectChain[0].response().remoteAddress().port).toBe( + server.PORT + ); + }); + }); + + describe('Request.isNavigationRequest', () => { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + const requests = new Map(); + page.on('request', (request) => + 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); + }); + itFailsFirefox('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 = []; + page.on('request', (request) => requests.push(request)); + await page.goto(server.PREFIX + '/pptr.png'); + expect(requests[0].isNavigationRequest()).toBe(true); + }); + }); + + describeFailsFirefox('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 = null; + try { + // @ts-expect-error purposeful bad input + await page.setExtraHTTPHeaders({ foo: 1 }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Expected value of header "foo" to be String, but "number" is found.' + ); + }); + }); + + describeFailsFirefox('Page.authenticate', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + server.setAuth('/empty.html', 'user', 'pass'); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + 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(null); + // Navigate to a different origin to bust Chrome's credential caching. + response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response.status()).toBe(401); + }); + 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) => 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); + }); + }); +}); diff --git a/test/oopif.spec.ts b/test/oopif.spec.ts new file mode 100644 index 0000000..845429a --- /dev/null +++ b/test/oopif.spec.ts @@ -0,0 +1,74 @@ +/** + * 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 { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('OOPIF', function () { + /* We use a special browser for this test as we need the --site-per-process flag */ + let browser; + let context; + let page; + + before(async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat(['--site-per-process']), + }) + ); + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + page = null; + context = null; + }); + + after(async () => { + await browser.close(); + browser = null; + }); + xit('should report oopif frames', async () => { + const { server } = getTestState(); + + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + expect(oopifs(context).length).toBe(1); + expect(page.frames().length).toBe(2); + }); + it('should load oopif iframes with subresources and request interception', async () => { + const { server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + expect(oopifs(context).length).toBe(1); + }); +}); + +/** + * @param {!BrowserContext} context + */ +function oopifs(context) { + return context + .targets() + .filter((target) => target._targetInfo.type === 'iframe'); +} diff --git a/test/page.spec.ts b/test/page.spec.ts new file mode 100644 index 0000000..1196dfc --- /dev/null +++ b/test/page.spec.ts @@ -0,0 +1,1770 @@ +/** + * 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 utils from './utils.js'; +const { waitEvent } = utils; +import expect from 'expect'; +import sinon from 'sinon'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { Page, Metrics } from '../lib/cjs/puppeteer/common/Page.js'; +import { JSHandle } from '../lib/cjs/puppeteer/common/JSHandle.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 = null; + await Promise.all([ + newPage + .evaluate(() => new Promise(() => {})) + .catch((error_) => (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); + }); + itFailsFirefox('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; + }); + itFailsFirefox('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); + }); + itFailsFirefox('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) => error), + newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => 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'), + utils.waitEvent(page, 'load'), + ]); + }); + }); + + // This test fails on Firefox on CI consistently but cannot be replicated + // locally. Skipping for now to unblock the Mitt release and given FF support + // isn't fully done yet but raising an issue to ask the FF folks to have a + // look at this. + describeFailsFirefox('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(); + page.on('response', handler); + await page.goto(server.EMPTY_PAGE); + expect(handler.callCount).toBe(1); + page.off('response', handler); + await page.goto(server.EMPTY_PAGE); + // Still one because we removed the handler. + expect(handler.callCount).toBe(1); + page.on('response', handler); + await page.goto(server.EMPTY_PAGE); + // Two now because we added the handler back. + expect(handler.callCount).toBe(2); + }); + }); + + describeFailsFirefox('Page.Events.error', function () { + it('should throw when page crashes', async () => { + const { page } = getTestState(); + + let error = null; + page.on('error', (err) => (error = err)); + page.goto('chrome://crash').catch(() => {}); + await waitEvent(page, 'error'); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describeFailsFirefox('Page.Events.Popup', function () { + it('should work', async () => { + const { page } = getTestState(); + + const [popup] = await Promise.all([ + new Promise((x) => page.once('popup', x)), + page.evaluate(() => window.open('about:blank')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + }); + it('should work with noopener', async () => { + const { page } = getTestState(); + + const [popup] = await Promise.all([ + new Promise((x) => page.once('popup', x)), + page.evaluate(() => window.open('about:blank', null, 'noopener')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!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([ + new Promise((x) => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!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([ + new Promise((x) => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!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([ + new Promise((x) => page.once('popup', x)), + page.$eval('a', (a: HTMLAnchorElement) => a.click()), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!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([ + new Promise((x) => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + }); + + describe('BrowserContext.overridePermissions', function () { + function getPermission(page, name) { + return page.evaluate( + (name) => + navigator.permissions.query({ name }).then((result) => 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'); + }); + itFailsFirefox('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 = null; + await context + // @ts-expect-error purposeful bad input for test + .overridePermissions(server.EMPTY_PAGE, ['foo']) + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unknown permission: foo'); + }); + itFailsFirefox('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'); + }); + itFailsFirefox('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'); + }); + itFailsFirefox('should trigger permission onchange', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + globalThis.events = []; + return navigator.permissions + .query({ name: 'geolocation' }) + .then(function (result) { + globalThis.events.push(result.state); + result.onchange = function () { + globalThis.events.push(result.state); + }; + }); + }); + expect(await page.evaluate(() => globalThis.events)).toEqual(['prompt']); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + ]); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + 'granted', + ]); + await context.clearPermissionOverrides(); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + 'granted', + 'prompt', + ]); + }); + itFailsFirefox( + 'should isolate permissions between browser contexs', + 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(); + } + ); + }); + + describe('Page.setGeolocation', function () { + itFailsFirefox('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( + () => + new Promise((resolve) => + 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 = null; + try { + await page.setGeolocation({ longitude: 200, latitude: 10 }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain('Invalid longitude "200"'); + }); + }); + + describeFailsFirefox('Page.setOfflineMode', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setOfflineMode(true); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (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(() => window.navigator.onLine)).toBe(true); + await page.setOfflineMode(true); + expect(await page.evaluate(() => window.navigator.onLine)).toBe(false); + await page.setOfflineMode(false); + expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); + }); + }); + + describeFailsFirefox('Page.emulateNetworkConditions', function () { + it('should change navigator.connection.effectiveType', async () => { + const { page, puppeteer } = getTestState(); + + const slow3G = puppeteer.networkConditions['Slow 3G']; + const fast3G = puppeteer.networkConditions['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('ExecutionContext.queryObjects', function () { + itFailsFirefox('should work', async () => { + const { page } = getTestState(); + + // Instantiate an object + await page.evaluate(() => (globalThis.set = new Set(['hello', 'world']))); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate( + (objects: JSHandle[]) => objects.length, + objectsHandle + ); + expect(count).toBe(1); + const values = await page.evaluate( + (objects) => Array.from(objects[0].values()), + objectsHandle + ); + expect(values).toEqual(['hello', 'world']); + }); + itFailsFirefox('should work for non-blank page', async () => { + const { page, server } = getTestState(); + + // Instantiate an object + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => (globalThis.set = new Set(['hello', 'world']))); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate( + (objects: JSHandle[]) => objects.length, + objectsHandle + ); + expect(count).toBe(1); + }); + it('should fail for disposed handles', async () => { + const { page } = getTestState(); + + const prototypeHandle = await page.evaluateHandle( + () => HTMLBodyElement.prototype + ); + await prototypeHandle.dispose(); + let error = null; + await page + .queryObjects(prototypeHandle) + .catch((error_) => (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(() => 42); + let error = null; + await page + .queryObjects(prototypeHandle) + .catch((error_) => (error = error_)); + expect(error.message).toBe( + 'Prototype JSHandle must not be referencing primitive value' + ); + }); + }); + + describeFailsFirefox('Page.Events.Console', function () { + it('should work', async () => { + const { page } = getTestState(); + + let message = null; + page.once('console', (m) => (message = m)); + await Promise.all([ + page.evaluate(() => console.log('hello', 5, { foo: 'bar' })), + waitEvent(page, 'console'), + ]); + 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', async () => { + const { page } = getTestState(); + + const messages = []; + page.on('console', (msg) => 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'); + 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) => msg.type())).toEqual([ + 'timeEnd', + 'trace', + 'dir', + 'warning', + 'error', + 'log', + ]); + expect(messages[0].text()).toContain('calling console.time'); + expect(messages.slice(1).map((msg) => msg.text())).toEqual([ + 'calling console.trace', + 'calling console.dir', + 'calling console.warn', + 'calling console.error', + 'JSHandle@promise', + ]); + }); + it('should not fail for window object', async () => { + const { page } = getTestState(); + + let message = null; + page.once('console', (msg) => (message = msg)); + await Promise.all([ + page.evaluate(() => console.error(window)), + waitEvent(page, 'console'), + ]); + 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) => 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) => (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) => (frame.onload = x)); + // 3. After that, remove the iframe. + frame.remove(); + }); + const popupTarget = page + .browserContext() + .targets() + .find((target) => 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'); + }); + }); + + describeFailsFirefox('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 = new Promise<{ metrics: Metrics; title: string }>( + (fulfill) => page.once('metrics', fulfill) + ); + await page.evaluate(() => console.timeStamp('test42')); + const metrics = await metricsPromise; + expect(metrics.title).toBe('test42'); + checkMetrics(metrics.metrics); + }); + function checkMetrics(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]).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(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + 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) => request.url() === server.PREFIX + '/digits/2.png' + ), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForRequest(() => false, { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + page.setDefaultTimeout(1); + await page + .waitForRequest(() => false) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + 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(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + 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 [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png', { timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + 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(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForResponse(() => false, { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + page.setDefaultTimeout(1); + await page + .waitForResponse(() => false) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.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) => response.url() === server.PREFIX + '/digits/2.png' + ), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + 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(() => + setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50) + ), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describeFailsFirefox('Page.exposeFunction', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return a * b; + }); + const result = await page.evaluate(async function () { + return await globalThis.compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should throw exception in page context', async () => { + const { page } = getTestState(); + + await page.exposeFunction('woof', function () { + throw new Error('WOOF WOOF'); + }); + const { message, stack } = await page.evaluate(async () => { + try { + await globalThis.woof(); + } catch (error) { + return { message: error.message, stack: error.stack }; + } + }); + expect(message).toBe('WOOF WOOF'); + expect(stack).toContain(__filename); + }); + 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.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(() => globalThis.woof()); + await page.reload(); + expect(called).toBe(true); + }); + it('should survive navigation', async () => { + const { page, server } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return a * b; + }); + + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async function () { + return await globalThis.compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should await returned promise', async () => { + const { page } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function () { + return await globalThis.compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames', async () => { + const { page, server } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + 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.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, b) { + return Promise.resolve(a * b); + }); + + const frame = page.frames()[1]; + const result = await frame.evaluate(async function () { + return await globalThis.compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work with complex objects', async () => { + const { page } = getTestState(); + + await page.exposeFunction('complexObject', function (a, b) { + return { x: a.x + b.x }; + }); + const result = await page.evaluate<() => Promise<{ x: number }>>( + async () => globalThis.complexObject({ x: 5 }, { x: 2 }) + ); + expect(result.x).toBe(7); + }); + }); + + describeFailsFirefox('Page.Events.PageError', function () { + it('should fire', async () => { + const { page, server } = getTestState(); + + let error = null; + page.once('pageerror', (e) => (error = e)); + await Promise.all([ + page.goto(server.PREFIX + '/error.html'), + waitEvent(page, 'pageerror'), + ]); + expect(error.message).toContain('Fancy'); + }); + }); + + describe('Page.setUserAgent', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + expect(await page.evaluate(() => 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(() => navigator.userAgent)).toContain( + 'Mozilla' + ); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should emulate device user-agent', async () => { + const { page, server, puppeteer } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => navigator.userAgent)).not.toContain( + 'iPhone' + ); + await page.setUserAgent(puppeteer.devices['iPhone 6'].userAgent); + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'iPhone' + ); + }); + }); + + 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, puppeteer } = getTestState(); + + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error = null; + await page + .setContent(``, { + timeout: 1, + }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default navigation timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error = null; + await page + .setContent(``) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should await resources to load', async () => { + const { page, server } = getTestState(); + + const imgPath = '/img.png'; + let imgResponse = null; + server.setRoute(imgPath, (req, res) => (imgResponse = res)); + let loaded = false; + const contentPromise = page + .setContent(``) + .then(() => (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) => 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) => div.textContent)).toBe( + 'aberración' + ); + }); + it('should work with emojis', async () => { + const { page } = getTestState(); + + await page.setContent('
🐥
'); + expect(await page.$eval('div', (div) => div.textContent)).toBe('🐥'); + }); + it('should work with newline', async () => { + const { page } = getTestState(); + + await page.setContent('
\n
'); + expect(await page.$eval('div', (div) => div.textContent)).toBe('\n'); + }); + }); + + describeFailsFirefox('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) => void error); + expect(await page.evaluate(() => globalThis.__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(() => globalThis.__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) => void error); + expect(await page.evaluate(() => globalThis.__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(() => globalThis.__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(() => globalThis.__injected)).toBe(42); + + await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__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 utils.attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ); + await frame + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await frame.evaluate(() => globalThis.__injected)).toBe( + undefined + ); + } + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + + { + const frame = await utils.attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ); + await frame + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await frame.evaluate(() => globalThis.__injected)).toBe(42); + } + }); + }); + + describe('Page.addScriptTag', function () { + it('should throw an error if no options are provided', async () => { + const { page } = getTestState(); + + let error = null; + try { + // @ts-expect-error purposefully passing bad options + await page.addScriptTag('/injectedfile.js'); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Provide an object with a `url`, `path` or `content` property' + ); + }); + + 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(() => globalThis.__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(() => globalThis.__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('window.__es6injected'); + expect(await page.evaluate(() => globalThis.__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('window.__es6injected'); + expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42); + }); + + it('should throw an error if loading from url fail', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addScriptTag({ url: '/nonexistfile.js' }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe('Loading script from /nonexistfile.js failed'); + }); + + 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(() => globalThis.__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( + () => globalThis.__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(() => globalThis.__injected)).toBe(35); + }); + + // @see https://github.com/puppeteer/puppeteer/issues/4840 + xit('should throw when added with content to the CSP page', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addScriptTag({ content: 'window.__injected = 35;' }) + .catch((error_) => (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 = null; + await page + .addScriptTag({ url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js' }) + .catch((error_) => (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 = null; + try { + // @ts-expect-error purposefully passing bad input + await page.addStyleTag('/injectedstyle.css'); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Provide an object with a `url`, `path` or `content` property' + ); + }); + + 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 } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addStyleTag({ url: '/nonexistfile.js' }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe('Loading style from /nonexistfile.js failed'); + }); + + 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) => 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)'); + }); + + itFailsFirefox( + 'should throw when added with content to the CSP page', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addStyleTag({ content: 'body { background-color: green; }' }) + .catch((error_) => (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 = null; + await page + .addStyleTag({ + url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css', + }) + .catch((error_) => (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); + }); + }); + + describeFailsFirefox('Page.setJavaScriptEnabled', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto( + 'data:text/html, ' + ); + let error = null; + await page.evaluate('something').catch((error_) => (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); + }); + itFailsFirefox( + '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('printing to PDF', function () { + it('can print to PDF and save to file', async () => { + // Printing to pdf is currently only supported in headless + const { isHeadless, page } = getTestState(); + + if (!isHeadless) return; + + const outputFile = __dirname + '/assets/output.pdf'; + await page.pdf({ path: outputFile }); + expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0); + fs.unlinkSync(outputFile); + }); + }); + + 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(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.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(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.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) => + select.addEventListener( + 'input', + () => ((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(() => globalThis.makeMultiple()); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + 'green', + 'red', + ]); + expect(await page.evaluate(() => globalThis.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(() => globalThis.result.onBubblingInput) + ).toEqual(['blue']); + expect( + await page.evaluate(() => globalThis.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(() => globalThis.makeMultiple()); + const result = await page.select('select', 'blue', 'black', 'magenta'); + expect( + result.reduce( + (accumulator, current) => + ['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.length).toEqual(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(() => globalThis.makeMultiple()); + await page.select('select', 'blue', 'black', 'magenta'); + await page.select('select'); + expect( + await page.$eval('select', (select: HTMLSelectElement) => + Array.from(select.options).every( + (option: HTMLOptionElement) => !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: HTMLSelectElement) => + Array.from(select.options).every( + (option: HTMLOptionElement) => !option.selected + ) + ) + ).toEqual(true); + }); + it('should throw if passed in non-strings', async () => { + const { page } = getTestState(); + + await page.setContent(''); + let error = null; + try { + // @ts-expect-error purposefully passing bad input + await page.select('select', 12); + } catch (error_) { + error = error_; + } + expect(error.message).toContain('Values must be strings'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3327 + itFailsFirefox( + '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(() => (window.Event = null)); + await page.select('select', 'blue'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + ]); + } + ); + }); + + describe('Page.Events.Close', function () { + itFailsFirefox('should work with window.close', async () => { + const { page, context } = getTestState(); + + const newPagePromise = new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target.page())) + ); + await page.evaluate( + () => (window['newPage'] = window.open('about:blank')) + ); + const newPage = await newPagePromise; + const closedPromise = new Promise((x) => newPage.on('close', x)); + await page.evaluate(() => window['newPage'].close()); + await closedPromise; + }); + it('should work with page.close', async () => { + const { context } = getTestState(); + + const newPage = await context.newPage(); + const closedPromise = new Promise((x) => newPage.on('close', x)); + 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 instance', async () => { + const { page, context } = getTestState(); + + expect(page.browserContext()).toBe(context); + }); + }); +}); diff --git a/test/queryselector.spec.ts b/test/queryselector.spec.ts new file mode 100644 index 0000000..7a147dd --- /dev/null +++ b/test/queryselector.spec.ts @@ -0,0 +1,507 @@ +/** + * 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'; // eslint-disable-line import/extensions +import { CustomQueryHandler } from '../lib/cjs/puppeteer/common/QueryHandler.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) => 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) => 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: HTMLElement) => e.textContent + div.textContent, + divHandle + ); + expect(text).toBe('hello world'); + }); + it('should throw error if no element is found', async () => { + const { page } = getTestState(); + + let error = null; + await page + .$eval('section', (e) => e.id) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'failed to find element matching selector "section"' + ); + }); + }); + + describe('pierceHandler', 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'); + const text = await div.evaluate( + (element: Element) => element.textContent + ); + expect(text).toBe('Hello'); + }); + it('should find all elements in shadow', async () => { + const { page } = getTestState(); + const divs = await page.$$('pierce/.foo'); + const text = await Promise.all( + divs.map((div) => + div.evaluate((element: Element) => element.textContent) + ) + ); + expect(text.join(' ')).toBe('Hello World'); + }); + }); + + // 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) => 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: number, three: number) => divs.length + two + three, + 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: HTMLElement) => + sections.reduce( + (acc, section) => acc + Number(section.textContent), + 0 + ) + Number(div.textContent), + divHandle + ); + expect(sum).toBe(8); + }); + it('should handle many elements', async () => { + 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) => + sections.reduce((acc, section) => 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.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => 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.length).toBe(0); + }); + }); + + describe('Path.$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.length).toBe(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.length).toBe(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: HTMLElement) => 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: HTMLElement) => node.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: HTMLElement) => node.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: HTMLElement) => node.innerText) + .catch((error) => 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: HTMLElement[]) => + nodes.map((n) => 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: HTMLElement[]) => + nodes.map((n) => 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) => 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.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => 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.length).toBe(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: HTMLElement) => 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: Element, selector: string) => + Array.from(element.querySelectorAll(selector)), + }; + before(() => { + const { puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('allArray', handler); + }); + + it('should have registered handler', async () => { + const { puppeteer } = getTestState(); + 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.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => 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.length).toBe(0); + }); + it('$$eval should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '
hello
beautiful
world!
' + ); + const divsCount = await page.$$eval( + 'allArray/div', + (divs) => 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: number, three: number) => divs.length + two + three, + 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: HTMLElement) => + sections.reduce( + (acc, section) => acc + Number(section.textContent), + 0 + ) + Number(div.textContent), + divHandle + ); + expect(sum).toBe(8); + }); + it('$$eval should handle many elements', async () => { + 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) => + sections.reduce((acc, section) => acc + Number(section.textContent), 0) + ); + expect(sum).toBe(500500); + }); + }); +}); diff --git a/test/requestinterception.spec.ts b/test/requestinterception.spec.ts new file mode 100644 index 0000000..57a06ac --- /dev/null +++ b/test/requestinterception.spec.ts @@ -0,0 +1,743 @@ +/** + * 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 utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('request interception', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describeFailsFirefox('Page.setRequestInterception', function () { + it('should intercept', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (utils.isFavicon(request)) { + request.continue(); + 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(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + expect(response.remoteAddress().port).toBe(server.PORT); + }); + 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) => request.continue()); + await page.setContent(` +
+ +
+ `); + await Promise.all([ + page.$eval('form', (form: HTMLFormElement) => form.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 = []; + page.on('request', (request) => { + if (!utils.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 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) => 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) => 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) => 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', () => ++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'); + }); + let failedRequest = null; + page.on('requestfailed', (request) => (failedRequest = request)); + await 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) => 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) => request.abort()); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (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 = []; + 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.length).toBe(5); + expect(requests[2].resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(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 = []; + page.on('request', (request) => { + request.continue(); + if (!utils.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) => + 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.length).toBe(5); + expect(requests[0].resourceType()).toBe('document'); + expect(requests[1].resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1].redirectChain(); + expect(redirectChain.length).toBe(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 { + await fetch('/non-existing.json'); + } catch (error) { + return 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) => res.end(responseCount++ * 11 + '')); + await page.setRequestInterception(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', (request) => { + if (utils.isFavicon(request)) { + request.continue(); + return; + } + spinner ? request.abort() : request.continue(); + spinner = !spinner; + }); + const results = await page.evaluate(() => + Promise.all([ + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + fetch('/zzz') + .then((response) => response.text()) + .catch(() => '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 = []; + 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.length).toBe(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 = []; + page.on('request', (request) => { + requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,
yo
'; + const text = await page.evaluate( + (url: string) => fetch(url).then((r) => r.text()), + dataURL + ); + expect(text).toBe('
yo
'); + expect(requests.length).toBe(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 = []; + 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.length).toBe(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) => 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) => res.end()); + page.on('request', (request) => 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 = []; + page.on('request', (request) => { + request.continue(); + requests.push(request); + }); + const response = await page.goto( + `data:text/html,` + ); + expect(response.status()).toBe(200); + expect(requests.length).toBe(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 = null; + page.on('request', async (r) => (request = r)); + page.$eval( + 'iframe', + (frame: HTMLIFrameElement, url: string) => (frame.src = url), + server.EMPTY_PAGE + ), + // Wait for request interception. + await utils.waitEvent(page, 'request'); + // Delete frame to cause request to be canceled. + await page.$eval('iframe', (frame) => frame.remove()); + let error = null; + await request.continue().catch((error_) => (error = error_)); + expect(error).toBe(null); + }); + it('should throw if interception is not enabled', async () => { + const { page, server } = getTestState(); + + let error = null; + page.on('request', async (request) => { + try { + await request.continue(); + } catch (error_) { + error = 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 not cache-safe', 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, false); + page.on('request', (request) => request.continue()); + + const cached = []; + page.on('requestservedfromcache', (r) => cached.push(r)); + + await page.reload(); + expect(cached.length).toBe(0); + }); + it('should cache if cache-safe', 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, true); + page.on('request', (request) => request.continue()); + + const cached = []; + page.on('requestservedfromcache', (r) => cached.push(r)); + + await page.reload(); + expect(cached.length).toBe(1); + }); + it('should load fonts if cache-safe', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true, true); + page.on('request', (request) => request.continue()); + + await page.goto(server.PREFIX + '/cached/one-style-font.html'); + await page.waitForResponse((r) => r.url().endsWith('/one-style.woff')); + }); + }); + + describeFailsFirefox('Request.continue', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => 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(() => 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 }); + }); + let consoleMessage = null; + page.on('console', (msg) => (consoleMessage = msg)); + await 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(() => 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(() => + 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'); + }); + }); + + describeFailsFirefox('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(() => 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(() => 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().length).toBe(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, + }); + }); + await page.evaluate((PREFIX) => { + const img = document.createElement('img'); + img.src = PREFIX + '/does-not-exist.png'; + document.body.appendChild(img); + return new Promise((fulfill) => (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(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + }); +}); + +/** + * @param {string} path + * @returns {string} + */ +function pathToFileURL(path) { + 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/test/run_static_server.js b/test/run_static_server.js new file mode 100644 index 0000000..6779e88 --- /dev/null +++ b/test/run_static_server.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +/** + * 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. + */ +const path = require('path'); +const { TestServer } = require('../utils/testserver/'); + +const port = 8907; +const httpsPort = 8908; +const assetsPath = path.join(__dirname, 'assets'); +const cachedPath = path.join(__dirname, 'assets', 'cached'); + +Promise.all([ + TestServer.create(assetsPath, port), + TestServer.createHTTPS(assetsPath, httpsPort), +]).then(([server, httpsServer]) => { + server.enableHTTPCache(cachedPath); + httpsServer.enableHTTPCache(cachedPath); + console.log(`HTTP: server is running on http://localhost:${port}`); + console.log(`HTTPS: server is running on https://localhost:${httpsPort}`); +}); diff --git a/test/screenshot.spec.ts b/test/screenshot.spec.ts new file mode 100644 index 0000000..223d6c4 --- /dev/null +++ b/test/screenshot.spec.ts @@ -0,0 +1,329 @@ +/** + * 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, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Screenshots', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.screenshot', function () { + itFailsFirefox('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'); + }); + itFailsFirefox('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'); + }); + itFailsFirefox( + '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'); + }); + itFailsFirefox('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) => page.close())); + }); + itFailsFirefox('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'); + }); + itFailsFirefox('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 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'); + }); + itFailsFirefox('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', + }); + // TODO (@jackfranklin): improve the screenshot types. + // - if we pass encoding: 'base64', it returns a string + // - else it returns a buffer. + // If we can fix that we can avoid this "as string" here. + expect(Buffer.from(screenshot as string, 'base64')).toBeGolden( + 'screenshot-sanity.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(() => 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 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'); + }); + itFailsFirefox( + '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(() => ({ + 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' + ); + }); + itFailsFirefox('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'); + }); + itFailsFirefox('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) => element.remove(), + elementHandle + ); + const screenshotError = await elementHandle + .screenshot() + .catch((error) => 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_) => 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'); + }); + itFailsFirefox('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/test/target.spec.ts b/test/target.spec.ts new file mode 100644 index 0000000..918510f --- /dev/null +++ b/test/target.spec.ts @@ -0,0 +1,294 @@ +/** + * 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 utils from './utils.js'; +const { waitEvent } = utils; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { Target } from '../lib/cjs/puppeteer/common/Target.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) => target.type() === 'page' && target.url() === 'about:blank' + ) + ).toBeTruthy(); + expect(targets.some((target) => 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.length).toBe(1); + expect(allPages).toContain(page); + }); + it('should contain browser target', async () => { + const { browser } = getTestState(); + + const targets = browser.targets(); + const browserTarget = targets.find((target) => 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) => p !== page); + expect( + await originalPage.evaluate(() => ['Hello', 'world'].join(' ')) + ).toBe('Hello world'); + expect(await originalPage.$('body')).toBeTruthy(); + }); + itFailsFirefox( + 'should report when a new page is created and closed', + async () => { + const { page, server, context } = getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget( + (target) => + target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html' + ) + .then((target) => target.page()), + page.evaluate( + (url: string) => window.open(url), + server.CROSS_PROCESS_PREFIX + '/empty.html' + ), + ]); + expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX); + expect(await otherPage.evaluate(() => ['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 closePagePromise = new Promise((fulfill) => + context.once('targetdestroyed', (target) => fulfill(target.page())) + ); + await otherPage.close(); + expect(await closePagePromise).toBe(otherPage); + + allPages = await Promise.all( + context.targets().map((target) => target.page()) + ); + expect(allPages).toContain(page); + expect(allPages).not.toContain(otherPage); + } + ); + itFailsFirefox( + 'should report when a service worker is created and destroyed', + async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const createdTarget = new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + + 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 = new Promise((fulfill) => + context.once('targetdestroyed', (target) => fulfill(target)) + ); + await page.evaluate(() => + globalThis.registrationPromise.then((registration) => + registration.unregister() + ) + ); + expect(await destroyedTarget).toBe(await createdTarget); + } + ); + itFailsFirefox('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) => target.type() === 'service_worker' + ); + const worker = await target.worker(); + expect(await worker.evaluate(() => self.toString())).toBe( + '[object ServiceWorkerGlobalScope]' + ); + }); + itFailsFirefox('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) => target.type() === 'shared_worker' + ); + const worker = await target.worker(); + expect(await worker.evaluate(() => self.toString())).toBe( + '[object SharedWorkerGlobalScope]' + ); + }); + itFailsFirefox('should report when a target url changes', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let changedTarget = new Promise((fulfill) => + context.once('targetchanged', (target) => fulfill(target)) + ); + await page.goto(server.CROSS_PROCESS_PREFIX + '/'); + expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/'); + + changedTarget = new Promise((fulfill) => + context.once('targetchanged', (target) => fulfill(target)) + ); + await page.goto(server.EMPTY_PAGE); + expect((await changedTarget).url()).toBe(server.EMPTY_PAGE); + }); + itFailsFirefox('should not report uninitialized pages', async () => { + const { context } = getTestState(); + + let targetChanged = false; + const listener = () => (targetChanged = true); + context.on('targetchanged', listener); + const targetPromise = new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + const newPagePromise = context.newPage(); + const target = await targetPromise; + expect(target.url()).toBe('about:blank'); + + const newPage = await newPagePromise; + const targetPromise2 = new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + const evaluatePromise = newPage.evaluate(() => window.open('about:blank')); + const target2 = await targetPromise2; + expect(target2.url()).toBe('about:blank'); + await evaluatePromise; + await newPage.close(); + expect(targetChanged).toBe(false); + context.removeListener('targetchanged', listener); + }); + itFailsFirefox( + 'should not crash while redirecting if original request was missed', + async () => { + const { page, server, context } = getTestState(); + + let serverResponse = null; + server.setRoute('/one-style.css', (req, res) => (serverResponse = res)); + // Open a new page. Use window.open to connect to the page later. + await Promise.all([ + page.evaluate( + (url: string) => window.open(url), + server.PREFIX + '/one-style.html' + ), + server.waitForRequest('/one-style.css'), + ]); + // Connect to the opened page. + const target = await context.waitForTarget((target) => + 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(); + } + ); + itFailsFirefox('should have an opener', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [createdTarget] = await Promise.all([ + new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ), + 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()).toBe(null); + }); + + describe('Browser.waitForTarget', () => { + itFailsFirefox('should wait for a target', async () => { + const { browser, puppeteer, server } = getTestState(); + + let resolved = false; + const targetPromise = browser.waitForTarget( + (target) => target.url() === server.EMPTY_PAGE + ); + targetPromise + .then(() => (resolved = true)) + .catch((error) => { + resolved = true; + if (error instanceof puppeteer.errors.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 puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + } + await page.close(); + }); + it('should timeout waiting for a non-existent target', async () => { + const { browser, server, puppeteer } = getTestState(); + + let error = null; + await browser + .waitForTarget((target) => target.url() === server.EMPTY_PAGE, { + timeout: 1, + }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + }); +}); diff --git a/test/touchscreen.spec.ts b/test/touchscreen.spec.ts new file mode 100644 index 0000000..36931d5 --- /dev/null +++ b/test/touchscreen.spec.ts @@ -0,0 +1,49 @@ +/** + * 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, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeFailsFirefox('Touchscreen', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should tap the button', async () => { + const { puppeteer, page, server } = getTestState(); + const iPhone = puppeteer.devices['iPhone 6']; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + await page.tap('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should report touches', async () => { + const { puppeteer, page, server } = getTestState(); + const iPhone = puppeteer.devices['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(() => globalThis.getResult())).toEqual([ + 'Touchstart: 0', + 'Touchend: 0', + ]); + }); +}); diff --git a/test/tracing.spec.ts b/test/tracing.spec.ts new file mode 100644 index 0000000..5e06f12 --- /dev/null +++ b/test/tracing.spec.ts @@ -0,0 +1,133 @@ +/** + * 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 { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('Tracing', function () { + let outputFile; + let browser; + let 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, 'assets', 'trace.json'); + }); + + afterEach(async () => { + await browser.close(); + browser = null; + page = null; + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + outputFile = null; + } + }); + 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-v8.cpu_profiler.hires'], + }); + await page.tracing.stop(); + + const traceJson = JSON.parse( + fs.readFileSync(outputFile, { encoding: 'utf8' }) + ); + expect(traceJson.metadata['trace-config']).toContain( + 'disabled-by-default-v8.cpu_profiler.hires' + ); + }); + it('should throw if tracing on two pages', async () => { + await page.tracing.start({ path: outputFile }); + const newPage = await browser.newPage(); + let error = null; + await newPage.tracing + .start({ path: outputFile }) + .catch((error_) => (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 null 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(null); + 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 = null; + try { + await page.tracing.stop(); + } catch (error_) { + error = error_; + } + expect(error).toBeDefined(); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..8b1f1e8 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["*.ts", "*.js"] +} diff --git a/test/tsconfig.test.json b/test/tsconfig.test.json new file mode 100644 index 0000000..3432441 --- /dev/null +++ b/test/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..935b44d --- /dev/null +++ b/test/utils.js @@ -0,0 +1,135 @@ +/** + * 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. + */ + +// TODO (@jackfranklin): convert to TS and enable type checking. + +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const expect = require('expect'); +const GoldenUtils = require('./golden-utils'); +const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) + ? path.join(__dirname, '..') + : path.join(__dirname, '..', '..'); + +const utils = (module.exports = { + extendExpectWithToBeGolden: function (goldenDir, outputDir) { + expect.extend({ + toBeGolden: (testScreenshot, goldenFilePath) => { + const result = GoldenUtils.compare( + goldenDir, + outputDir, + testScreenshot, + goldenFilePath + ); + + return { + message: () => result.message, + pass: result.pass, + }; + }, + }); + }, + + /** + * @returns {string} + */ + projectRoot: function () { + return PROJECT_ROOT; + }, + + /** + * @param {!Page} page + * @param {string} frameId + * @param {string} url + * @returns {!Frame} + */ + attachFrame: async function (page, frameId, url) { + const handle = await page.evaluateHandle(attachFrame, frameId, url); + return await handle.asElement().contentFrame(); + + async function attachFrame(frameId, url) { + const frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise((x) => (frame.onload = x)); + return frame; + } + }, + + isFavicon: function (request) { + return request.url().includes('favicon.ico'); + }, + + /** + * @param {!Page} page + * @param {string} frameId + */ + detachFrame: async function (page, frameId) { + await page.evaluate(detachFrame, frameId); + + function detachFrame(frameId) { + const frame = document.getElementById(frameId); + frame.remove(); + } + }, + + /** + * @param {!Page} page + * @param {string} frameId + * @param {string} url + */ + navigateFrame: async function (page, frameId, url) { + await page.evaluate(navigateFrame, frameId, url); + + function navigateFrame(frameId, url) { + const frame = document.getElementById(frameId); + frame.src = url; + return new Promise((x) => (frame.onload = x)); + } + }, + + /** + * @param {!Frame} frame + * @param {string=} indentation + * @returns {Array} + */ + dumpFrames: function (frame, indentation) { + indentation = indentation || ''; + let description = frame.url().replace(/:\d{4}\//, ':/'); + if (frame.name()) description += ' (' + frame.name() + ')'; + const result = [indentation + description]; + for (const child of frame.childFrames()) + result.push(...utils.dumpFrames(child, ' ' + indentation)); + return result; + }, + + /** + * @param {!EventEmitter} emitter + * @param {string} eventName + * @returns {!Promise} + */ + waitEvent: function (emitter, eventName, predicate = () => true) { + return new Promise((fulfill) => { + emitter.on(eventName, function listener(event) { + if (!predicate(event)) return; + emitter.removeListener(eventName, listener); + fulfill(event); + }); + }); + }, +}); diff --git a/test/waittask.spec.ts b/test/waittask.spec.ts new file mode 100644 index 0000000..ee76bbb --- /dev/null +++ b/test/waittask.spec.ts @@ -0,0 +1,773 @@ +/** + * 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 utils from './utils.js'; +import sinon from 'sinon'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('waittask specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.waitFor', function () { + /* This method is deprecated but we don't want the warnings showing up in + * tests. Until we remove this method we still want to ensure we don't break + * it. + */ + beforeEach(() => sinon.stub(console, 'warn').callsFake(() => {})); + + it('should wait for selector', async () => { + const { page, server } = getTestState(); + + let found = false; + const waitFor = page.waitFor('div').then(() => (found = true)); + await page.goto(server.EMPTY_PAGE); + expect(found).toBe(false); + await page.goto(server.PREFIX + '/grid.html'); + await waitFor; + expect(found).toBe(true); + }); + + it('should wait for an xpath', async () => { + const { page, server } = getTestState(); + + let found = false; + const waitFor = page.waitFor('//div').then(() => (found = true)); + await page.goto(server.EMPTY_PAGE); + expect(found).toBe(false); + await page.goto(server.PREFIX + '/grid.html'); + await waitFor; + expect(found).toBe(true); + }); + it('should not allow you to select an element with single slash xpath', async () => { + const { page } = getTestState(); + + await page.setContent(`
some text
`); + let error = null; + await page.waitFor('/html/body/div').catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + it('should timeout', async () => { + const { page } = getTestState(); + + const startTime = Date.now(); + const timeout = 42; + await page.waitFor(timeout); + expect(Date.now() - startTime).not.toBeLessThan(timeout / 2); + }); + it('should work with multiline body', async () => { + const { page } = getTestState(); + + const result = await page.waitForFunction(` + (() => true)() + `); + expect(await result.jsonValue()).toBe(true); + }); + it('should wait for predicate', async () => { + const { page } = getTestState(); + + await Promise.all([ + page.waitFor(() => window.innerWidth < 100), + page.setViewport({ width: 10, height: 10 }), + ]); + }); + it('should throw when unknown type', async () => { + const { page } = getTestState(); + + let error = null; + // @ts-expect-error purposefully passing bad type for test + await page.waitFor({ foo: 'bar' }).catch((error_) => (error = error_)); + expect(error.message).toContain('Unsupported target type'); + }); + it('should wait for predicate with arguments', async () => { + const { page } = getTestState(); + + await page.waitFor((arg1, arg2) => arg1 !== arg2, {}, 1, 2); + }); + + it('should log a deprecation warning', async () => { + const { page } = getTestState(); + + await page.waitFor(() => true); + + const consoleWarnStub = console.warn as sinon.SinonSpy; + + expect(consoleWarnStub.calledOnce).toBe(true); + expect( + consoleWarnStub.firstCall.calledWith( + 'waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.' + ) + ).toBe(true); + expect((console.warn as sinon.SinonSpy).calledOnce).toBe(true); + }); + }); + + describe('Frame.waitForFunction', function () { + it('should accept a string', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction('window.__FOO === 1'); + await page.evaluate(() => (globalThis.__FOO = 1)); + await watchdog; + }); + it('should work when resolved right before execution context disposal', async () => { + const { page } = getTestState(); + + await page.evaluateOnNewDocument(() => (globalThis.__RELOADED = true)); + await page.waitForFunction(() => { + if (!globalThis.__RELOADED) window.location.reload(); + return true; + }); + }); + it('should poll on interval', async () => { + const { page } = getTestState(); + + let success = false; + const startTime = Date.now(); + const polling = 100; + const watchdog = page + .waitForFunction(() => globalThis.__FOO === 'hit', { polling }) + .then(() => (success = true)); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(() => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + expect(Date.now() - startTime).not.toBeLessThan(polling / 2); + }); + it('should poll on interval async', async () => { + const { page } = getTestState(); + let success = false; + const startTime = Date.now(); + const polling = 100; + const watchdog = page + .waitForFunction(async () => globalThis.__FOO === 'hit', { polling }) + .then(() => (success = true)); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(async () => + document.body.appendChild(document.createElement('div')) + ); + 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(() => globalThis.__FOO === 'hit', { + polling: 'mutation', + }) + .then(() => (success = true)); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(() => + 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 () => globalThis.__FOO === 'hit', { + polling: 'mutation', + }) + .then(() => (success = true)); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(async () => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + }); + it('should poll on raf', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction(() => globalThis.__FOO === 'hit', { + polling: 'raf', + }); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + await watchdog; + }); + it('should poll on raf async', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction( + async () => globalThis.__FOO === 'hit', + { + polling: 'raf', + } + ); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + await watchdog; + }); + itFailsFirefox('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 = null; + await Promise.all([ + page + .waitForFunction(() => globalThis.__FOO === 'hit', { polling: 'raf' }) + .catch((error_) => (error = error_)), + page.evaluate(() => (globalThis.__FOO = 'hit')), + ]); + expect(error).toBe(null); + }); + it('should throw on bad polling value', async () => { + const { page } = getTestState(); + + let error = null; + try { + await page.waitForFunction(() => !!document.body, { + polling: 'unknown', + }); + } catch (error_) { + error = error_; + } + expect(error).toBeTruthy(); + expect(error.message).toContain('polling'); + }); + it('should throw negative polling interval', async () => { + const { page } = getTestState(); + + let error = null; + try { + await page.waitForFunction(() => !!document.body, { polling: -10 }); + } catch (error_) { + error = error_; + } + expect(error).toBeTruthy(); + 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(() => 5)).jsonValue()).toBe(5); + }); + it('should return the window as a success value', async () => { + const { page } = getTestState(); + + expect(await page.waitForFunction(() => 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.parentElement, {}, div) + .then(() => (resolved = true)); + expect(resolved).toBe(false); + await page.evaluate((element: HTMLElement) => element.remove(), div); + await waitForFunction; + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForFunction('false', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('waiting for function failed: timeout'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(1); + let error = null; + await page.waitForFunction('false').catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + expect(error.message).toContain('waiting for function failed: timeout'); + }); + it('should disable timeout when its set to 0', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction( + () => { + globalThis.__counter = (globalThis.__counter || 0) + 1; + return globalThis.__injected; + }, + { timeout: 0, polling: 10 } + ); + await page.waitForFunction(() => globalThis.__counter > 10); + await page.evaluate(() => (globalThis.__injected = true)); + await watchdog; + }); + it('should survive cross-process navigation', async () => { + const { page, server } = getTestState(); + + let fooFound = false; + const waitForFunction = page + .waitForFunction('globalThis.__FOO === 1') + .then(() => (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(() => (globalThis.__FOO = 1)); + await waitForFunction; + expect(fooFound).toBe(true); + }); + it('should survive navigations', async () => { + const { page, server } = getTestState(); + + const watchdog = page.waitForFunction(() => globalThis.__done); + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/consolelog.html'); + await page.evaluate(() => (globalThis.__done = true)); + await watchdog; + }); + }); + + 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) => + 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'); + }); + + itFailsFirefox('should work with removed MutationObserver', async () => { + const { page } = getTestState(); + + await page.evaluate(() => delete window.MutationObserver); + const [handle] = await Promise.all([ + page.waitForSelector('.zombo'), + page.setContent(`
anything
`), + ]); + expect( + await page.evaluate((x: HTMLElement) => 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 eHandle + .getProperty('tagName') + .then((e) => e.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( + () => + (document.querySelector('span').innerHTML = '

') + ); + await watchdog; + }); + + itFailsFirefox( + 'Page.waitForSelector is shortcut for main frame', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.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.executionContext().frame()).toBe(page.mainFrame()); + } + ); + + itFailsFirefox('should run in specified frame', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.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.executionContext().frame()).toBe(frame2); + }); + + itFailsFirefox('should throw when frame is detached', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError = null; + const waitPromise = frame + .waitForSelector('.box') + .catch((error) => (waitError = error)); + await utils.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(() => (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 visible', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('div', { visible: true }) + .then(() => (divFound = true)); + await page.setContent( + `
1
` + ); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divFound).toBe(false); + await page.evaluate(() => + 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('div#inner', { visible: true }) + .then(() => (divVisible = true)); + await page.setContent( + `
hi
` + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + 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(`
`); + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + 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(`
`); + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + 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(`
`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divRemoved = true)); + await page.waitForSelector('div'); // do a round trip + expect(divRemoved).toBe(false); + await page.evaluate(() => 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('non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForSelector('div', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `div` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const { page } = getTestState(); + + await page.setContent(`
`); + let error = null; + await page + .waitForSelector('div', { hidden: true, timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `div` to be hidden failed: timeout' + ); + }); + + it('should respond to node attribute mutation', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('.zombo') + .then(() => (divFound = true)); + await page.setContent(`
`); + expect(divFound).toBe(false); + await page.evaluate( + () => (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: HTMLElement) => x.textContent, + await waitForSelector + ) + ).toBe('anything'); + }); + it('should have correct stack trace for timeout', async () => { + const { page } = getTestState(); + + let error; + await page + .waitForSelector('.zombo', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error.stack).toContain('waiting for selector `.zombo` failed'); + // The extension is ts here as Mocha maps back via sourcemaps. + expect(error.stack).toContain('waittask.spec.ts'); + }); + }); + + describe('Frame.waitForXPath', function () { + const addElement = (tag) => + 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: HTMLElement) => x.textContent, + await waitForXPath + ) + ).toBe('hello world '); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForXPath('//div', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for XPath `//div` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + itFailsFirefox('should run in specified frame', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.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.executionContext().frame()).toBe(frame2); + }); + itFailsFirefox('should throw when frame is detached', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError = null; + const waitPromise = frame + .waitForXPath('//*[@class="box"]') + .catch((error) => (waitError = error)); + await utils.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(`
`); + const waitForXPath = page + .waitForXPath('//div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForXPath('//div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('display', 'none') + ); + expect(await waitForXPath).toBe(true); + expect(divHidden).toBe(true); + }); + 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: HTMLElement) => 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: HTMLElement) => x.textContent, + await waitForXPath + ) + ).toBe('some text'); + }); + }); +}); diff --git a/test/worker.spec.ts b/test/worker.spec.ts new file mode 100644 index 0000000..b4f81db --- /dev/null +++ b/test/worker.spec.ts @@ -0,0 +1,126 @@ +/** + * 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; +import { WebWorker } from '../lib/cjs/puppeteer/common/WebWorker.js'; +import { ConsoleMessage } from '../lib/cjs/puppeteer/common/ConsoleMessage.js'; +const { waitEvent } = utils; + +describeFailsFirefox('Workers', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.workers', async () => { + const { page, server } = getTestState(); + + await Promise.all([ + new Promise((x) => page.once('workercreated', x)), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + const worker = page.workers()[0]; + expect(worker.url()).toContain('worker.js'); + + expect(await worker.evaluate(() => globalThis.workerFunction())).toBe( + 'worker function result' + ); + + await page.goto(server.EMPTY_PAGE); + expect(page.workers().length).toBe(0); + }); + it('should emit created and destroyed events', async () => { + const { page } = getTestState(); + + const workerCreatedPromise = new Promise((x) => + page.once('workercreated', x) + ); + const workerObj = await page.evaluateHandle( + () => new Worker('data:text/javascript,1') + ); + const worker = await workerCreatedPromise; + const workerThisObj = await worker.evaluateHandle(() => this); + const workerDestroyedPromise = new Promise((x) => + page.once('workerdestroyed', x) + ); + await page.evaluate( + (workerObj: Worker) => workerObj.terminate(), + workerObj + ); + expect(await workerDestroyedPromise).toBe(worker); + const error = await workerThisObj + .getProperty('self') + .catch((error) => 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(() => 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 = new Promise((x) => + page.on('console', x) + ); + await page.evaluate( + () => 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().length).toBe(4); + expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe( + 'null' + ); + }); + it('should have an execution context', async () => { + const { page } = getTestState(); + + const workerCreatedPromise = new Promise((x) => + page.once('workercreated', x) + ); + await page.evaluate( + () => 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 = new Promise((x) => page.on('pageerror', x)); + await page.evaluate( + () => + 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/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..10d347f --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "allowJs": true, + "checkJs": true, + "target": "ES2019", + "moduleResolution": "node", + "module": "ES2015", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true, + "sourceMap": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e121a14 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +/** + * This configuration only exists for the API Extractor tool. See the details in + * CONTRIBUTING.md that describes our TypeScript setup. +*/ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "module": "esnext" + }, + "include": ["src"] +} diff --git a/typescript-if-required.js b/typescript-if-required.js new file mode 100644 index 0000000..96e6b54 --- /dev/null +++ b/typescript-if-required.js @@ -0,0 +1,61 @@ +/** + * 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 child_process = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const { promisify } = require('util'); + +const exec = promisify(child_process.exec); +const fsAccess = promisify(fs.access); + +const fileExists = async (filePath) => + fsAccess(filePath) + .then(() => true) + .catch(() => false); +/* + + * Now Puppeteer is built with TypeScript, we need to ensure that + * locally we have the generated output before trying to install. + * + * For users installing puppeteer this is fine, they will have the + * generated lib/ directory as we ship it when we publish to npm. + * + * However, if you're cloning the repo to contribute, you won't have the + * generated lib/ directory so this script checks if we need to run + * TypeScript first to ensure the output exists and is in the right + * place. + */ +async function compileTypeScript() { + return exec('npm run tsc').catch((error) => { + console.error('Error running TypeScript', error); + process.exit(1); + }); +} + +async function compileTypeScriptIfRequired() { + const libPath = path.join(__dirname, 'lib'); + const libExists = await fileExists(libPath); + if (libExists) return; + + console.log('Puppeteer:', 'Compiling TypeScript...'); + await compileTypeScript(); +} + +// It's being run as node typescript-if-required.js, not require('..') +if (require.main === module) compileTypeScriptIfRequired(); + +module.exports = compileTypeScriptIfRequired; diff --git a/utils/ESTreeWalker.js b/utils/ESTreeWalker.js new file mode 100644 index 0000000..1c6c6d4 --- /dev/null +++ b/utils/ESTreeWalker.js @@ -0,0 +1,135 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @unrestricted + */ +class ESTreeWalker { + /** + * @param {function(!ESTree.Node):(!Object|undefined)} beforeVisit + * @param {function(!ESTree.Node)=} afterVisit + */ + constructor(beforeVisit, afterVisit) { + this._beforeVisit = beforeVisit; + this._afterVisit = afterVisit || new Function(); + } + + /** + * @param {!ESTree.Node} ast + */ + walk(ast) { + this._innerWalk(ast, null); + } + + /** + * @param {!ESTree.Node} node + * @param {?ESTree.Node} parent + */ + _innerWalk(node, parent) { + if (!node) return; + node.parent = parent; + + if (this._beforeVisit.call(null, node) === ESTreeWalker.SkipSubtree) { + this._afterVisit.call(null, node); + return; + } + + const walkOrder = ESTreeWalker._walkOrder[node.type]; + if (!walkOrder) return; + + if (node.type === 'TemplateLiteral') { + const templateLiteral = /** @type {!ESTree.TemplateLiteralNode} */ (node); + const expressionsLength = templateLiteral.expressions.length; + for (let i = 0; i < expressionsLength; ++i) { + this._innerWalk(templateLiteral.quasis[i], templateLiteral); + this._innerWalk(templateLiteral.expressions[i], templateLiteral); + } + this._innerWalk( + templateLiteral.quasis[expressionsLength], + templateLiteral + ); + } else { + for (let i = 0; i < walkOrder.length; ++i) { + const entity = node[walkOrder[i]]; + if (Array.isArray(entity)) this._walkArray(entity, node); + else this._innerWalk(entity, node); + } + } + + this._afterVisit.call(null, node); + } + + /** + * @param {!Array.} nodeArray + * @param {?ESTree.Node} parentNode + */ + _walkArray(nodeArray, parentNode) { + for (let i = 0; i < nodeArray.length; ++i) + this._innerWalk(nodeArray[i], parentNode); + } +} + +/** @typedef {!Object} ESTreeWalker.SkipSubtree */ +ESTreeWalker.SkipSubtree = {}; + +/** @enum {!Array.} */ +ESTreeWalker._walkOrder = { + AwaitExpression: ['argument'], + ArrayExpression: ['elements'], + ArrowFunctionExpression: ['params', 'body'], + AssignmentExpression: ['left', 'right'], + AssignmentPattern: ['left', 'right'], + BinaryExpression: ['left', 'right'], + BlockStatement: ['body'], + BreakStatement: ['label'], + CallExpression: ['callee', 'arguments'], + CatchClause: ['param', 'body'], + ClassBody: ['body'], + ClassDeclaration: ['id', 'superClass', 'body'], + ClassExpression: ['id', 'superClass', 'body'], + ConditionalExpression: ['test', 'consequent', 'alternate'], + ContinueStatement: ['label'], + DebuggerStatement: [], + DoWhileStatement: ['body', 'test'], + EmptyStatement: [], + ExpressionStatement: ['expression'], + ForInStatement: ['left', 'right', 'body'], + ForOfStatement: ['left', 'right', 'body'], + ForStatement: ['init', 'test', 'update', 'body'], + FunctionDeclaration: ['id', 'params', 'body'], + FunctionExpression: ['id', 'params', 'body'], + Identifier: [], + IfStatement: ['test', 'consequent', 'alternate'], + LabeledStatement: ['label', 'body'], + Literal: [], + LogicalExpression: ['left', 'right'], + MemberExpression: ['object', 'property'], + MethodDefinition: ['key', 'value'], + NewExpression: ['callee', 'arguments'], + ObjectExpression: ['properties'], + ObjectPattern: ['properties'], + ParenthesizedExpression: ['expression'], + Program: ['body'], + Property: ['key', 'value'], + ReturnStatement: ['argument'], + SequenceExpression: ['expressions'], + Super: [], + SwitchCase: ['test', 'consequent'], + SwitchStatement: ['discriminant', 'cases'], + TaggedTemplateExpression: ['tag', 'quasi'], + TemplateElement: [], + TemplateLiteral: ['quasis', 'expressions'], + ThisExpression: [], + ThrowStatement: ['argument'], + TryStatement: ['block', 'handler', 'finalizer'], + UnaryExpression: ['argument'], + UpdateExpression: ['argument'], + VariableDeclaration: ['declarations'], + VariableDeclarator: ['id', 'init'], + WhileStatement: ['test', 'body'], + WithStatement: ['object', 'body'], + YieldExpression: ['argument'], +}; + +module.exports = ESTreeWalker; diff --git a/utils/apply_next_version.js b/utils/apply_next_version.js new file mode 100644 index 0000000..cd62839 --- /dev/null +++ b/utils/apply_next_version.js @@ -0,0 +1,32 @@ +const path = require('path'); +const fs = require('fs'); +const execSync = require('child_process').execSync; + +// Compare current HEAD to upstream main SHA. +// If they are not equal - refuse to publish since +// we're not tip-of-tree. +const upstream_sha = execSync( + `git ls-remote https://github.com/puppeteer/puppeteer --tags main | cut -f1` +).toString('utf8'); +const current_sha = execSync(`git rev-parse HEAD`).toString('utf8'); +if (upstream_sha.trim() !== current_sha.trim()) { + console.log('REFUSING TO PUBLISH: this is not tip-of-tree!'); + process.exit(1); + return; +} + +const package = require('../package.json'); +let version = package.version; +const dashIndex = version.indexOf('-'); +if (dashIndex !== -1) version = version.substring(0, dashIndex); +version += '-next.' + Date.now(); +console.log('Setting version to ' + version); +package.version = version; +fs.writeFileSync( + path.join(__dirname, '..', 'package.json'), + JSON.stringify(package, undefined, 2) + '\n' +); + +console.log( + 'IMPORTANT: you should update the pinned version of devtools-protocol to match the new revision.' +); diff --git a/utils/bisect.js b/utils/bisect.js new file mode 100644 index 0000000..d81fdec --- /dev/null +++ b/utils/bisect.js @@ -0,0 +1,229 @@ +#!/usr/bin/env node +/** + * 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. + */ + +const URL = require('url'); +const debug = require('debug'); +const pptr = require('..'); +const browserFetcher = pptr.createBrowserFetcher(); +const path = require('path'); +const fs = require('fs'); +const { fork } = require('child_process'); + +const COLOR_RESET = '\x1b[0m'; +const COLOR_RED = '\x1b[31m'; +const COLOR_GREEN = '\x1b[32m'; +const COLOR_YELLOW = '\x1b[33m'; + +const argv = require('minimist')(process.argv.slice(2), {}); + +const help = ` +Usage: + node bisect.js --good --bad +``` + +You can find the library on `window.mitt`. + +## Usage + +```js +import mitt from 'mitt' + +const emitter = mitt() + +// listen to an event +emitter.on('foo', e => console.log('foo', e) ) + +// listen to all events +emitter.on('*', (type, e) => console.log(type, e) ) + +// fire an event +emitter.emit('foo', { a: 'b' }) + +// clearing all events +emitter.all.clear() + +// working with handler references: +function onFoo() {} +emitter.on('foo', onFoo) // listen +emitter.off('foo', onFoo) // unlisten +``` + +### Typescript + +```ts +import mitt from 'mitt'; +const emitter: mitt.Emitter = mitt(); +``` + +## Examples & Demos + + + Preact + Mitt Codepen Demo +
+ preact + mitt preview +
+ +* * * + +## API + + + +#### Table of Contents + +- [mitt](#mitt) +- [all](#all) +- [on](#on) + - [Parameters](#parameters) +- [off](#off) + - [Parameters](#parameters-1) +- [emit](#emit) + - [Parameters](#parameters-2) + +### mitt + +Mitt: Tiny (~200b) functional event emitter / pubsub. + +Returns **Mitt** + +### all + +A Map of event names to registered handler functions. + +### on + +Register an event handler for the given type. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `"*"` for all events +- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event + +### off + +Remove an event handler for the given type. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to unregister `handler` from, or `"*"` +- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler function to remove + +### emit + +Invoke all handlers for the given type. +If present, `"*"` handlers are invoked after type-matched handlers. + +Note: Manually firing "\*" handlers is not supported. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** The event type to invoke +- `evt` **Any?** Any value (object is recommended and powerful), passed to each handler + +## Contribute + +First off, thanks for taking the time to contribute! +Now, take a moment to be sure your contributions make sense to everyone else. + +### Reporting Issues + +Found a problem? Want a new feature? First of all see if your issue or idea has [already been reported](../../issues). +If don't, just open a [new clear and descriptive issue](../../issues/new). + +### Submitting pull requests + +Pull requests are the greatest contributions, so be sure they are focused in scope, and do avoid unrelated commits. + +- Fork it! +- Clone your fork: `git clone https://github.com//mitt` +- Navigate to the newly cloned directory: `cd mitt` +- Create a new branch for the new feature: `git checkout -b my-new-feature` +- Install the tools necessary for development: `npm install` +- Make your changes. +- Commit your changes: `git commit -am 'Add some feature'` +- Push to the branch: `git push origin my-new-feature` +- Submit a pull request with full remarks documenting your changes. + +## License + +[MIT License](https://opensource.org/licenses/MIT) © [Jason Miller](https://jasonformat.com/) diff --git a/vendor/mitt/dist/mitt.es.js b/vendor/mitt/dist/mitt.es.js new file mode 100644 index 0000000..889e272 --- /dev/null +++ b/vendor/mitt/dist/mitt.es.js @@ -0,0 +1,2 @@ +export default function(n){return{all:n=n||new Map,on:function(t,e){var i=n.get(t);i&&i.push(e)||n.set(t,[e])},off:function(t,e){var i=n.get(t);i&&i.splice(i.indexOf(e)>>>0,1)},emit:function(t,e){(n.get(t)||[]).slice().map(function(n){n(e)}),(n.get("*")||[]).slice().map(function(n){n(t,e)})}}} +//# sourceMappingURL=mitt.es.js.map diff --git a/vendor/mitt/dist/mitt.es.js.map b/vendor/mitt/dist/mitt.es.js.map new file mode 100644 index 0000000..6576278 --- /dev/null +++ b/vendor/mitt/dist/mitt.es.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.es.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array;\nexport type WildCardEventHandlerList = Array;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton(type: EventType, handler: Handler): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff(type: EventType, handler: Handler): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"} \ No newline at end of file diff --git a/vendor/mitt/dist/mitt.js b/vendor/mitt/dist/mitt.js new file mode 100644 index 0000000..2bd0cf9 --- /dev/null +++ b/vendor/mitt/dist/mitt.js @@ -0,0 +1,2 @@ +module.exports=function(n){return{all:n=n||new Map,on:function(e,t){var i=n.get(e);i&&i.push(t)||n.set(e,[t])},off:function(e,t){var i=n.get(e);i&&i.splice(i.indexOf(t)>>>0,1)},emit:function(e,t){(n.get(e)||[]).slice().map(function(n){n(t)}),(n.get("*")||[]).slice().map(function(n){n(e,t)})}}}; +//# sourceMappingURL=mitt.js.map diff --git a/vendor/mitt/dist/mitt.js.map b/vendor/mitt/dist/mitt.js.map new file mode 100644 index 0000000..37f6f59 --- /dev/null +++ b/vendor/mitt/dist/mitt.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array;\nexport type WildCardEventHandlerList = Array;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton(type: EventType, handler: Handler): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff(type: EventType, handler: Handler): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"} \ No newline at end of file diff --git a/vendor/mitt/dist/mitt.modern.js b/vendor/mitt/dist/mitt.modern.js new file mode 100644 index 0000000..0777f6d --- /dev/null +++ b/vendor/mitt/dist/mitt.modern.js @@ -0,0 +1,2 @@ +export default function(e){return{all:e=e||new Map,on(t,n){const s=e.get(t);s&&s.push(n)||e.set(t,[n])},off(t,n){const s=e.get(t);s&&s.splice(s.indexOf(n)>>>0,1)},emit(t,n){(e.get(t)||[]).slice().map(e=>{e(n)}),(e.get("*")||[]).slice().map(e=>{e(t,n)})}}} +//# sourceMappingURL=mitt.modern.js.map diff --git a/vendor/mitt/dist/mitt.modern.js.map b/vendor/mitt/dist/mitt.modern.js.map new file mode 100644 index 0000000..5f669b2 --- /dev/null +++ b/vendor/mitt/dist/mitt.modern.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.modern.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array;\nexport type WildCardEventHandlerList = Array;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton(type: EventType, handler: Handler): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff(type: EventType, handler: Handler): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,GAAYC,EAAiBC,GAC5B,MAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,IAAaN,EAAiBC,GAC7B,MAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,KAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAKX,IAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAKX,IAAcA,EAAQD,EAAMU"} \ No newline at end of file diff --git a/vendor/mitt/dist/mitt.umd.js b/vendor/mitt/dist/mitt.umd.js new file mode 100644 index 0000000..ce0e005 --- /dev/null +++ b/vendor/mitt/dist/mitt.umd.js @@ -0,0 +1,2 @@ +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).mitt=n()}(this,function(){return function(e){return{all:e=e||new Map,on:function(n,t){var f=e.get(n);f&&f.push(t)||e.set(n,[t])},off:function(n,t){var f=e.get(n);f&&f.splice(f.indexOf(t)>>>0,1)},emit:function(n,t){(e.get(n)||[]).slice().map(function(e){e(t)}),(e.get("*")||[]).slice().map(function(e){e(n,t)})}}}}); +//# sourceMappingURL=mitt.umd.js.map diff --git a/vendor/mitt/dist/mitt.umd.js.map b/vendor/mitt/dist/mitt.umd.js.map new file mode 100644 index 0000000..642c894 --- /dev/null +++ b/vendor/mitt/dist/mitt.umd.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.umd.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array;\nexport type WildCardEventHandlerList = Array;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton(type: EventType, handler: Handler): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff(type: EventType, handler: Handler): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"6LAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"} \ No newline at end of file diff --git a/vendor/mitt/index.d.ts b/vendor/mitt/index.d.ts new file mode 100644 index 0000000..64ac8c1 --- /dev/null +++ b/vendor/mitt/index.d.ts @@ -0,0 +1,21 @@ +export declare type EventType = string | symbol; +export declare type Handler = (event?: T) => void; +export declare type WildcardHandler = (type: EventType, event?: any) => void; +export declare type EventHandlerList = Array; +export declare type WildCardEventHandlerList = Array; +export declare type EventHandlerMap = Map; +export interface Emitter { + all: EventHandlerMap; + on(type: EventType, handler: Handler): void; + on(type: '*', handler: WildcardHandler): void; + off(type: EventType, handler: Handler): void; + off(type: '*', handler: WildcardHandler): void; + emit(type: EventType, event?: T): void; + emit(type: '*', event?: any): void; +} +/** + * Mitt: Tiny (~200b) functional event emitter / pubsub. + * @name mitt + * @returns {Mitt} + */ +export default function mitt(all?: EventHandlerMap): Emitter; diff --git a/vendor/mitt/package.json b/vendor/mitt/package.json new file mode 100644 index 0000000..0105524 --- /dev/null +++ b/vendor/mitt/package.json @@ -0,0 +1,141 @@ +{ + "_from": "mitt@latest", + "_id": "mitt@2.1.0", + "_inBundle": false, + "_integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", + "_location": "/mitt", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "mitt@latest", + "name": "mitt", + "escapedName": "mitt", + "rawSpec": "latest", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "_shasum": "f740577c23176c6205b121b2973514eade1b2230", + "_spec": "mitt@latest", + "_where": "/Users/jacktfranklin/src/puppeteer", + "authors": [ + "Jason Miller " + ], + "bugs": { + "url": "https://github.com/developit/mitt/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Tiny 200b functional Event Emitter / pubsub.", + "devDependencies": { + "@types/chai": "^4.2.11", + "@types/mocha": "^7.0.2", + "@types/sinon": "^9.0.4", + "@types/sinon-chai": "^3.2.4", + "@typescript-eslint/eslint-plugin": "^3.0.1", + "@typescript-eslint/parser": "^3.0.1", + "chai": "^4.2.0", + "documentation": "^13.0.0", + "eslint": "^7.1.0", + "eslint-config-developit": "^1.2.0", + "esm": "^3.2.25", + "microbundle": "^0.12.3", + "mocha": "^8.0.1", + "npm-run-all": "^4.1.5", + "rimraf": "^3.0.2", + "sinon": "^9.0.2", + "sinon-chai": "^3.5.0", + "ts-node": "^8.10.2", + "typescript": "^3.9.3" + }, + "eslintConfig": { + "extends": [ + "developit", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module" + }, + "env": { + "browser": true, + "mocha": true, + "jest": false, + "es6": true + }, + "globals": { + "expect": true + }, + "rules": { + "semi": [ + 2, + "always" + ], + "jest/valid-expect": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0 + } + }, + "eslintIgnore": [ + "dist", + "index.d.ts" + ], + "esmodules": "dist/mitt.modern.js", + "files": [ + "src", + "dist", + "index.d.ts" + ], + "homepage": "https://github.com/developit/mitt", + "jsnext:main": "dist/mitt.es.js", + "keywords": [ + "events", + "eventemitter", + "emitter", + "pubsub" + ], + "license": "MIT", + "main": "dist/mitt.js", + "mocha": { + "extension": [ + "ts" + ], + "require": [ + "ts-node/register", + "esm" + ], + "spec": [ + "test/*_test.ts" + ] + }, + "module": "dist/mitt.es.js", + "name": "mitt", + "repository": { + "type": "git", + "url": "git+https://github.com/developit/mitt.git" + }, + "scripts": { + "build": "npm-run-all --silent clean -p bundle -s docs", + "bundle": "microbundle", + "clean": "rimraf dist", + "docs": "documentation readme src/index.ts --section API -q --parse-extension ts", + "lint": "eslint src test --ext ts --ext js", + "mocha": "mocha test", + "release": "npm run -s build -s && npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", + "test": "npm-run-all --silent typecheck lint mocha test-types", + "test-types": "tsc test/test-types-compilation.ts --noEmit", + "typecheck": "tsc --noEmit" + }, + "source": "src/index.ts", + "typings": "index.d.ts", + "umd:main": "dist/mitt.umd.js", + "version": "2.1.0" +} diff --git a/vendor/mitt/src/index.ts b/vendor/mitt/src/index.ts new file mode 100644 index 0000000..7b5342f --- /dev/null +++ b/vendor/mitt/src/index.ts @@ -0,0 +1,92 @@ + +/** + * @public + */ +export type EventType = string | symbol; + +// An event handler can take an optional event argument +// and should not return a value +/** + * @public + */ +export type Handler = (event?: T) => void; +export type WildcardHandler = (type: EventType, event?: any) => void; + +// An array of all currently registered event handlers for a type +export type EventHandlerList = Array; +export type WildCardEventHandlerList = Array; + +// A map of event types and their corresponding event handlers. +export type EventHandlerMap = Map; + +export interface Emitter { + all: EventHandlerMap; + + on(type: EventType, handler: Handler): void; + on(type: '*', handler: WildcardHandler): void; + + off(type: EventType, handler: Handler): void; + off(type: '*', handler: WildcardHandler): void; + + emit(type: EventType, event?: T): void; + emit(type: '*', event?: any): void; +} + +/** + * Mitt: Tiny (~200b) functional event emitter / pubsub. + * @name mitt + * @returns {Mitt} + */ +export default function mitt(all?: EventHandlerMap): Emitter { + all = all || new Map(); + + return { + + /** + * A Map of event names to registered handler functions. + */ + all, + + /** + * Register an event handler for the given type. + * @param {string|symbol} type Type of event to listen for, or `"*"` for all events + * @param {Function} handler Function to call in response to given event + * @memberOf mitt + */ + on(type: EventType, handler: Handler) { + const handlers = all.get(type); + const added = handlers && handlers.push(handler); + if (!added) { + all.set(type, [handler]); + } + }, + + /** + * Remove an event handler for the given type. + * @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` + * @param {Function} handler Handler function to remove + * @memberOf mitt + */ + off(type: EventType, handler: Handler) { + const handlers = all.get(type); + if (handlers) { + handlers.splice(handlers.indexOf(handler) >>> 0, 1); + } + }, + + /** + * Invoke all handlers for the given type. + * If present, `"*"` handlers are invoked after type-matched handlers. + * + * Note: Manually firing "*" handlers is not supported. + * + * @param {string|symbol} type The event type to invoke + * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler + * @memberOf mitt + */ + emit(type: EventType, evt: T) { + ((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); }); + ((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); }); + } + }; +} diff --git a/vendor/tsconfig.cjs.json b/vendor/tsconfig.cjs.json new file mode 100644 index 0000000..f73a8d5 --- /dev/null +++ b/vendor/tsconfig.cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "exclude": [ + "mitt/dist" + ], + "compilerOptions": { + "composite": true, + "outDir": "../lib/cjs/vendor", + "module": "CommonJS" + } +} diff --git a/vendor/tsconfig.esm.json b/vendor/tsconfig.esm.json new file mode 100644 index 0000000..1f0bae8 --- /dev/null +++ b/vendor/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "exclude": [ + "mitt/dist" + ], + "compilerOptions": { + "composite": true, + "outDir": "../lib/esm/vendor", + "module": "esnext" + } +} diff --git a/versions.js b/versions.js new file mode 100644 index 0000000..716c452 --- /dev/null +++ b/versions.js @@ -0,0 +1,41 @@ +/** + * 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 Chromium version => Puppeteer version. + // In Chromium roll patches, use 'NEXT' for the Puppeteer version. + ['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'], +]); + +module.exports = versionsPerRelease; diff --git a/web-test-runner.config.js b/web-test-runner.config.js new file mode 100644 index 0000000..39a9b01 --- /dev/null +++ b/web-test-runner.config.js @@ -0,0 +1,44 @@ +/** + * 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 { chromeLauncher } = require('@web/test-runner-chrome'); + +module.exports = { + files: ['test-browser/**/*.spec.js'], + browserStartTimeout: 60 * 1000, + browsers: [ + chromeLauncher({ + async createPage({ browser }) { + const page = await browser.newPage(); + page.evaluateOnNewDocument((wsEndpoint) => { + window.__ENV__ = { wsEndpoint }; + }, browser.wsEndpoint()); + + return page; + }, + }), + ], + plugins: [ + { + // turn expect UMD into an es module + name: 'esmify-expect', + transform(context) { + if (context.path === '/node_modules/expect/build-es5/index.js') { + return `const module = {}; const exports = {};\n${context.body};\n export default module.exports;`; + } + }, + }, + ], +}; From d0515f37e971ea0440ff3843d4252c17ce3aa5a0 Mon Sep 17 00:00:00 2001 From: tasneemkoushar Date: Sat, 29 May 2021 20:49:53 +0530 Subject: [PATCH 2/3] fix: modified --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bcb8be3..431ec10 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "puppeteer", + "name": "new-pptr.dev", "version": "9.1.1-post", "description": "A high-level API to control headless Chrome over the DevTools Protocol", "main": "./cjs-entry.js", "types": "lib/types.d.ts", - "repository": "github:puppeteer/puppeteer", + "repository": "https://github.com/tasneemkoushar/pptr.dev", "engines": { "node": ">=10.18.1" }, From 7cfa7d90d9ff2db2f95ece86866467cc51aa64e0 Mon Sep 17 00:00:00 2001 From: tasneemkoushar Date: Sat, 29 May 2021 20:59:35 +0530 Subject: [PATCH 3/3] fix: modified2 --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 431ec10..6423cb9 100644 --- a/package.json +++ b/package.json @@ -115,8 +115,7 @@ }, "husky": { "hooks": { - "commit-msg": "commitlint --env HUSKY_GIT_PARAMS", - "pre-push": "npm run ensure-pinned-deps" + "commit-msg": "commitlint --env HUSKY_GIT_PARAMS" } } }